Repository navigation
fix(sentry): thin a device's retry loop so it can't spend the quota - #3400
Open
vpetersson-bot wants to merge 1 commit into
Open
vpetersson-bot wants to merge 1 commit into
vpetersson-bot wants to merge 1 commit into
Conversation
The project's Sentry quota is being consumed faster than it replenishes, so errors stop being accepted partway through each billing period and every issue's lastSeen freezes on the day the allowance ran out. The cost of that is not the missing events, it is that "it has not recurred since" stops being evidence of anything -- a regression can land and leave no record. What spends the quota is depth, not breadth. A device stuck in a retry loop repeats one error thousands of times while the rest of the fleet contributes a handful each: in a fully recorded day one board produced 1,761 of an issue's 1,762 events (ANTHIAS-3H) and another 1,063 of 1,093 (ANTHIAS-2D). Two boards crowded out every other issue. Count occurrences per issue per process over a one-hour window and keep the 1st, 2nd, 4th, 8th, ..., dropping what falls between two powers of two. A loop costs O(log n) events instead of n -- 1,761 becomes 11 -- and the magnitude is not lost, because each surviving event carries its true running count in a throttle.occurrence tag. Every device still reports every distinct issue it hits, so nothing can be hidden by this; only repetition of something already reported is. The gate runs last in before_send on purpose, so the transient noise filtered above it never spends a key's budget. Issues are keyed by exception type plus the innermost Anthias frame, or for logging-integration events by logger plus the unformatted message template -- the template being what keeps "Timed out waiting for UP message from <ForkPoolWorker-26>" and the same line for worker 27 one key rather than one per worker (ANTHIAS-1Q). An event matching neither shape is left unthrottled rather than given a coarse key that could merge unrelated issues. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
There was a problem hiding this comment.
Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.
|
This branch has not been deployed
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's wrong today
The project's Sentry quota is consumed faster than it replenishes, so errors stop being accepted partway through each billing period. The cost is not the missing events — it is that every issue's
lastSeenfreezes on the day the allowance ran out, so "it has not recurred since" stops being evidence of anything. A regression can land and leave no record.What spends the quota is depth, not breadth. A device stuck in a retry loop repeats one error thousands of times while the rest of the fleet contributes a handful each. In a fully recorded day, one board produced 1,761 of an issue's 1,762 events (ANTHIAS-3H) and another produced 1,063 of 1,093 (ANTHIAS-2D) — two boards crowding out every other issue.
Nothing bounds this today.
before_sendfilters transient noise (redis blips, DNS failures offline, yt-dlp download errors), but a genuine error repeated ten thousand times passes every one of those checks.What this does
Counts occurrences per issue, per process, over a one-hour window, and keeps the 1st, 2nd, 4th, 8th, 16th, … — dropping what falls between two powers of two. A loop costs O(log n) events instead of n: 1,761 becomes 11.
The magnitude is not lost with the events. Each surviving event carries its true running count in a
throttle.occurrencetag, so triage can still tell the 4th occurrence from the 4th distinct event.Two properties the design turns on:
before_send. Noise filtered above it is not a report and must not spend a key's budget — otherwise a loop of filtered redis blips would throttle the one real error raised alongside it.Issues are keyed by exception type plus the innermost Anthias frame (falling back to the innermost frame of any kind), or for logging-integration events by logger plus the unformatted message template. The template is what keeps "Timed out waiting for UP message from
<ForkPoolWorker-26>" and the same line for worker 27 a single key rather than one per worker (ANTHIAS-1Q). An event matching neither shape is left unthrottled rather than given a coarse key that could merge unrelated issues — this filter must never be the reason a real bug goes unseen.Bookkeeping is bounded at 512 live keys per process, evicting expired windows first and then the oldest; the overflow path can only let more events through, never fewer. The window clock is
time.monotonic, so a board whose wall clock jumps years when NTP first lands doesn't expire or freeze every window at once.How I know it's right
15 new tests in
tests/test_sentry.pycovering: the powers-of-two sequence; the occurrence tag (and that the first event is untagged, and that an existing tag mapping survives); that distinct issues don't share a budget; that noise dropped earlier spends no budget; that one log template is one key across its arguments while different templates are not; that an unkeyable event is never throttled; window expiry resetting the count; and both eviction paths.Full unit suite: 2402 passed. The one failure on this branch,
test_viewer.py::TestWaitForWaylandSocket::test_probe_reports_not_ready_for_a_non_socket_path, reproduces identically on untouchedmasterunderpytest -n autoand passes when run alone — a pre-existing parallel-run flake, unrelated to this change.ruff check,ruff format --checkandmypyare clean.A new module-level autouse fixture clears the throttle state around every test in
test_sentry.py; the counters are module scope, so without it a key counted by one test would still be counted in the next.Follow-up, not in this PR
Thinning the loops buys headroom but does not by itself bring the fleet under its allowance. The quota level is a separate decision, and the per-device loops that produce these bursts (ANTHIAS-3H, ANTHIAS-D) are still worth fixing at source.
🤖 Generated with Claude Code