Skip to content

Commit bc1cd2e

Browse files
committed
gh-155854: Fix asyncio.shield() leaking cancelled waiters
1 parent fcfa919 commit bc1cd2e

3 files changed

Lines changed: 19 additions & 0 deletions

File tree

Lib/asyncio/tasks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,10 @@ def _inner_done_callback(inner):
994994
def _outer_done_callback(outer):
995995
if not inner.done():
996996
inner.remove_done_callback(_inner_done_callback)
997+
# gh-155854: waiter is gone but inner lives on, clean up here
998+
if cur_task is not None:
999+
inner.remove_done_callback(_clear_awaited_by_callback)
1000+
futures.future_discard_from_awaited_by(inner, cur_task)
9971001
# Keep only one callback to log on cancel
9981002
inner.remove_done_callback(_log_on_exception)
9991003
inner.add_done_callback(_log_on_exception)

Lib/test/test_asyncio/test_tasks.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2150,6 +2150,19 @@ def test_shield_duplicate_log_once(self):
21502150
test_utils.run_briefly(self.loop)
21512151
mock_handler.assert_called_once()
21522152

2153+
def test_shield_discards_awaited_by_on_outer_cancel(self):
2154+
# gh-155854: a cancelled waiter must not stay in inner's await-graph
2155+
async def coro():
2156+
inner = self.new_future(self.loop)
2157+
for _ in range(3):
2158+
asyncio.shield(inner).cancel()
2159+
await asyncio.sleep(0)
2160+
self.assertFalse(inner._asyncio_awaited_by)
2161+
self.assertEqual(1, len(inner._callbacks))
2162+
inner.cancel()
2163+
2164+
self.loop.run_until_complete(self.new_task(self.loop, coro()))
2165+
21532166
def test_shield_shortcut(self):
21542167
fut = self.new_future(self.loop)
21552168
fut.set_result(42)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`asyncio.shield` keeping cancelled waiters alive via
2+
``awaited_by``.

0 commit comments

Comments
 (0)