Skip to content

fix: reopen recorded service conditions within groups - #602

Merged
rmcdaniel merged 6 commits into
mainfrom
fix/service-grouped-condition-reopen
Oct 1, 2026
Merged

rmcdaniel merged 6 commits into
mainfrom
fix/service-grouped-condition-reopen

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Problem

The published Rust Worker can reopen a scalar condition after an insufficient signal. The same condition in a nested parallel group or keyed selection is rejected with 409 invalid_commands. Subsequent replay and delayed deadline checks also expose incorrect logical completion and a stalled selection.

The untouched published counterfactual uses Workflow 2.3.1, Rust SDK 2.1.2 and the exact Server 2.4.36 image recorded in #601.

Change

  • Preserve a proven condition's authored occurrence and group path across physical reopens. Keep strict new-group validation and reject changed identities, duplicate reopens and unproven predecessors.
  • Preserve a pending condition after false wakes. Bind group completion and selection resolution to its latest physical wait, while keeping the original operation identity.
  • Commit a winner only when the predicate resolves. Schedule one successor replay when the Worker is waiting for the canonical winner and no live workflow task already exists.
  • Keep condition timeout resolution bound to its timer fire after acknowledgement. Pass the authored condition or signal kind through the delayed timer closure check, while retaining the timer transport status and member validation.
  • Prepare patch product train 2.3.2. Protocol remains 1.19 for ordinary Workers. Cooperative cancellation remains separately gated.

Evidence and delivery

  • Focused bridge counterfactuals reproduce the valid grouped rejections, missing successor and delayed deadline errors.
  • Corrected focused feature suite: 34 tests / 255 assertions pass. Logical selection and timeout unit suite: 15 tests / 110 assertions pass.
  • Native unit and coverage checkpoint: 2,145 tests / 16,371 assertions pass in each run, V2 coverage 65.94%. Style and static analysis pass.
  • The final full database/Laravel/coverage matrix passes on 71f31baabea53cb4f4d334c6291896987f88db3a in run 36900284907. Normal PR checks also pass on this head.
  • The durable replay corpus and published baseline reproducer are retained in this PR. The extended fresh-process Worker consumer is retained in fix: replay grouped condition physical reopens sdk-rust#56.
  • All five actual Worker source cases now pass through WorkflowCompleted with typed satisfied or timed-out results and a fresh process for every claim. This includes nested parallel members finishing while the condition remains false, final keyed selection, and both delayed deadline paths. Exact published PHP/Python/Rust qualification remains before delivery.

Refs #601. Keep the issue open until the stable Server artifact and consumers are verified. Shared cooperative cancellation work remains separately gated in durable-workflow/.github#136.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Confirmed before/after for physical reopens

The unchanged published consumer in tests/fixtures/service-grouped-condition-reopen-rust now passes all three cases against the corrected source candidate. The same compiled executable and published Rust SDK 2.1.2 produced scalar pass, nested parallel failure and keyed selection failure against the untouched Server 2.4.36 digest in the initial counterfactual.

Corrected tuple: Native fc4ef6d5c29d219e595ad4d4f37f004b55b60585, Server qualification commit 982111fd4521a247d5323082c44b2d45d0ff2c59, published Rust SDK 2.1.2. The temporary Server branch declares an explicitly unpublished 2.3.2-alpha.601 package with exact Git source/dist authority. It is a source qualification, not a released image. Its local image ID is sha256:bf52ef278bf2c35546fe2c1221e0350d1ae3dd922306052518a142e78e82f950.

Case Untouched published image Corrected source candidate
Scalar condition pass, physical opens 1 and 2 pass, physical opens 1 and 2
Nested parallel condition 409 invalid_commands pass, physical opens 3 and 4
Keyed selection condition 409 invalid_commands pass, physical opens 2 and 3, no premature selection winner

Local checks: focused feature 30 tests / 226 assertions, declared replay consumer 1 test / 616 assertions, style and static analysis pass. The official corpus validator passes with counterfactual_enforced: true, one new replay fixture and a real failure at the base binding. Current PR build 36891632062 passes on the exact head. Full source database/coverage qualification and Rust actual Worker 36891976969 are still running.

