diff --git a/beamsync/server.go b/beamsync/server.go index a2f3559..7380fee 100644 --- a/beamsync/server.go +++ b/beamsync/server.go @@ -30,6 +30,8 @@ type EventCallback func(eventName string, data string) //go:embed ui/*.html ui/*.png var uiFS embed.FS +const rateLimitPruneInterval = 2 * time.Minute + // serverState holds per-instance connection tracking (no more package-level globals). type serverState struct { mu sync.Mutex @@ -98,13 +100,30 @@ type clientRateLimiter struct { } func newClientRateLimiter(limit int, window time.Duration) *clientRateLimiter { - return &clientRateLimiter{ + limiter := &clientRateLimiter{ limit: limit, window: window, maxClients: 4096, clients: make(map[string]*rateLimitState), now: time.Now, } + limiter.startBackgroundPruner() + return limiter +} + +func (l *clientRateLimiter) startBackgroundPruner() { + if l.limit <= 0 || l.window <= 0 { + return + } + + go func() { + ticker := time.NewTicker(rateLimitPruneInterval) + defer ticker.Stop() + + for range ticker.C { + l.prune(l.now()) + } + }() } func (l *clientRateLimiter) allow(client string) rateLimitDecision { @@ -117,8 +136,6 @@ func (l *clientRateLimiter) allow(client string) rateLimitDecision { l.mu.Lock() defer l.mu.Unlock() - l.prune(now) - state, ok := l.clients[client] if !ok || now.Sub(state.windowStart) >= l.window { if !ok { @@ -159,6 +176,9 @@ func (l *clientRateLimiter) allow(client string) rateLimitDecision { } func (l *clientRateLimiter) prune(now time.Time) { + l.mu.Lock() + defer l.mu.Unlock() + for client, state := range l.clients { if now.Sub(state.lastSeen) > 2*l.window { delete(l.clients, client)