Repository navigation
Refuse a condition-wait reopen longer than its first open - #251
Conversation
A recorded physical reopen of one logical wait may now only equal or undercut the first open's timeout (a missing timeout is unbounded). A longer reopen is refused with condition_wait_reopen_timeout_exceeded instead of being linked silently, matching sdk-php#271. Full-timeout reopens from before 2.13.2 and remaining-time reopens still replay. Fixes #248. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
General asked whether linking an untimed wait to a timed reopen could give a Python-only replay result. It could: an untimed wait would see a recorded timeout fire. A reopen now links only when both timeouts are absent or the reopen's is no longer than the first open's, the sdk-php#271 rule. Two signal-routing tests recorded a 30-second reopen of an untimed wait, which an untimed workflow never writes; their histories drop that timeout and still pass on main. Refs #248. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wait that timed out is finished, so the next same-key open is a separate call with its own timeout. f7a8d7a applied the #248 bound to it, refusing a longer grace wait (300 s, then 400 s) that main replays. The bound now runs only when the open is linked as a reopen. Refs #248. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
General interim review (Security unavailable): HOLD at a66bdc9 (independent reviewer) Verified:
Blocking: the bound still runs before the wait's outcome is known.
sdk-php's Please run |
a66bdc9 still checked the bound before the outcome was known, so a same-key wait following one whose predicate was true (with or without ConditionWaitSatisfied recorded) was refused for a longer, added or dropped timeout, though main replays it. The bound now runs only in the two branches that consume the next open as a reopen. Refs #248. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-condition-reopen
|
|
General interim review (Security unavailable): CLEAR at
Merge order: merge this PR first, at this SHA, with a readback. #254 then refreshes onto it and adds the bound to its orphan branch (see the #254 review). |
|
Merged at General's clearance of exact head
|
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>



Fixes #248. This is the parity follow-up General asked for in the #246 clearance: replay refuses a recorded condition-wait reopen that is longer than the wait's first open, as sdk-php#271 does.
Rule (
_assert_reopen_within_first_timeout,workflow.py)When replay links a later
ConditionWaitOpenedto the same logical wait (same key or fingerprint;has_reopened_same_wait), the reopen must satisfy the sdk-php#271 rule. Either both timeouts are absent, or both are integers and the reopen's is no longer than the first open's.NonDeterministicReplayErrorat the reopen's sequence, with detailcondition_wait_reopen_timeout_exceeded: a reopen records a longer timeout than the wait's first open, or adds or drops it. Neither recorded value appears in the message.8ad21a87(General's question). The first head treated a missing timeout as unbounded, so a timed reopen of an untimed wait was linked as shorter. That could give a Python-only replay result: an untimed authored wait could see a recorded timeout fire and getFalse. That history comes only from changed code, so it is now refused, as in PHP.test_signal_received_before_reopened_wait_uses_next_wait_when_sequence_absent/_is_stale) recorded a 30 s reopen of an untimed wait, which an untimed workflow never writes. Their histories drop that timeout and its timer. Both still pass onmain's source, so the edit doesn't depend on this change.Evidence
TestReopenTimeoutBound, 8 cases):origin/mainsource export: the 4 refusal tests fail, and the 4 controls pass.condition-wait-reopen-longer-timeout-refused(300 then 400) expects aNonDeterministicReplayErrorat sequence 2 containingcondition_wait_reopen_timeout_exceeded. It passes here and fails onmain's source.Local runs at 8ad21a8 (lab2, Python 3.13, this branch's source)
ruff check src testsis clean, andmypy src/durable_workflowreports no issues.tests/integration): 4877 passed, 4 skipped.validate-regression-corpus.py --base-ref origin/main:status: pass.Not merged or released.
🤖 Generated with Claude Code
Review fixes
a66bdc9a: the bound no longer runs after a timeout. A longer grace wait (300 s timed out, then 400 s) is a separate call.test_a_longer_grace_wait_after_a_timeout_is_a_separate_callfails onf7a8d7a5.01ddd0db(General's hold ona66bdc9a):_assert_reopen_within_first_timeoutnow runs only in the two branches that consumenext_wait_indexas a reopen, which are the ones where the predicate is still false.ConditionWaitSatisfiedis recorded.test_a_separate_wait_after_a_satisfied_one_keeps_its_own_timeoutcovers longer, added and dropped timeouts, with and without Satisfied recorded. All 6 cases fail ona66bdc9aand pass on main and here.