You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Replay a condition wait whose orphaned duplicate's timer fires #253
Python counterpart of sdk-php#283 (P0 in PHP, where it was a 3.9.2 regression). In Python it is not a regression: every release tested fails, including 2.12.4, 2.13.1, 2.13.2 (installed from PyPI) and main d5e31249.
Problem
Server 3.0.x can record a duplicate open_condition_wait (sdk-php#269 part b, server#1015). A second queued workflow task re-sends the open while the first wait is still open, so the Server opens a new sequence and leaves the first wait's timer live. If a wrong signal then satisfies and reopens the newer wait, and the orphan's condition_timeout timer fires later, Python replay fails with NonDeterministicReplayError at the duplicate's sequence.
Cause: the first wait_condition yield binds to the orphan's ConditionWaitOpened. Its later TimerFired resolution is honoured as timed_out before the duplicate open is considered. Workflow code therefore takes the timeout branch, for example scheduling an activity, while history continues with the duplicate → satisfied → reopen chain.
Reproduction
The shape is sample-app's, from sdk-php#283:
wait_condition(key="decision-1", timeout=300) opens w3 (seq 1, 300) and then w4 (seq 2, 300) with no wake between.
A wrong signal at +120 s leads to ConditionWaitSatisfied w4 and TimerCancelled, then a reopen w5 (seq 3, 300 or 180).
TimerFired condition_timeout for w3 at +300 s.
Before step 3, replay reopens correctly. After it, replay throws.
Done when
A wait counts as reopened when the next same-logical open (same key or fingerprint) was recorded before the wait's terminal event, even if its timer fired later. That is the state when the next wait opened, which is General's rule for sdk-php#283.
A wait that timed out before the next same-key open is still finished, and the next open is a separate call (the grace vector, TestWaitAfterATimedOutWait).
sample-app's shape is a corpus fixture. It replays to the reopen (no throw) on the fixed head and throws on main.
Tests cover both reopen shapes (300 and 180), and the grace and longer-grace controls.
Prepare Python SDK 2.14.0 #257 is merged at the approved head 48562620 as e2c607b86496523d34ba2347b961ee0ccb0e76bc, with parents main e319c9b0 and 48562620. The merge tree 72321dfb equals the head tree, so the go held. The merge carries version/product-train/registry-version 2.14.0.
The annotated tag 2.14.0 (object 73db6973) peels to e2c607b8.
Publisher: the tag push triggered publish.yml, run 41777555563, at exactly e2c607b8. Its build, publish, verify and github-release jobs all succeeded.
PyPI2.14.0 serves two files. Each is byte-identical to the run's dist artifact, and each sha256 matches the PyPI JSON:
Provenance: PyPI's integrity endpoint returns, for each file, a publish attestation from GitHub publisher durable-workflow/sdk-python, workflow publish.yml, environment pypi. The in-toto subject name and sha256 equal the downloaded file.
GitHub Release2.14.0 is not a draft or prerelease and targets main. releases/latest is 2.14.0.
Installed acceptance (lab2, arm64): fresh venvs installed from PyPI and run with python -I, against published native durableworkflow/server:3.0.5 (sha256:3cb6e5bf8ec766ac2abd933369b40a77af62794b6d7b921ce21d3fb263e88b72) on SQLite. The container and volume have since been removed.
Check
2.14.0
2.13.2 control
Orphaned-duplicate shape (sdk-php#283 / #253), reopen recorded as 300
replays, reopens with 0 (the first deadline has passed)
NonDeterministicReplayError
Same shape, reopen recorded as 180
replays, reopens with 0
NonDeterministicReplayError
Live typed activity round trip on 3.0.5: activity.defn(typed=True) and schedule_activity(result_type=Customer)
Activity
Merge main (#251) and bound an orphan's duplicate like any reopen
rmcdaniel commentedon Nov 4, 2026
Python SDK 2.14.0 is published, and its installed-package acceptance passes. This release carries #253, #248, #252 and #184 slice 1.
Release chain (General's conditional go on #257):
48562620ase2c607b86496523d34ba2347b961ee0ccb0e76bc, with parents maine319c9b0and48562620. The merge tree72321dfbequals the head tree, so the go held. The merge carries version/product-train/registry-version 2.14.0.2.14.0(object73db6973) peels toe2c607b8.publish.yml, run 41777555563, at exactlye2c607b8. Its build, publish, verify and github-release jobs all succeeded.distartifact, and each sha256 matches the PyPI JSON:durable_workflow-2.14.0-py3-none-any.whl, 339,804 bytes, sha256d98549a2a9a844733b19f1cabc33d28a9c01560b772b4cd28f4215268d4d0328;durable_workflow-2.14.0.tar.gz, 690,329 bytes, sha25615ec395de81eddabf5d139a4a294466c62ab851384ee47ba2e372505092ab9bc.durable-workflow/sdk-python, workflowpublish.yml, environmentpypi. The in-toto subject name and sha256 equal the downloaded file.releases/latestis 2.14.0.Installed acceptance (lab2, arm64): fresh venvs installed from PyPI and run with
python -I, against published nativedurableworkflow/server:3.0.5(sha256:3cb6e5bf8ec766ac2abd933369b40a77af62794b6d7b921ce21d3fb263e88b72) on SQLite. The container and volume have since been removed.NonDeterministicReplayErrorNonDeterministicReplayErroractivity.defn(typed=True)andschedule_activity(result_type=Customer){"tier": "gold", "tags": ["vip", "upgraded"], "type": "Customer"}