Skip to content
Merged
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
22 changes: 0 additions & 22 deletions Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,25 +927,6 @@ def _done_callback(fut, cur_task=cur_task):
return outer


def _log_on_exception(fut):
if fut.cancelled():
return

exc = fut.exception()
if exc is None:
return

context = {
'message':
f'{exc.__class__.__name__} exception in shielded future',
'exception': exc,
'future': fut,
}
if fut._source_traceback:
context['source_traceback'] = fut._source_traceback
fut._loop.call_exception_handler(context)


def shield(arg):
"""Wait for a future, shielding it from cancellation.
Expand Down Expand Up @@ -1010,9 +991,6 @@ def _inner_done_callback(inner):
def _outer_done_callback(outer):
if not inner.done():
inner.remove_done_callback(_inner_done_callback)
# Keep only one callback to log on cancel
inner.remove_done_callback(_log_on_exception)
inner.add_done_callback(_log_on_exception)
if cur_task is not None:
inner.remove_done_callback(_clear_awaited_by_callback)
futures.future_discard_from_awaited_by(inner, cur_task)
Expand Down
34 changes: 30 additions & 4 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2148,7 +2148,7 @@ def test_shield_cancel_outer(self):
self.assertTrue(outer.cancelled())
self.assertEqual(0, 0 if outer._callbacks is None else len(outer._callbacks))
self.assertFalse(inner._asyncio_awaited_by)
self.assertTrue({f for f, _ctx in inner._callbacks or []} <= {asyncio.tasks._log_on_exception})
self.assertFalse(inner._callbacks)

def test_shield_cancel_outer_result(self):
mock_handler = mock.Mock()
Expand All @@ -2163,6 +2163,8 @@ def test_shield_cancel_outer_result(self):
mock_handler.assert_not_called()

def test_shield_cancel_outer_exception(self):
# gh-156321: an exception in the inner future must not be reported
# eagerly, as it may still be retrieved later.
mock_handler = mock.Mock()
self.loop.set_exception_handler(mock_handler)
inner = self.new_future(self.loop)
Expand All @@ -2172,7 +2174,29 @@ def test_shield_cancel_outer_exception(self):
test_utils.run_briefly(self.loop)
inner.set_exception(Exception('foo'))
test_utils.run_briefly(self.loop)
mock_handler.assert_not_called()
self.assertIsInstance(inner.exception(), Exception)

def test_shield_cancel_outer_exception_never_retrieved(self):
# gh-156321: an exception nobody retrieves is reported by the inner
# future itself when it is garbage collected, like any other future.
mock_handler = mock.Mock()
self.loop.set_exception_handler(mock_handler)
inner = self.new_future(self.loop)
outer = asyncio.shield(inner)
test_utils.run_briefly(self.loop)
outer.cancel()
test_utils.run_briefly(self.loop)
inner.set_exception(Exception('foo'))
test_utils.run_briefly(self.loop)
mock_handler.assert_not_called()
inner = None
outer = None
support.gc_collect()
mock_handler.assert_called_once()
context = mock_handler.call_args[0][1]
self.assertEndsWith(context['message'], 'exception was never retrieved')
self.assertIsInstance(context['exception'], Exception)

def test_shield_cancel_outer_in_task(self):
inner = self.new_future(self.loop)
Expand All @@ -2187,9 +2211,9 @@ async def coro():
task = self.new_task(self.loop, coro())
self.loop.run_until_complete(task)
self.assertFalse(inner._asyncio_awaited_by)
self.assertTrue({f for f, _ctx in inner._callbacks or []} <= {asyncio.tasks._log_on_exception})
self.assertFalse(inner._callbacks)

def test_shield_duplicate_log_once(self):
def test_shield_cancel_outer_twice_exception(self):
mock_handler = mock.Mock()
self.loop.set_exception_handler(mock_handler)
inner = self.new_future(self.loop)
Expand All @@ -2201,9 +2225,11 @@ def test_shield_duplicate_log_once(self):
test_utils.run_briefly(self.loop)
outer.cancel()
test_utils.run_briefly(self.loop)
self.assertFalse(inner._callbacks)
inner.set_exception(Exception('foo'))
test_utils.run_briefly(self.loop)
mock_handler.assert_called_once()
mock_handler.assert_not_called()
self.assertIsInstance(inner.exception(), Exception)

def test_shield_shortcut(self):
fut = self.new_future(self.loop)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix :func:`asyncio.shield` reporting an exception from the inner future
through the loop exception handler as soon as it completes after the shield
was cancelled, even when the exception is retrieved afterwards. The
exception is now reported only if it is never retrieved, as with any other
future.
Loading