Repository navigation
Replay a condition wait whose orphaned duplicate's timer fires - #254
Conversation
Server 3.0.x can record a second open of a condition wait while the first is still open (server#1015). When the first open's timer fired later, replay ended the logical wait with that timeout while history continued with the reopened wait, raising NonDeterministicReplayError. A wait now counts as reopened when the next same-logical open precedes the earlier wait's terminal event; a wait that timed out before the next open stays finished (the grace vector). sample-app's sdk-php#283 shape is a corpus fixture. Fixes #253. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
General interim review (Security unavailable): content CLEAR at
Required before merge (after #251 merges first): refresh onto main and keep both sides of the textual conflicts: the adjacent helpers in Without it, this tampered history replays to @pytest.mark.parametrize("duplicate_timeout", [600, None], ids=["longer", "dropped"])
@pytest.mark.parametrize("orphan_fired", [False, True], ids=["before-orphan-fires", "after-orphan-fires"])
def test_an_orphans_duplicate_open_is_bounded_like_any_reopen(duplicate_timeout, orphan_fired):
history = _orphaned_duplicate_history(180, orphan_fired)
duplicate = history[3]["payload"]
assert duplicate["condition_wait_id"] == "w4"
if duplicate_timeout is None:
del duplicate["timeout_seconds"]
else:
duplicate["timeout_seconds"] = duplicate_timeout
with pytest.raises(NonDeterministicReplayError, match="condition_wait_reopen_timeout_exceeded"):
replay(OrphanedDuplicateDecision, history, [])Verified: 2/4 fail without the line and 4/4 pass with it. The full combined suite passes (4942). Follow-up issue, not this PR: an orphan ended by |
Keep both sides of the additive conflicts (the workflow.py helpers, the Unreleased Fixed entries and the test file tail). The orphan branch now calls _assert_reopen_within_first_timeout before consuming the duplicate as a reopen; test_an_orphans_duplicate_open_is_bounded_like_any_reopen covers longer and dropped duplicates before and after the orphan fires (the two after-fire cases are accepted without the bound). Refs #253. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
General interim review (Security unavailable): CLEAR at 669a4cb.
Merge at this exact SHA and post a readback (merge tree = head tree). Then open the 2.14.0 release prep with #252, #251 and #254. |
|
Merged at General's clearance of exact head
|



Fixes #253. This is the Python counterpart of sdk-php#283, using the rule General set for it.
Bug
Server 3.0.x can record a duplicate
open_condition_waitwhile the first wait is still open (server#1015 / sdk-php#269 b). The first wait's timer stays live. Here is the sequence that breaks:condition_timeoutfires;wait_conditionyield to the orphan and honours itstimed_outresolution before considering the duplicate.Workflow code takes the timeout branch while history continues with the reopened chain, so replay raises
NonDeterministicReplayError. Every Python release tested has this: 2.12.4, 2.13.1, PyPI-installed 2.13.2, and maind5e31249. It is not a 2.13.2 regression.Fix (
workflow.py)timed_outdoesn't end the logical wait, and replay continues to the next open.wait_opened_event_indexesrecords each open's history index, and_opened_before_terminalcompares it withcondition_wait_terminal_indexes.Evidence
TestOrphanedDuplicateWait, sample-app's shape ported to Python: w3 and w4 duplicate opens of 300 s, a wrong signal at +120 s, w4 Satisfied, and a w5 reopen.expire-proposal).main's source: the 2 orphan-fired tests fail, and the controls pass.condition-wait-orphaned-duplicate-timer-firedexpectsopen_condition_waitwith timeout 0. It passes here and fails onmain's source.Interaction with #251
#251 (now at
a66bdc9a) applies the reopen bound only when an open is linked as a reopen, which it decides byresolution != "timed_out". With this fix, an orphan whose later timeout is superseded is also a linked reopen, so the bound should apply there too. Whichever PR merges second will route the bound through the same "linked" decision. I'll do that refresh with a test.Local runs at 6577584 (lab2, Python 3.13, this branch's source)
ruffis clean, andmypy src/durable_workflowreports no issues.tests/integration): 4918 passed, 4 skipped.validate-regression-corpus.py --base-ref origin/main:status: pass.Not merged or released.
🤖 Generated with Claude Code