Fix DurableDeferred race wake-up - #7179
Open
tim-smart wants to merge 10 commits into
Open
Conversation
🦋 Changeset detectedLatest commit: 1003c98 The changes in this PR will be included in the next version bump. This PR includes changesets to release 30 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
Bundle Size AnalysisGenerated from PR build output; treat the content below as untrusted.
|
…erred-race-wakeup # Conflicts: # packages/effect/test/unstable/workflow/WorkflowEngine.test.ts
Key pendingDeferredResults by executionId like the other per-execution maps in ClusterWorkflowEngine, and drop the unused deferredWaiters clear in layerMemory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the waiter-registry plus interrupt-and-replay wake with a first-class in-process signal. raceAll now races an extra wake arm that parks on a latch, wins with a deferred completion that arrives while another branch is active, and is the single place that converts "every branch suspended" into a durable suspension. Deferred completion just records the result and opens the latch; the run is never interrupted or replayed. Also restores the memory-engine live wake. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the identity-set branch check with a marker that survives spread clones, so an await wrapped in DurableDeferred.into still registers with its race. The guard itself stays: engines capture the calling context for activity execution, and an unconditional registration lets a deferred awaited inside an activity body win the race with a non-branch value (covered by the new clock-capture test). Also strengthen the memory wake test so removing the live-wake block fails deterministically and cover the completion-during-suspension-commit window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wake arm no longer returns a raw deferred exit as the race result, which was only correct when a branch was exactly DurableDeferred.await. On completion it now re-runs the branch that registered the deferred, so the branch's own pipeline produces the winner: transformed awaits keep their transformations and a bare DurableClock.sleep branch returns its own value. Registrations carry a direct/bubbled flag: a direct await can be re-run even while its branch is still unwinding through the activity count, while bubbled registrations from nested races defer to the inner race until its branch is dead. This also removes the marker guard, and a durable clock inside a racing activity now wins live via the activity reset path instead of being ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reviewer verified the bubbled-waits-for-dead-branch guard changes nothing observable: treating every registration as re-runnable leaves all suites green with identical work counts across nested race shapes. The property it guarded is already structural: a parked await never produces a value, a live nested race handling the same wake can only produce the same winner, branches are replay-safe, and completions are idempotent. Deleting the flag also deletes the dead-branch tracking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.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.
Summary
DurableDeferred.raceAlla first-class in-process wake instead of interrupt-and-replayEffect.raceAll's success-biased failure semantics for durable racesinto-wrapped, or inside an activity body) now wins the race liveRoot cause
A pending
DurableDeferred.awaitsuspends its own race branch, so the workflow stays active while another branch can still succeed. The engine's only wake primitive was suspend-and-replay, so a persisted completion had nobody to deliver to and the activity became the eventual winner.Design
Branches keep their normal semantics: awaiting a pending deferred (or a suspended activity) interrupts the branch, exactly as before. The race gains one extra arm, a wake arm, that parks on a latch and is the single place race outcomes beyond a normal branch win are decided:
awaiton a pending deferred registers it (flaggeddirect) before suspending; nested races bubble their registrations to the enclosing branch (flagged bubbled). The context reaches activity bodies too, since engines capture the calling context for activity execution.This deletes the waiter registry, the interrupt-and-replay path, and the waiter lifecycle cleanup from the previous iterations. The per-execution pending-result retention and the entity's
concurrency: 2remain, serving the read fast-path and the wake delivery.Test coverage
Mutation-verified guards (each mutation kills exactly its test):
runs === 1)into-wrapped branch wakesawaitkeeps its transformation on a wake (signal!)DurableClock.sleepbranch wins with its own valueWAIT-SUSPENDEDpath)Validation
pnpm vitest run packages/effect/test(8106 passed)deno task test run packages/effect/test/unstable/workflow/WorkflowEngine.test.ts(5 passed)pnpm lintpnpm checkCloses EFF-588
Closes #7176