Skip to content

Commit b0c9fc3

Browse files
authored
gh-156523: Fix asyncio.as_completed() not recording the awaiting task (#156527)
1 parent a11ab09 commit b0c9fc3

3 files changed

Lines changed: 68 additions & 0 deletions

File tree

Lib/asyncio/tasks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,9 +562,11 @@ def __init__(self, aws, timeout):
562562
self._timeout_handle = None
563563

564564
loop = events.get_event_loop()
565+
self._cur_task = current_task()
565566
todo = {ensure_future(aw, loop=loop) for aw in set(aws)}
566567
for f in todo:
567568
f.add_done_callback(self._handle_completion)
569+
futures.future_add_to_awaited_by(f, self._cur_task)
568570
if todo and timeout is not None:
569571
self._timeout_handle = (
570572
loop.call_later(timeout, self._handle_timeout)
@@ -595,13 +597,15 @@ def __next__(self):
595597
def _handle_timeout(self):
596598
for f in self._todo:
597599
f.remove_done_callback(self._handle_completion)
600+
futures.future_discard_from_awaited_by(f, self._cur_task)
598601
self._done.put_nowait(None) # Sentinel for _wait_for_one().
599602
self._todo.clear() # Can't do todo.remove(f) in the loop.
600603

601604
def _handle_completion(self, f):
602605
if not self._todo:
603606
return # _handle_timeout() was here first.
604607
self._todo.remove(f)
608+
futures.future_discard_from_awaited_by(f, self._cur_task)
605609
self._done.put_nowait(f)
606610
if not self._todo and self._timeout_handle is not None:
607611
self._timeout_handle.cancel()

Lib/test/test_asyncio/test_graph.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,68 @@ async def main(t1, t2):
298298
]
299299
])
300300

301+
async def test_stack_as_completed(self):
302+
# gh-156523: as_completed() must record the awaiting task
303+
stack_for_inner = None
304+
305+
async def inner():
306+
await asyncio.sleep(0)
307+
nonlocal stack_for_inner
308+
stack_for_inner = capture_test_stack()
309+
310+
async def main(t):
311+
for f in asyncio.as_completed([t]):
312+
await f
313+
314+
t = asyncio.create_task(inner(), name='inner')
315+
await main(t)
316+
self.assertFalse(t._asyncio_awaited_by)
317+
318+
self.assertEqual(stack_for_inner[0], [
319+
'T<inner>',
320+
['s capture_test_stack', 'a inner'],
321+
[
322+
['T<anon>',
323+
['a get', 'a _wait_for_one', 'a main',
324+
'a test_stack_as_completed'],
325+
[]
326+
]
327+
]
328+
])
329+
330+
async def test_stack_as_completed_timeout(self):
331+
# gh-156523: the awaiting task must be dropped when as_completed() times out
332+
stack_for_inner = None
333+
334+
async def inner():
335+
nonlocal stack_for_inner
336+
stack_for_inner = capture_test_stack()
337+
await asyncio.sleep(3600)
338+
339+
async def main(t):
340+
with self.assertRaises(TimeoutError):
341+
for f in asyncio.as_completed([t], timeout=0.01):
342+
await f
343+
344+
t = asyncio.create_task(inner(), name='inner')
345+
await main(t)
346+
self.assertFalse(t._asyncio_awaited_by)
347+
t.cancel()
348+
with self.assertRaises(asyncio.CancelledError):
349+
await t
350+
351+
self.assertEqual(stack_for_inner[0], [
352+
'T<inner>',
353+
['s capture_test_stack', 'a inner'],
354+
[
355+
['T<anon>',
356+
['a get', 'a _wait_for_one', 'a main',
357+
'a test_stack_as_completed_timeout'],
358+
[]
359+
]
360+
]
361+
])
362+
301363
async def test_stack_task(self):
302364

303365
stack_for_inner = None
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`asyncio.as_completed` not recording the awaiting task in the call
2+
graph.

0 commit comments

Comments
 (0)