2026-04-05 21:34:32 +00:00
|
|
|
package ws
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"time"
|
2026-08-16 17:05:06 +02:00
|
|
|
|
2026-08-28 06:54:32 +02:00
|
|
|
"github.com/J3vb/OwnCord/Server/syncutil"
|
2026-04-05 21:34:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TopicRateLimiter enforces per-topic throughput caps to prevent a single
|
|
|
|
|
// busy channel from saturating the broadcast loop and starving others.
|
|
|
|
|
type TopicRateLimiter struct {
|
2026-08-16 17:05:06 +02:00
|
|
|
mu syncutil.Mutex
|
2026-04-05 21:34:32 +00:00
|
|
|
buckets map[Topic]*tokenBucket
|
|
|
|
|
defaultRate int // messages per window
|
|
|
|
|
window time.Duration // window size
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tokenBucket is a simple sliding-window rate limiter per topic.
|
|
|
|
|
type tokenBucket struct {
|
|
|
|
|
tokens int
|
|
|
|
|
lastReset time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewTopicRateLimiter creates a rate limiter with the given default rate.
|
|
|
|
|
// rate is the maximum messages per window for any single topic.
|
|
|
|
|
func NewTopicRateLimiter(rate int, window time.Duration) *TopicRateLimiter {
|
|
|
|
|
return &TopicRateLimiter{
|
|
|
|
|
buckets: make(map[Topic]*tokenBucket),
|
|
|
|
|
defaultRate: rate,
|
|
|
|
|
window: window,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Allow checks whether a message to the given topic is within the rate limit.
|
|
|
|
|
// Returns true if allowed, false if the topic has exceeded its quota.
|
|
|
|
|
func (trl *TopicRateLimiter) Allow(topic Topic) bool {
|
|
|
|
|
trl.mu.Lock()
|
|
|
|
|
defer trl.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
b, ok := trl.buckets[topic]
|
|
|
|
|
if !ok {
|
|
|
|
|
b = &tokenBucket{tokens: trl.defaultRate, lastReset: now}
|
|
|
|
|
trl.buckets[topic] = b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset bucket if window has elapsed.
|
|
|
|
|
if now.Sub(b.lastReset) >= trl.window {
|
|
|
|
|
b.tokens = trl.defaultRate
|
|
|
|
|
b.lastReset = now
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if b.tokens <= 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
b.tokens--
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cleanup removes stale buckets that haven't been used recently.
|
|
|
|
|
// Call periodically to prevent unbounded memory growth.
|
|
|
|
|
func (trl *TopicRateLimiter) Cleanup(maxAge time.Duration) {
|
|
|
|
|
trl.mu.Lock()
|
|
|
|
|
defer trl.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
for topic, b := range trl.buckets {
|
|
|
|
|
if now.Sub(b.lastReset) > maxAge {
|
|
|
|
|
delete(trl.buckets, topic)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|