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
This commit is contained in:
2026-07-20 13:13:58 -04:00
parent 57aec2c6b3
commit 1d6c9bdfe6
2 changed files with 31 additions and 1 deletions
+6
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"time"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config" "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) 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 return &DB{db}, nil
} }
+25 -1
View File
@@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
"time"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
@@ -12,16 +13,21 @@ import (
type ipLimiter struct { type ipLimiter struct {
mu sync.Mutex mu sync.Mutex
limiters map[string]*rate.Limiter limiters map[string]*rate.Limiter
lastUsed map[string]time.Time
rate rate.Limit rate rate.Limit
burst int burst int
} }
func newIPLimiter(r rate.Limit, burst int) *ipLimiter { func newIPLimiter(r rate.Limit, burst int) *ipLimiter {
return &ipLimiter{ l := &ipLimiter{
limiters: make(map[string]*rate.Limiter), limiters: make(map[string]*rate.Limiter),
lastUsed: make(map[string]time.Time),
rate: r, rate: r,
burst: burst, 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 { 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) lim = rate.NewLimiter(l.rate, l.burst)
l.limiters[ip] = lim l.limiters[ip] = lim
} }
l.lastUsed[ip] = time.Now()
return lim 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. // RateLimit returns middleware that limits requests per IP.
// requestsPerSecond is the sustained rate, burst is the max burst size. // requestsPerSecond is the sustained rate, burst is the max burst size.
func RateLimit(requestsPerSecond float64, burst int) func(http.Handler) http.Handler { func RateLimit(requestsPerSecond float64, burst int) func(http.Handler) http.Handler {