Skip to content

feat(hybridcloud): Size webhook mailboxes from a rolling rate, not a volume gate - #123449

Open
vaind wants to merge 5 commits into
masterfrom
ivandlugos/size-mailbox-buckets-from-rolling-rate
Open

feat(hybridcloud): Size webhook mailboxes from a rolling rate, not a volume gate#123449
vaind wants to merge 5 commits into
masterfrom
ivandlugos/size-mailbox-buckets-from-rolling-rate

Conversation

@vaind

@vaind vaind commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Sentry forwards an integration's webhooks through a "mailbox" -- a queue drained in order, one payload at a time -- so one noisy repository makes every other payload for that integration wait behind it. Splitting an integration across sub-mailboxes lets them drain in parallel; which one a payload lands in is key % count, where the key is the repository or issue it is about.

Two things decide that split today and both are wrong. Whether to split is an hourly Redis counter: an integration splits only after 3,000 payloads inside one clock hour. That window resets on the hour, so a second key with a one-day expiry has to carry the decision across the reset; a burst straddling the boundary trips neither hour; and a Redis error means no split at all. GitHub opted out of the whole mechanism years ago with an always_bucket flag. How far to split is hardcoded per provider -- 100 for GitHub and GitLab, 10 for Jira and Azure DevOps -- so an integration sending a handful of payloads an hour is split as widely as one sending constantly, and the number moves only when someone edits it.

Both are replaced by one rule: count what the mailbox actually received recently, and size the split to that.

How it works

Arrivals are counted over a rolling 15 minutes as five 3-minute Redis counters, summed on read. A counter takes writes only while it is the current one, so its expiry is a real bound rather than one pushed forward on every write -- and nothing needs remembering across a reset, which is why the second key disappears rather than moving.

The split is payloads // (worker_threads × payloads_per_thread), rounded down to a power of two and capped. worker_threads is how many payloads one drain delivers at once, so the unit is a pass of the delivery pool, and four passes is what one mailbox should be worth. Powers of two because key % 2n leaves a key where key % n put it or exactly n along, so doubling moves half the keys instead of nearly all; rounding down means the rate must double before a bucket is added, so an integration sitting on the boundary is not re-mapped every window. A window Redis cannot answer sizes to the cap, because an outage must not put a busy integration back onto one serial queue.

Which providers

Every change of divisor re-maps keys, so a payload can land in a different sub-mailbox from the one its predecessor is still queued in. Only a provider whose delivery tolerates reordering can absorb that, and hybridcloud.webhookpayload.skip_on_failure_providers already records exactly that set -- the same option the drain reads. Reading it here means a provider earns a rate-sized split only once it tolerates what the split costs, rather than sitting on a hardcoded list nobody dares delete. That option now covers every forwarding provider, so all of them are rate-sized; the fixed fallback is what applies where the option is narrower, which today means self-hosted and dev until #123422 realigns the checked-in default.

Worth checking

payloads_per_thread (4) and max_mailbox_buckets (64) are reasoned, not measured. They are options rather than constants so acting on a reading is an edit instead of a deploy, and the new buckets tag on the routing metric is what would let us tune them -- it needs a Datadog tag-config change first, which is not retroactive.

Mailbox names move both ways on deploy: quiet integrations lose their split, and Jira and Azure DevOps integrations busy enough to earn one get it for the first time. Payloads already queued under an old name drain normally.

The window counts arrivals, so it sizes for steady state and not for a backlog -- if delivery stalls, inflow is unchanged and the split stays put.

Refs CW-1987

@linear-code

linear-code Bot commented Sep 2, 2026

Copy link
Copy Markdown

CW-1987

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Sep 2, 2026
@vaind
vaind force-pushed the ivandlugos/size-mailbox-buckets-from-rolling-rate branch from 158f25a to 0d208e8 Compare September 2, 2026 18:54
@vaind
vaind changed the base branch from ivandlugos/bucket-webhook-mailboxes-whenever-key-exists to ivandlugos/mailbox-name-value-object September 2, 2026 18:54
@vaind
vaind marked this pull request as ready for review September 2, 2026 18:56
@vaind
vaind requested review from a team as code owners September 2, 2026 18:56
@vaind
vaind requested a review from joseph-sentry September 2, 2026 18:56
Comment thread src/sentry/hybridcloud/webhook_mailbox_sizing.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0d208e8. Configure here.

Comment thread src/sentry/hybridcloud/webhook_mailbox_sizing.py
Comment thread src/sentry/integrations/middleware/hybrid_cloud/parser.py
Base automatically changed from ivandlugos/mailbox-name-value-object to master September 4, 2026 09:20
@vaind
vaind force-pushed the ivandlugos/size-mailbox-buckets-from-rolling-rate branch from 0d208e8 to aa75e46 Compare September 4, 2026 09:59
@vaind vaind changed the title feat(hybridcloud): Size webhook mailbox buckets from a rolling rate feat(hybridcloud): Size webhook mailboxes from a rolling rate, not a volume gate Sep 4, 2026
Comment thread src/sentry/hybridcloud/webhook_mailbox_sizing.py
Three ways sizing could break the ingest path it runs on. Both depth options
are automator-modifiable and unvalidated, so a zero reached the division; the
drain already floors worker_threads at one rather than rejecting it. A Redis
reply that would not destructure or coerce escaped the fail-open fallback and
would have 500'd a webhook we could still have queued. And callers build the
mailbox as an argument, so a shed payload was counted -- and paid for a Redis
write -- before the shed check ran.
…from-rolling-rate' into fold-gate-and-sizing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant