From 1d6c9bdfe6ba970b53c2d66c7ef0e4ea1169daf7 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Mon, 20 Jul 2026 13:13:58 -0400 Subject: [PATCH] perf: rate limiter cleanup goroutine, DB pool constraints - ipLimiter evicts stale entries after 10 min of inactivity via 5 min sweep - DB pool capped at 25 max open, 5 idle, 15 min lifetime --- internal/db/db.go | 6 ++++++ internal/middleware/ratelimit.go | 26 +++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/internal/db/db.go b/internal/db/db.go index 0275914..ae77487 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "time" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config" @@ -24,6 +25,11 @@ func New(cfg *config.Config) (*DB, error) { return nil, fmt.Errorf("ping database: %w", err) } + // ponytail: prevent unbounded connection growth under burst load + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(15 * time.Minute) + return &DB{db}, nil } diff --git a/internal/middleware/ratelimit.go b/internal/middleware/ratelimit.go index eae1129..fc45cc3 100644 --- a/internal/middleware/ratelimit.go +++ b/internal/middleware/ratelimit.go @@ -4,6 +4,7 @@ import ( "net/http" "strings" "sync" + "time" "golang.org/x/time/rate" ) @@ -12,16 +13,21 @@ import ( type ipLimiter struct { mu sync.Mutex limiters map[string]*rate.Limiter + lastUsed map[string]time.Time rate rate.Limit burst int } func newIPLimiter(r rate.Limit, burst int) *ipLimiter { - return &ipLimiter{ + l := &ipLimiter{ limiters: make(map[string]*rate.Limiter), + lastUsed: make(map[string]time.Time), rate: r, burst: burst, } + // ponytail: evict inactive limiters every 5 minutes to prevent unbounded map growth + go l.cleanup() + return l } func (l *ipLimiter) getLimiter(ip string) *rate.Limiter { @@ -33,9 +39,27 @@ func (l *ipLimiter) getLimiter(ip string) *rate.Limiter { lim = rate.NewLimiter(l.rate, l.burst) l.limiters[ip] = lim } + l.lastUsed[ip] = time.Now() return lim } +// cleanup runs in a background goroutine and removes limiters that haven't +// been accessed in 10 minutes. +func (l *ipLimiter) cleanup() { + for { + time.Sleep(5 * time.Minute) + l.mu.Lock() + now := time.Now() + for ip, last := range l.lastUsed { + if now.Sub(last) > 10*time.Minute { + delete(l.limiters, ip) + delete(l.lastUsed, ip) + } + } + l.mu.Unlock() + } +} + // RateLimit returns middleware that limits requests per IP. // requestsPerSecond is the sustained rate, burst is the max burst size. func RateLimit(requestsPerSecond float64, burst int) func(http.Handler) http.Handler {