Next: establish final satisfaction, timeout and cold replay semantics after reopens, then qualify and publish the actual customer artifacts. #601 remains open and #602 remains draft.

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.47644% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.78%. Comparing base (fb3f3e5) to head (71f31ba).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
src/V2/Support/DefaultWorkflowTaskBridge.php 98.80% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #602      +/-   ##
==========================================
+ Coverage   88.74%   88.78%   +0.03%     
==========================================
  Files         472      472              
  Lines       64963    65126     +163     
==========================================
+ Hits        57654    57821     +167     
+ Misses       7309     7305       -4     
Flag Coverage Δ
v2 88.78% <99.47%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Source qualification checkpoint and remaining delayed deadline defect

The isolated actual Worker consumer used ordinary protocol 1.19 with cooperative cancellation disabled. Every workflow claim ran in a fresh process.

Exact source tuple:

  • Native 9118311fb36ed7bd6ecd4023e00a7bc9025c47e3
  • Rust 891420d38464f3810365ac19bc27516620e08c25
  • Server qualification branch 8d8c5ea1b9be664800df559499f0bd330ea5b7f7
  • Unpublished local image sha256:df540c0f925de550cda2f1431ef6b72e1c0f4dbee11ecef8f2548f7b7b388221, containing the authority-verified Native source above as 2.3.2-alpha.601
Case Outcome
Scalar condition, insufficient vote then final vote WorkflowCompleted
Nested parallel condition, other members finish while predicate stays false, then final vote Pending condition preserved, then WorkflowCompleted
Keyed condition selection, insufficient vote then final vote Canonical winner committed and replayed, then WorkflowCompleted
Nested condition deadline after repeated false wakes Condition timer fires and workflow completes
Keyed condition deadline after a false wake Product failure: timer job fails before committing its fire, leaving the timer task leased

The delayed selection deadline failure is now reproduced directly by three canonical feature cases: initial condition, reopened condition and signal deadlines. All three reject selection_member_kind on the unchanged timer-closure path. That path compares the timer transport kind against the authored wait operation kind. The immediate zero-deadline test did not exercise this queued job path. The fix keeps the transport status and passes the causal operation kind separately, preserving the member validation.

Native unit and coverage runs at the checkpoint each pass 2,145 tests / 16,371 assertions, with 15 existing notices and 5 skips. V2 line coverage is 65.94%, above the 60% accepted baseline. The preceding full database/Laravel/coverage matrix passed in run 36896774188. Final checkpoint PR checks passed in run 36898365688. Rust checks passed in run 36898809083.

The locked fresh-process consumer is retained for review and rerunning. The SDK marker counterfactual on the same Server checkpoint does not complete the keyed condition winner, while the corrected Rust source does.

Next action: finish the delayed timer closure correction, repeat the focused gates and the five actual Worker cases, then qualify the exact published PHP/Python/Rust tuple through the stable Server artifact. Both fix PRs remain drafts. No stable artifact or cooperative capability has been published. The completed candidate stack has been removed with its volumes and network.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Final actual Worker source qualification: all five cases pass

The corrected Native and Rust source now complete every bounded case with a fresh Worker process for each claim. The consumer asserts the handler's typed satisfied or timed-out value and the durable WorkflowCompleted history event.

Case Physical condition opens Satisfied wakes Result
Scalar condition, insufficient then sufficient signal 2 2 Satisfied, completed
Nested parallel condition, other members finish while predicate remains false 3 3 ConditionSatisfied, completed
Keyed selection condition, insufficient then sufficient signal 2 2 ConditionSatisfied, selected, completed
Keyed selection deadline after insufficient signal 2 1 ConditionTimedOut, selected, completed
Nested parallel deadline after another false wake 3 2 ConditionTimedOut, completed

