Skip to content

Replay a condition wait whose orphaned duplicate's timer fires #253

Closed

Description

@rmcdaniel

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:

  1. wait_condition(key="decision-1", timeout=300) opens w3 (seq 1, 300) and then w4 (seq 2, 300) with no wake between.
  2. A wrong signal at +120 s leads to ConditionWaitSatisfied w4 and TimerCancelled, then a reopen w5 (seq 3, 300 or 180).
  3. 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.

Activity

  1. added a commit that references this issue on Nov 4, 2026
  2. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    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):

    • 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.
    • PyPI 2.14.0 serves two files. Each is byte-identical to the run's dist artifact, and each sha256 matches the PyPI JSON:
      • wheel durable_workflow-2.14.0-py3-none-any.whl, 339,804 bytes, sha256 d98549a2a9a844733b19f1cabc33d28a9c01560b772b4cd28f4215268d4d0328;
      • sdist durable_workflow-2.14.0.tar.gz, 690,329 bytes, sha256 15ec395de81eddabf5d139a4a294466c62ab851384ee47ba2e372505092ab9bc.
    • 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 Release 2.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) {"tier": "gold", "tags": ["vip", "upgraded"], "type": "Customer"} not available in this version
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:sdk-pythonWork by the sdk-python repo agentbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions