mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-11-09 08:54:27 +00:00
fix(ratelimit): bound memory and CPU per request
The sustained gate stored one entry for every attempt, denied ones too, and summed all entries on each request. A flood grew memory with rate times interval and made each request slower. Both gates now keep only the newest limit plus one attempt times. A Lean proof shows this decides the same as the full count. One check dropped from 29,502 ns and 11 allocations to 43 ns and none. The go-ratecounter dependency is gone.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// attemptWindow decides "more than limit attempts in the trailing window" while
|
||||
// storing only the newest limit+1 attempt times. The attempt number limit+1
|
||||
// counted back from now lies inside the window exactly when the full count
|
||||
// exceeds limit (see theorems/Theorems/SlidingWindow.lean), so memory and time
|
||||
// per attempt stay constant however hard a client floods.
|
||||
type attemptWindow struct {
|
||||
now func() time.Time
|
||||
base time.Time // monotonic origin for stored offsets
|
||||
ring []int64 // offsets from base, grows to capacity then wraps
|
||||
mu sync.Mutex
|
||||
window time.Duration
|
||||
head int // index of the oldest stored attempt once the ring is full
|
||||
size int // limit+1: attempts kept; the ring grows lazily up to this
|
||||
}
|
||||
|
||||
func newAttemptWindow(window time.Duration, limit int) *attemptWindow {
|
||||
return &attemptWindow{
|
||||
now: time.Now,
|
||||
base: time.Now(),
|
||||
window: window,
|
||||
size: max(limit, 0) + 1,
|
||||
}
|
||||
}
|
||||
|
||||
// recordAndExceeds records an attempt now and reports whether the attempts in
|
||||
// the trailing window, this one included, outnumber the limit.
|
||||
func (w *attemptWindow) recordAndExceeds() bool {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
t := int64(w.now().Sub(w.base))
|
||||
if len(w.ring) < w.size {
|
||||
w.ring = append(w.ring, t)
|
||||
} else {
|
||||
w.ring[w.head] = t
|
||||
w.head = (w.head + 1) % len(w.ring)
|
||||
}
|
||||
if len(w.ring) < w.size {
|
||||
return false
|
||||
}
|
||||
return w.ring[w.head] > t-int64(w.window)
|
||||
}
|
||||
Reference in New Issue
Block a user