Skip to content

Resend rate-limited receipts with backoff, but never after resolution - #97

Merged
eivindjonassen merged 1 commit into
mainfrom
fix/receipt-backoff
Nov 1, 2026
Merged

eivindjonassen merged 1 commit into
mainfrom
fix/receipt-backoff

Conversation

@eivindjonassen

Copy link
Copy Markdown
Member

Summary

Bug. A MessageRateExceeded receipt called markRetry, but markRetry only updated rows still in queued. The row was ticketed, so nothing changed. It stayed ticketed, its receipt was checked again on every tick, and the message was never resent. This was found while adding tests in #88. A test locked in that behavior, and this PR replaces it.

Fix. markRetry also accepts ticketed rows. It moves them back to queued with next_attempt_at_ms = now + 2^min(attempts, 8) s (capped at 15 min), clears ticket_id and receipt_due_at_ms, and records MESSAGE_RATE_EXCEEDED. markTicketed already counts the send, so a ticketed row keeps its count, and the receipt backoff matches the ticket backoff (2 s, then 4 s, …). While delivery is paused, rows still fail with PAUSED. All other outcomes are unchanged.

A gap this would have opened. Resolution only failed queued rows. A sent row waits up to 15 minutes for its receipt. If the user answered the permission in that window and the receipt came back rate-limited, the fix above would have resent a notification for something already resolved. So resolving an interaction now also marks its ticketed rows last_error_code = 'RESOLVED' and clears their push data, as failed rows get. They stay ticketed, so the receipt is still checked:

Receipt for a resolved, ticketed row Result
ok row deleted
DeviceNotRegistered device disabled, failed / DEVICE_NOT_REGISTERED
MessageRateExceeded failed / RESOLVED, nothing resent
other error failed / EXPO_RECEIPT_ERROR

A queued row can't carry RESOLVED, because resolving a queued row fails it right away, so the mark is unambiguous. Interactions resolved while a send request is in flight were already safe, and a test now covers that.

No schema change. Rate limits still have no attempt limit, for tickets or receipts, as before. Adding one would be a separate change to markRetry.

Verification

  • Broker tests: 207 pass. These failed on main: rate-limited receipts get requeued with the ticket backoff, they're resent after it and back off again, and markRetry requeues ticketed rows without counting the send twice. This one failed before the resolution guard: no resend for resolved interactions. New guards that passed before and still pass: DeviceNotRegistered for resolved rows, interactions resolved during the send, and rate limits while paused.
  • Broker typecheck and build, pnpm lint
  • CRAP: acceptPluginEvent 10.0, markRetry 5.0, applyReceipt 4.0
  • Tests cover the changed behavior.
  • Platform behavior is verified on the affected targets, or the gap is documented. Broker only, tested on Linux with an in-memory database and stubbed Expo responses. Not tested against Expo's real rate limits.
  • No credentials, addresses, identifiers, user content, private paths, or deployment files are included.
  • Documentation reflects the resulting behavior. No documented behavior changes.

@eivindjonassen
eivindjonassen force-pushed the fix/receipt-backoff branch 2 times, most recently from 67c41b0 to f15809e Compare November 1, 2026 20:57
A MessageRateExceeded receipt called markRetry, which only updated
queued rows, so the ticketed row was checked again on every tick and
never resent. markRetry now requeues ticketed rows with the ticket
backoff, without counting the send twice.

Resolving an interaction now also marks its ticketed rows RESOLVED and
clears their push data. They keep their receipt check, so
DeviceNotRegistered still disables the device, but markRetry fails them
instead of resending a notification for a resolved interaction.
@eivindjonassen
eivindjonassen merged commit 172f5e6 into main Nov 1, 2026
4 checks passed
@eivindjonassen
eivindjonassen deleted the fix/receipt-backoff branch November 1, 2026 21:13
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.

1 participant