Skip to content

Commit 903cf01

Browse files
deadlovelllmiss-islington
authored andcommitted
gh-157213: Fix stale asyncio.gather() edges in the await graph (GH-157214)
(cherry picked from commit 392ad7e) Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
1 parent 090b5cd commit 903cf01

4 files changed

Lines changed: 53 additions & 0 deletions

File tree

Lib/asyncio/tasks.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,11 @@ def cancel(self, msg=None):
770770
return ret
771771

772772

773+
def _discard_awaited_by(children, waiter, outer):
774+
for fut in children:
775+
futures.future_discard_from_awaited_by(fut, waiter)
776+
777+
773778
def gather(*coros_or_futures, return_exceptions=False):
774779
"""Return a future aggregating results from the given coroutines/futures.
775780
@@ -903,6 +908,10 @@ def _done_callback(fut, cur_task=cur_task):
903908
children.append(fut)
904909

905910
outer = _GatheringFuture(children, loop=loop)
911+
if cur_task is not None:
912+
# gh-157213: a child outliving gather() must lose the awaited-by edge
913+
outer.add_done_callback(
914+
functools.partial(_discard_awaited_by, children, cur_task))
906915
# Run done callbacks after GatheringFuture created so any post-processing
907916
# can be performed at this point
908917
# optimization: in the special case that *all* futures finished eagerly,

Lib/test/test_asyncio/test_graph.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,29 @@ async def main():
175175
]
176176
])
177177

178+
async def test_stack_gather_survivor(self):
179+
# gh-157213: a child that outlives gather() must not be shown as awaited
180+
181+
async def fail():
182+
raise ValueError
183+
184+
async def survivor():
185+
await asyncio.Future()
186+
187+
t = asyncio.create_task(survivor(), name='survivor')
188+
with self.assertRaises(ValueError):
189+
await asyncio.gather(t, fail())
190+
191+
self.assertEqual(capture_test_stack(fut=t)[0], [
192+
'T<survivor>',
193+
['a survivor'],
194+
[]
195+
])
196+
197+
t.cancel()
198+
with self.assertRaises(asyncio.CancelledError):
199+
await t
200+
178201
async def test_stack_shield(self):
179202

180203
stack_for_shield = None

Lib/test/test_asyncio/test_tasks.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,25 @@ async def coro():
12311231

12321232
self.loop.run_until_complete(self.new_task(self.loop, coro()))
12331233

1234+
def test_gather_discards_awaited_by_for_pending(self):
1235+
# gh-157213: a child outliving gather() must lose the awaited-by edge
1236+
async def fail():
1237+
raise ValueError
1238+
1239+
async def survivor():
1240+
await asyncio.Future()
1241+
1242+
async def coro():
1243+
t = self.new_task(self.loop, survivor())
1244+
with self.assertRaises(ValueError):
1245+
await asyncio.gather(t, fail())
1246+
self.assertFalse(t._asyncio_awaited_by)
1247+
t.cancel()
1248+
with self.assertRaises(asyncio.CancelledError):
1249+
await t
1250+
1251+
self.loop.run_until_complete(self.new_task(self.loop, coro()))
1252+
12341253
def test_wait_really_done(self):
12351254
# there is possibility that some tasks in the pending list
12361255
# became done but their callbacks haven't all been called yet
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`asyncio.gather` leaving stale await-graph edges on children that
2+
outlive it.

0 commit comments

Comments
 (0)