mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-11-04 08:04:10 +00:00
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.
50 lines
1.4 KiB
Go
50 lines
1.4 KiB
Go
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)
|
|
}
|