You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit fd7679b
Browse filesBrowse the repository at this point in the historyBrowse files
fix(rate-limit): the backoff a strategy means, an outage named as one, and per-type buckets
Three wire defects from #1004/#1006:
- EWMA's period is a half-life, so the wait is period*log2(score/rate), not
period. The Lua script returns its score, and the limiter computes the wait
(rate 6, period 60s, score 24 -> 120s where the old code said 60s).
- A limiter outage answered "rate-limited: slow down"; every client backed off
for our fault. It now answers "error: rate limiter unavailable", still
fail-closed.
- The message bucket is keyed by message type, read from a bounded frame
prefix, so a REQ is no longer refused because its EVENTs filled the bucket.
hit() returns { limited, retryAfterMs? } so each strategy answers in its own
terms; callers without an estimate keep sending the period.
fix(rate-limit): send the backoff the strategy actually means, name an outage as an error, and bucket messages by type
6
+
7
+
Three defects on one wire path, from the spike in #1004.
8
+
9
+
**The EWMA backoff was a half-life, sent as a delay.**`RateLimit.period` is the decay half-life under `ewma`, so the time until a limit lifts is `period · log2(R_new / rate)`, not `period`. With `rate: 6, period: 60000`, a client four times over the limit was told to wait 60 s and needed 120 s. The Lua script now returns its score instead of a yes/no, and the limiter returns `period · log2(score / rate)`. It was only accidentally safe before: the hash carried a TTL of exactly `period`, so a quiet client's state expired anyway — an unstated coupling that would break the moment anyone lengthened the TTL to keep state warm.
10
+
11
+
**A limiter outage was announced as `rate-limited`.** A failing backend made every connected client back off for a fault on this side. It now answers `error: rate limiter unavailable` and still rejects the event; fail-closed behaviour is unchanged.
12
+
13
+
**The message bucket ignored the message type.** One bucket per client meant a `REQ` could be refused because its `EVENT`s had filled it — the wrong message throttled, and the wrong one told to back off. The bucket key now carries the type, read from a bounded prefix of the frame because the limiter runs before validation.
14
+
15
+
`IRateLimiter.hit` returns `IRateLimiterResult` (`limited`, plus `retryAfterMs` when the strategy can estimate one) so each strategy answers in its own terms; a caller with no estimate keeps sending the period.
0 commit comments