Repository navigation
perf: count RetryBudget and the rate-mode window in time buckets - #181
Merged
Merged
Conversation
The window now moves in steps of ttl / 10, so a deposit or withdrawal stops counting between 0.9 * ttl and ttl after it was made. Memory stays constant and no call has to drop a backlog of timestamps under the lock.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
RetryBudgetused to keep one timestamp per deposit and per withdrawal in two deques, and trimmed them on every call. It now keeps counters in ten buckets ofttl / 10, plus running totals. A call moves the current bucket forward if time has moved on, then bumps one bucket and one total. The lock is held for a fixed amount of work no matter how much traffic the budget has seen.The circuit breaker's rate-mode window (
_RollingWindow) was already split into ten buckets, but it summed all ten on every recorded outcome. It now keeps running totals too, so recording an outcome and reading the rate are both constant-time. Its window semantics are unchanged, and nothing outside the class was touched.The constructor parameters, their defaults, the 0.22.0 validation and the injectable clock are all unchanged.
Why
At 50k requests per second with the default 10 s
ttl, the budget held about 500k timestamps (17 MB). After a quiet spell longer thanttl, the next call had to drop all of them while holding the lock, and every thread and event loop sharing the budget waited for it. Bucketed counters need about 1.3 KB at any traffic level, and the worst call does at most ten bucket resets.Measurements
Simulated 50k rps over a 10 s window with the injectable clock, CPython 3.14.7 on an Apple Silicon laptop. Per-operation numbers are best-of-7 timeit with the clock-advance cost subtracted. The machine was busy with other work, so the ranges come from four alternating before/after runs.
RetryBudget.depositRetryBudget.try_withdraw_CircuitBreakerState.record)_RollingWindow.totals)RetryBudgetmemory after 10 s at 50k rpsRetryBudgetcall after an idle gap longer thanttlThe rate-mode window's memory was already constant and stays so. The slowest single steady-state call was in the millisecond range both before and after, which is scheduler preemption on a loaded machine, so those numbers are left out of the table.
Behaviour change for 0.22.0
RetryBudgetnow counts in steps ofttl / 10. A deposit or withdrawal stops counting between0.9 * ttlandttlafter it was made, instead of exactlyttlafter. With the defaultttl=10.0, a request can drop out of the budget up to one second early. Nothing is ever counted for longer thanttl.The rate-mode circuit breaker already moved its window in steps of
window_seconds / 10, so its behaviour does not change. Both are now stated indocs/resilience.md.Tests
ttl=10stops counting at 10.0 s, withdrawals expire with their bucket, and an expired bucket releases only its own counts. Two of them fail on main on purpose, because they describe the new edge.try_withdraw()answer must be one the reference allows when events expire anywhere between0.9 * ttlandttl. A similar property checks_RollingWindow.totalsagainst a timestamp reference. Both fail when the bucket width or the eviction is broken.ttland checks that the running totals still equal the bucket sums. It fails within one run on free-threaded 3.14t if the lock is removed.len(budget._deposits),len(budget._withdrawn)) now read the counters. No existing test about expiry timing had to change: they all step the clock by more than a bucket.