Skip to content
Merged
6 changes: 6 additions & 0 deletions .agents/skills/release-preparation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ Never:

## Required Input

Version 0.1.31 is reserved for the race-cancellation semantic cutover. Do not
prepare a 0.1.31+ release or hotfix from an older branch without all queue,
positional-wait and exact continue-as-new carry-forward fixes. The subsequent
release PR uses 0.1.32; never change the semantic threshold to that release
number. See the reservation in `RELEASE_POLICY.md`.

Obtain the target semantic version from the user. Do not choose a major, minor,
or patch bump without confirmation.

Expand Down
84 changes: 83 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,89 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **Continue-as-new admission window** - A CAN-terminal fetch that cannot
yet see the successor's start no longer acknowledges instance-scoped inputs.
Queue messages, positional signals and cancellation requests are abandoned
with the unregistered-handler backoff (`unregistered_backoff`, 1 s doubling
to 60 s by default), keeping the attempt, until the start is visible. Their
order relative to later arrivals is best effort, as for any delayed abandon.
If the abandon fails, the lease expires and the inputs are fetched again.
A start that never becomes visible (for example one deleted by an unpatched
worker) ends in poison after `max_attempts`, about five minutes with the
defaults, instead of an endless wait.
A poisoned successor is recorded as its own failed execution (a start row and
the failure), so the previous execution still ends in CAN. A history written
by an earlier runtime, with the failure after the CAN row, is still terminal,
so later inputs are acknowledged rather than waiting for a start already
consumed.
Stale completions for the previous execution wait with the other inputs and
the successor discards them. This dispatcher correction is
not version-gated and changes no existing recorded replay decision.
All orchestration workers must be patched to close the ingestion window
in a mixed deployment.
A CAN start now takes priority over a duplicate client start regardless of
batch order. Duplicate starts cannot replace a running instance's pinned
handler name, version or input. A colliding sub-orchestration start now
fails its parent when the target is still running, not only when it is
finished; a running instance's own parent re-sending the same call (with a
known parent execution id) is still ignored.

