@@ -125,6 +125,22 @@ def _slow_store() -> tuple[Store, str]:
125125 return store , store .to_json ()
126126
127127
128+ def _async_slow_store () -> tuple [Store , str ]:
129+ store = Store ()
130+ node = _node ("slow_async" , (
131+ "async def slow_async(n):\n "
132+ " import asyncio\n "
133+ " total = 0\n "
134+ " for i in range(n):\n "
135+ " await asyncio.sleep(0.1)\n "
136+ " total += i\n "
137+ " return total"
138+ ))
139+ h = store .put (node )
140+ store .set_ref ("slow_async" , h )
141+ return store , store .to_json ()
142+
143+
128144def _progress_store () -> tuple [Store , str ]:
129145 store = Store ()
130146 node = _node ("process" , (
@@ -238,8 +254,42 @@ async def test_handle_task_skips_cancelled(self, backend: InMemoryBackend) -> No
238254 assert task .task_id not in backend .results
239255
240256 @pytest .mark .asyncio
241- async def test_cancel_during_execution (self , backend : InMemoryBackend ) -> None :
242- """Worker sends its result normally; cancel semantics are client-side."""
257+ async def test_cancel_during_async_execution (
258+ self , backend : InMemoryBackend , monkeypatch : pytest .MonkeyPatch ,
259+ ) -> None :
260+ """Cancelling during async execution interrupts the function."""
261+ monkeypatch .setattr (_remote , "_HEARTBEAT_INTERVAL" , 0.1 )
262+
263+ _ , json_str = _async_slow_store ()
264+ # 30 iterations × 0.1 s = 3 s without cancellation
265+ task = Task (graph_json = json_str , function_name = "slow_async" , args = (30 ,))
266+
267+ worker = Worker (auto_install = False )
268+
269+ async def cancel_later () -> None :
270+ await asyncio .sleep (0.15 )
271+ await backend .cancel_task (task .task_id )
272+
273+ t0 = time .monotonic ()
274+ asyncio .create_task (cancel_later ())
275+ await _handle_task (worker , backend , task .to_json ())
276+ elapsed = time .monotonic () - t0
277+
278+ # Should finish well before 3 s (the function was interrupted)
279+ assert elapsed < 1.5
280+
281+ # Worker sends a cancelled envelope
282+ assert task .task_id in backend .results
283+ env = ResultEnvelope .from_json (backend .results [task .task_id ])
284+ assert env .status == "cancelled"
285+
286+ @pytest .mark .asyncio
287+ async def test_cancel_during_sync_execution (
288+ self , backend : InMemoryBackend , monkeypatch : pytest .MonkeyPatch ,
289+ ) -> None :
290+ """Cancelling during sync execution marks result as cancelled."""
291+ monkeypatch .setattr (_remote , "_HEARTBEAT_INTERVAL" , 0.1 )
292+
243293 _ , json_str = _slow_store ()
244294 task = Task (graph_json = json_str , function_name = "slow" , args = (5 ,))
245295
@@ -252,10 +302,10 @@ async def cancel_later() -> None:
252302 asyncio .create_task (cancel_later ())
253303 await _handle_task (worker , backend , task .to_json ())
254304
255- # Worker always sends its result; client reads cancel envelope first
305+ # Executor thread can't be interrupted, but the coroutine is cancelled
256306 assert task .task_id in backend .results
257307 env = ResultEnvelope .from_json (backend .results [task .task_id ])
258- assert env .status == "ok "
308+ assert env .status == "cancelled "
259309
260310
261311# ---------------------------------------------------------------------------
0 commit comments