Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,10 @@ def _inner_done_callback(inner):
def _outer_done_callback(outer):
if not inner.done():
inner.remove_done_callback(_inner_done_callback)
# gh-155854: waiter is gone but inner lives on, clean up here
if cur_task is not None:
inner.remove_done_callback(_clear_awaited_by_callback)
futures.future_discard_from_awaited_by(inner, cur_task)
# Keep only one callback to log on cancel
inner.remove_done_callback(_log_on_exception)
inner.add_done_callback(_log_on_exception)
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2150,6 +2150,19 @@ def test_shield_duplicate_log_once(self):
test_utils.run_briefly(self.loop)
mock_handler.assert_called_once()

def test_shield_discards_awaited_by_on_outer_cancel(self):
# gh-155854: a cancelled waiter must not stay in inner's await-graph
async def coro():
inner = self.new_future(self.loop)
for _ in range(3):
asyncio.shield(inner).cancel()
await asyncio.sleep(0)
self.assertFalse(inner._asyncio_awaited_by)
self.assertEqual(1, len(inner._callbacks))
inner.cancel()

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

def test_shield_shortcut(self):
fut = self.new_future(self.loop)
fut.set_result(42)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :func:`asyncio.shield` keeping cancelled waiters alive via
``awaited_by``.
Loading