Exact source tuple:

  • Native 71f31baabea53cb4f4d334c6291896987f88db3a, represented honestly as unpublished 2.3.2-alpha.601 in the temporary qualification image.
  • Rust runtime and consumer a8b5d0245b04ab4daebdf46eb5906d49e8af0784. Current PR head e2739dd9bb3066e72264a90751c8885bea886767 changes only the required 2.1.3 changelog entry.
  • Temporary Server qualification source 5590d7f1317a0a8bd86fb602fbde428a86433d80, built through the repository Dockerfile and normal package provenance verification.
  • Local image ID sha256:46e29e4048ee12af6fe3c428d2f2b5987e0b39d12640bd48001e34615c6b4959. Ordinary protocol 1.19, cooperative cancellation disabled.
  • Isolated MySQL 8 and Redis 7 stack. No live or customer state used. The stack was removed after the run.

The locked actual Worker consumer and commands are retained in SDK PR 56. Run cargo build --locked --manifest-path tests/fixtures/grouped-condition-worker-consumer/Cargo.toml --target-dir target in Rust 1.86, then run its binary with the isolated Server URL. The parent launches a new subprocess for each claim and exits nonzero for any scenario or typed-result failure. This final invocation exited 0 and produced no stderr.

This is source qualification. Next: merge and publish Rust 2.1.3 and Native 2.3.2, build the stable Server from main with the published Native package, then qualify the exact published Server digest and PHP/Python/Rust consumers before closing #601. Cooperative feature delivery remains separately gated.

Raw result records

{"claim_process":"fresh-process-per-claim","mode":"condition","next_signal":{"completed":true,"opens_before_final_signal":2,"outcome":"pass","satisfied":2},"opens":2,"phases":[{"event_type":"StartAccepted","sequence":null},{"event_type":"WorkflowStarted","sequence":null},{"event_type":"ConditionWaitOpened","sequence":1},{"event_type":"SignalReceived","sequence":null},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"ConditionWaitSatisfied","sequence":1},{"event_type":"ConditionWaitOpened","sequence":2},{"event_type":"SignalReceived","sequence":null},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"ConditionWaitSatisfied","sequence":2},{"event_type":"WorkflowCompleted","sequence":null}],"result":{"outcome":"pass"}}
{"claim_process":"fresh-process-per-claim","mode":"parallel","next_signal":{"completed":true,"opens_before_final_signal":3,"outcome":"pass","satisfied":3},"opens":3,"phases":[{"event_type":"StartAccepted","sequence":null},{"event_type":"WorkflowStarted","sequence":null},{"event_type":"TimerScheduled","sequence":1},{"event_type":"SignalWaitOpened","sequence":2},{"event_type":"ConditionWaitOpened","sequence":3},{"event_type":"SignalReceived","sequence":null},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"ConditionWaitSatisfied","sequence":3},{"event_type":"ConditionWaitOpened","sequence":4},{"event_type":"SignalReceived","sequence":null},{"event_type":"TimerFired","sequence":1},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"SignalApplied","sequence":2},{"event_type":"ConditionWaitSatisfied","sequence":4},{"event_type":"ConditionWaitOpened","sequence":5},{"event_type":"SignalReceived","sequence":null},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"ConditionWaitSatisfied","sequence":5},{"event_type":"WorkflowCompleted","sequence":null}],"result":{"outcome":"pass"}}
{"claim_process":"fresh-process-per-claim","mode":"selection","next_signal":{"completed":true,"opens_before_final_signal":2,"outcome":"pass","satisfied":2},"opens":2,"phases":[{"event_type":"StartAccepted","sequence":null},{"event_type":"WorkflowStarted","sequence":null},{"event_type":"TimerScheduled","sequence":1},{"event_type":"ConditionWaitOpened","sequence":2},{"event_type":"SignalReceived","sequence":null},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"ConditionWaitSatisfied","sequence":2},{"event_type":"ConditionWaitOpened","sequence":3},{"event_type":"SignalReceived","sequence":null},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"ConditionWaitSatisfied","sequence":3},{"event_type":"SelectionResolved","sequence":null},{"event_type":"WorkflowCompleted","sequence":null}],"result":{"outcome":"pass"}}
{"claim_process":"fresh-process-per-claim","mode":"selection-timeout","next_signal":{"completed":true,"opens_before_final_signal":2,"outcome":"pass","satisfied":1},"opens":2,"phases":[{"event_type":"StartAccepted","sequence":null},{"event_type":"WorkflowStarted","sequence":null},{"event_type":"TimerScheduled","sequence":1},{"event_type":"ConditionWaitOpened","sequence":2},{"event_type":"TimerScheduled","sequence":2},{"event_type":"SignalReceived","sequence":null},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"ConditionWaitSatisfied","sequence":2},{"event_type":"TimerCancelled","sequence":2},{"event_type":"ConditionWaitOpened","sequence":3},{"event_type":"TimerScheduled","sequence":3},{"event_type":"TimerFired","sequence":3},{"event_type":"SelectionResolved","sequence":null},{"event_type":"WorkflowCompleted","sequence":null}],"result":{"outcome":"pass"}}
{"claim_process":"fresh-process-per-claim","mode":"parallel-timeout","next_signal":{"completed":true,"opens_before_final_signal":3,"outcome":"pass","satisfied":2},"opens":3,"phases":[{"event_type":"StartAccepted","sequence":null},{"event_type":"WorkflowStarted","sequence":null},{"event_type":"TimerScheduled","sequence":1},{"event_type":"SignalWaitOpened","sequence":2},{"event_type":"ConditionWaitOpened","sequence":3},{"event_type":"TimerScheduled","sequence":3},{"event_type":"SignalReceived","sequence":null},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"ConditionWaitSatisfied","sequence":3},{"event_type":"TimerCancelled","sequence":3},{"event_type":"ConditionWaitOpened","sequence":4},{"event_type":"TimerScheduled","sequence":4},{"event_type":"SignalReceived","sequence":null},{"event_type":"TimerFired","sequence":1},{"event_type":"MessageCursorAdvanced","sequence":null},{"event_type":"SignalApplied","sequence":2},{"event_type":"ConditionWaitSatisfied","sequence":4},{"event_type":"TimerCancelled","sequence":4},{"event_type":"ConditionWaitOpened","sequence":5},{"event_type":"TimerScheduled","sequence":5},{"event_type":"TimerFired","sequence":5},{"event_type":"WorkflowCompleted","sequence":null}],"result":{"outcome":"pass"}}

