Skip to content

fix(slack): back off between failed outbox deliveries (audit H4) - #428

Merged
passcod merged 1 commit into
mainfrom
claude/pr-370-fix-h4-slack-backoff
Aug 1, 2026
Merged

fix(slack): back off between failed outbox deliveries (audit H4)#428
passcod merged 1 commit into
mainfrom
claude/pr-370-fix-h4-slack-backoff

Conversation

@passcod

@passcod passcod commented Aug 1, 2026

Copy link
Copy Markdown
Member

Fixes H4 (high) from the audit in #370.

The bug

SlackOutbox::mark_failed incremented attempts and recorded the error, but never touched deliver_after — the column claim_pending filters on. A failed row was immediately claimable again, and the drainer ticks every 5 seconds, so all MAX_ATTEMPTS = 10 were burnt in roughly 45–90 seconds. After that the row is stamped gave_up_at and never retried.

The retry budget was effectively a count of ticks rather than a span of time. A Slack outage of a few minutes — a routine occurrence — permanently dropped every incident open and resolve enqueued during it, and nobody was paged for them once Slack came back.

(The give-up escalation goes out through the same failing path, so it's dropped the same way. That's a separate concern and not addressed here.)

The fix

mark_failed now advances deliver_after by retry_backoff(attempts): 15s, doubling per attempt, capped at 15 minutes. The same ten attempts now span about an hour (15s, 30s, 1m, 2m, 4m, 8m, then 15m a few times), which rides out a routine Slack incident.

The schedule is a plain function in database::slack_outbox, so it's unit-testable and the drainer can log the wait alongside its retry warning. mark_failed also returns the new attempt count.

Nothing about give-up changes: a row that exhausts its attempts is still abandoned, it just takes an hour of real outage to get there instead of a minute.

Tests

  • mark_failed_holds_the_row_back_for_its_backoff — after mark_failed the row is not reclaimable on the next tick, and is reclaimable once its backoff has elapsed (failure is not give-up).
  • retry_backoff_doubles_then_holds_at_the_cap — the schedule itself, including the assertion that the ten-attempt budget spans at least 45 minutes.

Generated by Claude Code

`mark_failed` incremented `attempts` but never moved `deliver_after`, which
`claim_pending` filters on. A failed row was therefore reclaimed on the very
next 5-second tick, so all ten attempts were spent in about a minute — after
which the row is stamped `gave_up_at` and never retried again.

That made the retry budget a count of ticks rather than a span of time: any
Slack outage past ~a minute permanently dropped every incident open and
resolve enqueued during it, and operators were never paged for them once
Slack recovered.

`mark_failed` now advances `deliver_after` by `retry_backoff(attempts)` —
15s doubling to a 15-minute cap — which spends the same ten attempts over
about an hour, enough to ride out a routine Slack incident. It also returns
the new attempt count, and the drainer logs the wait alongside its retry
warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGfH1cdFKPnKpM7ytRThft
@passcod
passcod marked this pull request as ready for review August 1, 2026 19:40
@passcod
passcod added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit 59a40e9 Aug 1, 2026
7 checks passed
@passcod
passcod deleted the claude/pr-370-fix-h4-slack-backoff branch August 1, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants