Skip to content

GH-5120 A durable batch's scheduled retry is held in memory instead of lost - #5122

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-5120-durable-batch-scheduled-retry
Oct 22, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
gh-5120-durable-batch-scheduled-retry

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes #5120.

Problem

A native batch (BatchMessagesOf<T>().UseDurableInbox()) whose handler fails under a ScheduleRetry policy was logged as rescheduled but never ran again. Its members' inbox rows sat Incoming, attempts 0, no execution_time, owned by the live node, which nothing reclaims until that node restarts.

The batch is a carrier envelope with no inbox row of its own; only its members are persisted. DurableReceiver.MoveToScheduledUntilAsync rescheduled it through ScheduleExecutionAsync, an UPDATE ... where id = @id on the carrier's id. That matched nothing and succeeded silently.

Fix

Batch retries are held in memory. DurableReceiver now schedules a batch carrier's retry in memory, the same way BufferedReceiver schedules everything, and re-enqueues the same carrier when it comes due.

  • Its Attempts keeps counting, so the policy runs out and dead letters the batch as a unit instead of retrying forever. Spreading the schedule across the member rows would have rebuilt a fresh carrier at attempt 0 every time.
  • The members' rows are untouched: owned by this node while the retry is pending, and recovered from their rows if the node goes away.
  • DrainAsync cancels pending batch retries before it releases the members' rows, so a late retry cannot run them alongside the node that picks them up.
  • Trade-off: a batch retry pending at the moment of a crash is not persisted. Its members are recovered from their rows, with their attempt count starting over.

An update that matches nothing now warns. The RDBMS inbox (SQL Server, PostgreSQL, MySQL, SQLite) and Oracle now log a warning when ScheduleExecutionAsync updates no row, instead of succeeding silently. RavenDB and Cosmos DB are not covered.

Tests

  • SqlServerTests Bug_5120_scheduled_retry_of_a_durable_batch: the reporter's configuration.
    • A batch that fails once is retried and its members settle (red without the fix).
    • A batch that always fails runs exactly 4 times (1 + 3 retries), then is dead lettered and settles.
  • SqlServerTests scheduling_an_envelope_with_no_inbox_row_5120: the warning is logged for a missing row and not for an existing one.
  • CoreTests durable_batch_scheduled_retry_5120: the carrier is re-executed after its scheduled time; it is never rescheduled through the inbox; a pending retry does not fire after a drain. The first two are red without the fix.
  • Full CoreTests: 3742 passed, 0 failed. Full SqlServerTests: 660 passed, 0 failed. dotnet build wolverine.slnx -c Release -f net9.0: clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U

…f lost

A native batch on a durable queue is a carrier envelope with no inbox row; only its members are
persisted. ScheduleRetry rescheduled it by UPDATE-ing the row for the carrier's id, which matched
nothing, so the retry silently never ran and the members sat 'Incoming', owned by the live node.

DurableReceiver now holds a batch carrier's retry in memory and re-enqueues the same carrier, so its
Attempts keeps counting toward the policy's limit. The members' rows stay owned here while it is
pending; a drain cancels pending batch retries before releasing those rows to another node.

Also, the RDBMS and Oracle inboxes now warn when ScheduleExecutionAsync matches no row, rather than
succeeding silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U
@jeremydmiller
jeremydmiller merged commit c5d2838 into main Oct 22, 2026
46 of 47 checks passed
This was referenced Oct 23, 2026
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.

Batching with durable local queue: ScheduleRetry leaves failed batch members stuck in inbox

1 participant