Skip to content

Commit 45f6e79

Browse files
Keep admitted Nexus calls pending during cold replay
1 parent 1265b62 commit 45f6e79

3 files changed

Lines changed: 46 additions & 2 deletions

File tree

‎src/durable_workflow/workflow.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6751,6 +6751,11 @@ def _advance_selection_clock(base: int, size: int, failure: BaseException | None
67516751
continue
67526752
ctx.logger._set_replaying(False)
67536753
_assert_pending_step_matches(cmd)
6754+
if _next_unconsumed_recorded_step() is not None:
6755+
# Admission already has its durable sequence. A later task
6756+
# waits for that call's terminal history instead of starting
6757+
# the same operation again at a new sequence.
6758+
return _state(pending)
67546759
pending.append(cmd)
67556760
return _state(pending)
67566761
if isinstance(cmd, RecordSideEffect):
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/durable-workflow/.github/main/regression-corpus/evidence-schema.json",
3+
"fixture_schema": "durable-workflow.replay-regression/v1",
4+
"id": "nexus-pending-cancellation-no-readmission",
5+
"protocol_version": "1.20",
6+
"bindings": ["python"],
7+
"workflow": {"type": "tests.replay.nexus-terminal-delivery", "input": [], "payload_codec": "avro"},
8+
"history": [
9+
{"event_type": "ServiceCallStarted", "payload": {
10+
"sequence": 1, "service_call_id": "original-service-call", "endpoint_name": "greeter",
11+
"service_name": "shared", "operation_name": "greet", "operation_mode": "sync", "wait_for": "completed",
12+
"status": "started", "service_call": {"service_call_id": "original-service-call", "target_namespace": "shared", "status": "started"}
13+
}},
14+
{"event_type": "CooperativeCancellationRequested", "recorded_at": "2026-10-12T07:00:00Z", "payload": {
15+
"workflow_command_id": "original-cancellation", "workflow_run_id": "caller-run",
16+
"cleanup_deadline_at": "2026-10-12T07:00:30Z"
17+
}}
18+
],
19+
"expected": {"command_sequence": []}
20+
}

‎tests/test_nexus_durable_wait.py‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ def test_new_nexus_call_serializes_the_leased_service_operation() -> None:
8484
def test_started_call_stays_pending_and_cold_replay_receives_terminal_result() -> None:
8585
started = _service_event("ServiceCallStarted", "started")
8686
pending = replay(DurableNexusCaller, [started], [], workflow_id="caller", run_id="caller-run")
87-
assert len(pending.commands) == 1
88-
assert isinstance(pending.commands[0], NexusServiceCall)
87+
assert pending.commands == []
8988

9089
expected = {"greeting": "hello world"}
9190
completed = _service_event("ServiceCallCompleted", "completed", expected)
@@ -95,6 +94,26 @@ def test_started_call_stays_pending_and_cold_replay_receives_terminal_result() -
9594
assert resumed.commands[0].result == expected
9695

9796

97+
def test_cancellation_request_and_cold_replays_do_not_readmit_pending_call() -> None:
98+
started = _service_event("ServiceCallStarted", "started")
99+
requested = {
100+
"event_type": "CooperativeCancellationRequested",
101+
"recorded_at": "2026-10-12T07:00:00Z",
102+
"payload": {
103+
"workflow_command_id": "original-cancellation",
104+
"workflow_run_id": "caller-run",
105+
"cleanup_deadline_at": "2026-10-12T07:00:30Z",
106+
},
107+
}
108+
for history in ([started], [started, requested]):
109+
for _ in range(3):
110+
# A fresh workflow instance represents both another task and a
111+
# replacement worker. Neither may allocate another admission.
112+
outcome = replay(DurableNexusCaller, history, [], workflow_id="caller", run_id="caller-run")
113+
assert outcome.commands == []
114+
assert outcome.cancellation_delivery is None
115+
116+
98117
@workflow.defn(name="tests.nexus.catch-failure")
99118
class CatchingNexusCaller:
100119
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]

0 commit comments

Comments
 (0)