Skip to content

Commit 88f0e31

Browse files
Finish fenced remote claims without delaying worker shutdown on observer cancellation
1 parent 9f36749 commit 88f0e31

2 files changed

Lines changed: 35 additions & 14 deletions

File tree

‎src/durable_workflow/worker.py‎

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2318,22 +2318,17 @@ async def observe_ownership() -> None:
23182318
return result
23192319
finally:
23202320
abandoned.set()
2321-
shutdown.cancel()
2322-
observation.cancel()
2323-
with contextlib.suppress(asyncio.CancelledError, Exception):
2324-
await shutdown
2325-
with contextlib.suppress(asyncio.CancelledError, Exception):
2326-
await observation
2327-
if not invocation.done():
2328-
invocation.cancel()
23292321

2330-
def discard_late_result(future: asyncio.Task[Any]) -> None:
2331-
if not future.cancelled():
2332-
future.exception()
2322+
def discard_late_result(future: asyncio.Task[Any]) -> None:
2323+
if not future.cancelled():
2324+
future.exception()
23332325

2334-
# A running thread or cancellation-resistant callable can outlive
2335-
# the await. Its heartbeat and eventual publication stay fenced.
2336-
invocation.add_done_callback(discard_late_result)
2326+
# The abandoned claim must finish without waiting for callback or
2327+
# observer cancellation. All late progress/publication stays fenced.
2328+
for background in (invocation, observation, shutdown):
2329+
if not background.done():
2330+
background.cancel()
2331+
background.add_done_callback(discard_late_result)
23372332

23382333
async def _run_activity_task(self, task: dict[str, Any]) -> str:
23392334
self._track_worker_session_from_task(task)

‎tests/test_cooperative_remote_worker.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,32 @@ def callback() -> object:
121121
release.set()
122122

123123

124+
async def test_shutdown_expiry_of_tracked_remote_attempt_fences_a_cancellation_resistant_result(owner) -> None:
125+
worker, client = owner
126+
worker._shutdown_timeout = 0.01
127+
entered, late_fenced = asyncio.Event(), asyncio.Event()
128+
129+
async def callback() -> object:
130+
entered.set()
131+
try:
132+
await asyncio.Event().wait()
133+
except asyncio.CancelledError:
134+
with pytest.raises(_RemoteActivityExecutionAborted):
135+
await activity.context().heartbeat()
136+
late_fenced.set()
137+
return object()
138+
139+
worker.activities["remote"] = callback
140+
execution = worker._track(worker._run_activity_task(task()))
141+
await asyncio.wait_for(entered.wait(), timeout=2)
142+
await asyncio.wait_for(worker.stop(), timeout=2)
143+
await asyncio.wait_for(late_fenced.wait(), timeout=2)
144+
assert execution.cancelled() or execution.result() == "claim_aborted"
145+
client.heartbeat_activity_task.assert_not_awaited()
146+
client.complete_activity_task.assert_not_awaited()
147+
client.fail_activity_task.assert_not_awaited()
148+
149+
124150
@pytest.mark.parametrize("synchronous", [False, True])
125151
async def test_authored_heartbeat_stays_on_owner_loop_and_preserves_typed_result(owner, synchronous: bool) -> None:
126152
worker, client = owner

0 commit comments

Comments
 (0)