Skip to content

Commit 71f37f5

Browse files
[3.15] gh-157213: Fix stale asyncio.gather() edges in the await graph (GH-157214) (#157312)
Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
1 parent 929c34f commit 71f37f5

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
@@ -775,6 +775,11 @@ def cancel(self, msg=None):
775775
return ret
776776

777777

778+
def _discard_awaited_by(children, waiter, outer):
779+
for fut in children:
780+
futures.future_discard_from_awaited_by(fut, waiter)
781+
782+
778783
def gather(*coros_or_futures, return_exceptions=False):
779784
"""Return a future aggregating results from the given coroutines/futures.
780785
@@ -908,6 +913,10 @@ def _done_callback(fut, cur_task=cur_task):
908913
children.append(fut)
909914

910915
outer = _GatheringFuture(children, loop=loop)
916+
if cur_task is not None:
917+
# gh-157213: a child outliving gather() must lose the awaited-by edge
918+
outer.add_done_callback(
919+
functools.partial(_discard_awaited_by, children, cur_task))
911920
# Run done callbacks after GatheringFuture created so any post-processing
912921
# can be performed at this point
913922
# 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
@@ -202,6 +202,29 @@ async def main():
202202
]
203203
])
204204

205+
async def test_stack_gather_survivor(self):
206+
# gh-157213: a child that outlives gather() must not be shown as awaited
207+
208+
async def fail():
209+
raise ValueError
210+
211+
async def survivor():
212+
await asyncio.Future()
213+
214+
t = asyncio.create_task(survivor(), name='survivor')
215+
with self.assertRaises(ValueError):
216+
await asyncio.gather(t, fail())
217+
218+
self.assertEqual(capture_test_stack(fut=t)[0], [
219+
'T<survivor>',
220+
['a survivor'],
221+
[]
222+
])
223+
224+
t.cancel()
225+
with self.assertRaises(asyncio.CancelledError):
226+
await t
227+
205228
async def test_stack_shield(self):
206229

207230
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)