Repository navigation
Fix queue and signal race replay, exact continue-as-new carry, input loss between executions, and KV key order - #67
Merged
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com>
prune_kv_values_updated_before and get_kv_all_keys used HashMap order, which differs between replays of the same turn, so replay failed with `kv clear mismatch`. Use key order, and let replay match a run of KeyValueCleared rows in any order so histories recorded by older runtimes still replay. Fixes #57. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com>
Akhil Gupta (akhil9tiet)
approved these changes
Oct 10, 2026
When the batch of an instance whose latest execution ended in ContinuedAsNew is poisoned (the successor's first turn keeps failing, its handler is missing, or its start never appears), the failure is now committed as execution N+1: a synthetic OrchestrationStarted built from the CAN start, or from execution N's name and parent when the start is missing, followed by the poison failure. Execution N keeps ending in ContinuedAsNew, and later inputs see a failed current execution. Before, the poison path appended OrchestrationFailed to execution N after its CAN row. Such histories, written by earlier runtimes, are still treated as terminal. A duplicate client start in the poisoned batch never becomes the successor's start. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com>
This was referenced Oct 11, 2026
Affan Dar (affandar)
approved these changes
Oct 11, 2026
This was referenced Oct 11, 2026
Affan Dar (affandar)
added a commit
that referenced
this pull request
Oct 11, 2026
Resolve conflicts with #67 in CHANGELOG.md and docs/migration-guide.md. Both sides added new entries in the same places; keep all of them. In the changelog, the #67 entries come first under Fixed and Changed, then this PR's. No code conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuCN2hdY59UrmFYYc7DSBJ
Affan Dar (affandar)
added a commit
to microsoft/duroxide-node
that referenced
this pull request
Oct 12, 2026
* Update duroxide to 0.1.31 (race fixes) duroxide 0.1.31 fixes dropped queue reads and signal waits, the continue-as-new carry of unread queue messages, inputs that arrive between two executions, duplicate starts, and the KV key order used by prune. The SDK compiles unchanged: it does not use TurnResult or any other changed public item. - Require duroxide 0.1.31. - Temporarily patch duroxide and duroxide-pg to their unreleased fix branches (microsoft/duroxide#67, microsoft/duroxide-pg#35). Remove the patch and pin the released crates before release. - Document the new contract: a read that loses a race takes no message, continueAsNew carries exactly the unread messages, getKvAllKeys returns keys in key order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com> * test: race dequeueEvent against a timer; KV key order Add regression tests for the duroxide 0.1.31 fixes, ported from the Node reproduction of the queue/timer race: - A drain loop races ctx.dequeueEvent() against a 20 ms timer while 300 numbered messages arrive with 0-30 ms gaps. Every message must arrive once and in order, and the timer must win at least once. - The same race with a 10 ms timer, where every timer win continues as new. Every message must still arrive once and in order, so a dropped read must not count as a consumer of the carried messages. - Eight KV keys set out of order are listed in key order and pruned, and the next turn replays the writes and the prune. Before microsoft/duroxide#57 was fixed, both used hash order, which differs between replays, so the replay failed with "nondeterministic: kv clear mismatch". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com> * Update duroxide to 0.1.32 and duroxide-pg to 0.1.36 Both are on crates.io, so remove the temporary [patch.crates-io] block that built against their fix branches. 0.1.31 was reserved and never published. Changelog: add the duroxide-pg lock fix and the shutdown change. Docs: Runtime.shutdown now returns as soon as the runtime stops; the timeout is a grace period, not a fixed delay. --------- Co-authored-by: Waldemort Agent <waldemort@noreply.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com> Co-authored-by: Affan Dar <affandar@hotmail.com>
Affan Dar (affandar)
added a commit
to microsoft/duroxide-python
that referenced
this pull request
Oct 12, 2026
* Update duroxide to 0.1.31 (race fixes) duroxide 0.1.31 fixes dropped queue reads and signal waits, the continue-as-new carry of unread queue messages, inputs that arrive between two executions, duplicate starts, and the KV key order used by prune. The SDK compiles unchanged: it does not use TurnResult or any other changed public item. - Require duroxide 0.1.31. - Temporarily patch duroxide and duroxide-pg to their unreleased fix branches (microsoft/duroxide#67, microsoft/duroxide-pg#35). Remove the patch and pin the released crates before release. - Document the new contract: a read that loses a race takes no message, continue_as_new carries exactly the unread messages, get_kv_all_keys returns keys in key order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com> * test: race dequeue_event against a timer; KV key order Add regression tests for the duroxide 0.1.31 fixes, the same as in duroxide-node, ported from the Node reproduction of the queue/timer race: - A drain loop races ctx.dequeue_event() against a 20 ms timer while 300 numbered messages arrive with 0-30 ms gaps. Every message must arrive once and in order, and the timer must win at least once. - The same race with a 10 ms timer, where every timer win continues as new. Every message must still arrive once and in order, so a dropped read must not count as a consumer of the carried messages. - Eight KV keys set out of order are listed in key order and pruned, and the next turn replays the writes and the prune. Before microsoft/duroxide#57 was fixed, both used hash order, which differs between replays, so the replay failed with "nondeterministic: kv clear mismatch". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com> * Update duroxide to 0.1.32 and duroxide-pg to 0.1.36 Both are on crates.io, so remove the temporary [patch.crates-io] block that built against their fix branches. 0.1.31 was reserved and never published. Changelog: add the duroxide-pg lock fix and the shutdown change. Docs: Runtime.shutdown now returns as soon as the runtime stops; the timeout is a grace period, not a fixed delay. --------- Co-authored-by: Waldemort Agent <waldemort@noreply.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Affan Dar <1480118+affandar@users.noreply.github.com> Co-authored-by: Affan Dar <affandar@hotmail.com>
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.
Opened on behalf of Affan Dar (@affandar).
Fixes #66. Fixes #40. Fixes #57.
Related, not fixed here: #58 (this PR only adds collision notices to the failure commit), #4 / #5 (messages queued before a brand-new instance starts; Fix 5 covers only the gap between two executions).
Summary
This PR fixes a family of race bugs around dropped queue reads and signal waits, the carry-forward of queue messages at
continue_as_new, inputs that arrive while an instance is between two executions, the history written when acontinue_as_newsuccessor is poisoned, and a replay failure caused by the order of KV prune clears. Several of them lose messages silently or make replay fail with a nondeterminism error.dequeue_eventfuture kept its FIFO position until its cancellation row was replayed, so live execution and replay could pick different winners (nondeterminism), skip a message or read one twiceHashSetorder, so two runs of the same turn could write different historiesContinuedAsNewhistory but not yet the successor's start acknowledged (deleted) every input in the batchmax_attemptsabandon_orchestration_itemundid attempts and applied its delay to every row of the instance, including rows the fetch never lockedprune_kv_values_updated_beforeandget_kv_all_keysusedHashMaporder, which differs between replays, so replay failed withkv clear mismatchOrchestrationFailedwas appended to the previous execution after itsContinuedAsNewrow, so the execution had two terminal rows and the runtime still read it as "continued as new"OrchestrationStartedfrom the CAN start, or from execution N's name and parent when the start is missing, then the failure). Histories written by older runtimes are still treated as terminalCompatibility
duroxide_versionstamp on the execution's first history row. Executions started before 0.1.31 keep the old behaviour, including its known defects, until they finish or continue as new. An unparsable stamp keeps the old behaviour and logs a warning.SubOrchFailedare unchanged.RELEASE_POLICY.mdreserves 0.1.31: no release numbered 0.1.31 or later may ship without this change.TurnResult::ContinueAsNewgains the fieldunconsumed_queue_arrivals. Code that builds or exhaustively matches the variant must add the field or use...abandon_orchestration_itemmust apply its attempt undo and its delay only to the rows that fetch locked. New public validations:provider_validations::race_replay::*,poison_message::orchestration_ignore_attempt_preserves_hidden_startandpoison_message::orchestration_delayed_abandon_preserves_unlocked_rows.Testing
cargo fmt --check, Clippy-D warningscargo test --workspace --all-featureson the pinned Rust 1.97.1continue_as_new(in-memory SQLite, file SQLite, PostgreSQL) plus two varied-timer casesDeveloped on Rust 1.99; after rebasing onto
main, every commit and the full workspace suite were re-checked on the repository's pinned Rust 1.97.1.Commits
1ed4085replay: cancel dropped queue reads with execution-pinned policy (4 files, +60 / -5)7d986e0replay: preserve bound-slot signal admission and cancellation (1 file, +31 / -15)632065dcontinue-as-new: carry exactly the unread queue positions (3 files, +53 / -4)615b59ddispatcher: preserve CAN admission and terminal input handling (4 files, +102 / -15)e39ae3bsqlite: scope abandon attempt undo and delay to locked rows (1 file, +19 / -29)f607838test: cover queue signal and CAN replay boundaries (20 files, +14,376 / -5)91589c2docs: describe the race policy cutover and deferred work (10 files, +343 / -57)a33bf24kv: clear and list keys in key order (4 files, +110 / -4)67415d0poison: record a continue-as-new successor as its own execution (8 files, +497 / -62)Downstream
The SDK and provider PRs stay drafts until this PR is released; each builds against this branch through a temporary
[patch.crates-io].End to end in microsoft/PilotSwarm. PilotSwarm
main(77c68e1a) ran on one machine twice: with the released duroxide-node 0.2.0, and with duroxide-node built from this PR and microsoft/duroxide-pg#35.race(dequeueEvent, scheduleTimer)loops, 300 messages each (PostgreSQL ×5, in-memory SQLite ×3)nondeterministic: history schedule but no emitted actioncontinue_as_newevery 200 cycles (×2)Deferred follow-ups are listed in
design/queue-race-d2-follow-ups.md.