@rmcdaniel

Copy link
Copy Markdown
Member Author

Reviewed the final runtime diff and regression evidence at 71f31baabea53cb4f4d334c6291896987f88db3a.

  • Reopen admission requires the recorded authored occurrence, key, fingerprint, timeout and complete group path. New group validation and duplicate rejection remain strict.
  • A false wake preserves the pending logical member. Winner and parent completion use its latest physical wait, with the original durable operation identity.
  • Final selected satisfaction creates one successor only when no live workflow task exists. Terminal completion paths avoid an extra task.
  • Delayed timer closure validates the causal condition/signal operation while retaining timer transport status. Timeout acknowledgement keeps the first causal winner event.
  • The final full MySQL/PostgreSQL/MariaDB, Laravel 9–13, corpus, quality and coverage matrix passes: https://github.com/durable-workflow/workflow/actions/runs/36900284907.
  • Five actual fresh-process Rust Worker source cases pass, with raw evidence: fix: reopen recorded service conditions within groups #602 (comment).

Ready to merge the patch source. Issue #601 remains open for exact published Rust 2.1.3, Native 2.3.2 and stable Server follow-through. No cooperative feature activation is included.

@rmcdaniel
rmcdaniel marked this pull request as ready for review October 1, 2026 17:45
@rmcdaniel
rmcdaniel merged commit d8594a6 into main Oct 1, 2026
38 checks passed
@rmcdaniel
rmcdaniel deleted the fix/service-grouped-condition-reopen branch October 1, 2026 17:46
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.

2 participants