- **KV key order** - `prune_kv_values_updated_before` clears keys in key order and
`get_kv_all_keys` returns them in key order, instead of `HashMap` order that
differed between replays (`nondeterministic: kv clear mismatch`, #57). Replay
accepts a run of `KeyValueCleared` rows in any order, so histories recorded by
older runtimes still replay.

- **SQLite abandon affects only the locked rows** - `abandon_orchestration_item`
now undoes the attempt increment and applies its delay only to the rows that
fetch locked, matching PostgreSQL and the provider contract. Before, it
updated every visible row of the instance, so a delayed abandon could hide a
continue-as-new start that became visible after the fetch, repeatedly while
inputs kept arriving. New provider validation:
`orchestration_delayed_abandon_preserves_unlocked_rows`.

- **Shared race cancellation cutover** - Positional waits now cancel at drop
and discard stale signals assigned to a dropped slot. Continue-as-new
carries exact unread arrival positions, including held and non-prefix
dequeues.
Positional signals require a bound open slot. Same-batch signals applied
before a replacement is bound are dropped as early, even if that wait was
emitted by an earlier poll. This retains the documented early-event contract.
These corrections share the 0.1.31 execution threshold with the queue fix.
Activity/child terminal cleanup and cancellation validation are unchanged by
this continue-as-new path.
Dropped-future and terminal activity cancellations are now written in ID
order within each type, for every execution. Replay compares them as sets,
so recorded histories are unaffected.
Earlier executions preserve their original policy, including known defects.
Version 0.1.31 is reserved and must never be published without these fixes;
the subsequent release PR may use 0.1.32 and retains the same threshold.

- **Queued-event race replay** - Dropping an unresolved dequeue now
excludes its subscription from FIFO matching immediately, including when the
token has not yet been bound. A timer followed by a queue message in the same
turn replays the covered immediate-replacement race shapes consistently.
Creation-versus-poll ordering, cancellation of an older dequeue after a newer
one resolved, and poll-on-demand gaps remain separate known limitations.
The behavior is pinned by the execution's `OrchestrationStarted` core version:
executions started before 0.1.31 retain legacy cancellation semantics, and
executions started by 0.1.31 or later use the fix. Existing executions switch
only when a new execution is started by a runtime at or above the threshold,
including continue-as-new. This avoids
changing race winners in otherwise valid old histories.
The doc-hidden `bind_token` helper now applies a previously recorded dequeue
drop when binding its token to a queue schedule only under V0131; contexts
default to Legacy.

### Changed

- The public `TurnResult::ContinueAsNew` variant now includes
`unconsumed_queue_arrivals`. External code constructing or exhaustively
matching this variant must add the field or use `..` in its match.
This is a Rust source-compatibility change, not a persisted event-format change.

## [0.1.30] - 2026-07-29

**Release:** <https://crates.io/crates/duroxide/0.1.30>
Expand Down Expand Up @@ -1237,4 +1320,3 @@ return ctx.continue_as_new(input).await;
- OpenTelemetry metrics and structured logging
- Provider validation test suite
- Comprehensive documentation

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[package]
name = "duroxide"
version = "0.1.30"
version = "0.1.31"
edition = "2024"
description = "Durable code execution framework for Rust"
license = "MIT"
repository = "https://github.com/microsoft/duroxide"
homepage = "https://github.com/microsoft/duroxide"
documentation = "https://docs.rs/duroxide"
readme = "README.md"
exclude = ["tests/fixtures/**", "tests/queue_race_upgrade.rs"]
keywords = ["durable", "workflow", "orchestration", "async", "tasks"]
categories = ["asynchronous", "concurrency"]

Expand Down Expand Up @@ -41,6 +42,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio-native-tls", "sqlite", "mac
libsqlite3-sys = { version = ">=0.28", features = ["bundled"], optional = true }

[dev-dependencies]
sha2 = "0.10"
# Used in tests/examples
tempfile = "3"
proptest = "1.4"
Expand Down
8 changes: 8 additions & 0 deletions RELEASE_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ internal pipeline instructions.

## Public Release Handoff

Version 0.1.31 is reserved for the execution-pinned race-cancellation cutover.
Never tag or publish 0.1.31 or a later version from a branch that lacks the
queue, positional-wait, and exact continue-as-new carry-forward fixes.
The initial execution stamp selects these semantics permanently. An old
hotfix branch numbered 0.1.31+ would record decisions that corrected runtimes
cannot replay with that stamp. The subsequent release preparation uses 0.1.32;
the semantic threshold remains 0.1.31.

GitHub Actions in this repository are limited to building and testing changes.

1. Prepare a release pull request that updates the package version, changelog,
Expand Down
12 changes: 12 additions & 0 deletions design/queue-race-d2-follow-ups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Queue-race follow-ups

These items are not part of the queue-race fix and are tracked separately.

| Area | Follow-up |
|---|---|
| Continue-as-new admission | Remove the admission window in providers: write the continue-as-new start so no fetch cutoff can exclude it, and delay abandoned instances with an instance cooldown instead of re-timing rows. Until then a waiting instance is re-fetched on the unregistered-handler backoff and poisoned after `max_attempts` if the start never appears. |
| Collision wording | Include terminal-after-continue-as-new status in collision-notification wording. |
| Dropped abandon futures | Await the pre-existing dropped `abandon_orchestration_item` futures, or remove them intentionally. |
| Collision on poison | Notify a colliding start's parent when the same batch is consumed by poison handling. |
| Schedule lookup | Replace the O(n) schedule-to-token lookup with a reverse map. |
| Successor first-turn commit failure | When a successor's first-turn commit fails, the infrastructure-failure path still builds its failure from the previous execution's history and commits it under the successor's execution id (related to #58). Poison already records the successor as its own execution. |
Loading
Loading