Skip to content

Commit efdaae5

Browse files
miss-islingtonChristianHrsambv
authored
[3.14] gh-86802: Fix asyncio memory leak; shielded task exceptions log once through the exception handler (gh-134331) (gh-134343)
(cherry picked from commit f695eca) Co-authored-by: Christian Harries <[email protected]> Co-authored-by: Łukasz Langa <[email protected]>
1 parent 6ce2045 commit efdaae5

File tree

3 files changed

+72
-7
lines changed

3 files changed

+72
-7
lines changed

Lib/asyncio/tasks.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,25 @@ def _done_callback(fut, cur_task=cur_task):
908908
return outer
909909

910910

911+
def _log_on_exception(fut):
912+
if fut.cancelled():
913+
return
914+
915+
exc = fut.exception()
916+
if exc is None:
917+
return
918+
919+
context = {
920+
'message':
921+
f'{exc.__class__.__name__} exception in shielded future',
922+
'exception': exc,
923+
'future': fut,
924+
}
925+
if fut._source_traceback:
926+
context['source_traceback'] = fut._source_traceback
927+
fut._loop.call_exception_handler(context)
928+
929+
911930
def shield(arg):
912931
"""Wait for a future, shielding it from cancellation.
913932
@@ -953,14 +972,11 @@ def shield(arg):
953972
else:
954973
cur_task = None
955974

956-
def _inner_done_callback(inner, cur_task=cur_task):
957-
if cur_task is not None:
958-
futures.future_discard_from_awaited_by(inner, cur_task)
975+
def _clear_awaited_by_callback(inner):
976+
futures.future_discard_from_awaited_by(inner, cur_task)
959977

978+
def _inner_done_callback(inner):
960979
if outer.cancelled():
961-
if not inner.cancelled():
962-
# Mark inner's result as retrieved.
963-
inner.exception()
964980
return
965981

966982
if inner.cancelled():
@@ -972,10 +988,16 @@ def _inner_done_callback(inner, cur_task=cur_task):
972988
else:
973989
outer.set_result(inner.result())
974990

975-
976991
def _outer_done_callback(outer):
977992
if not inner.done():
978993
inner.remove_done_callback(_inner_done_callback)
994+
# Keep only one callback to log on cancel
995+
inner.remove_done_callback(_log_on_exception)
996+
inner.add_done_callback(_log_on_exception)
997+
998+
if cur_task is not None:
999+
inner.add_done_callback(_clear_awaited_by_callback)
1000+
9791001

9801002
inner.add_done_callback(_inner_done_callback)
9811003
outer.add_done_callback(_outer_done_callback)

Lib/test/test_asyncio/test_tasks.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,6 +2116,46 @@ def test_shield_cancel_outer(self):
21162116
self.assertTrue(outer.cancelled())
21172117
self.assertEqual(0, 0 if outer._callbacks is None else len(outer._callbacks))
21182118

2119+
def test_shield_cancel_outer_result(self):
2120+
mock_handler = mock.Mock()
2121+
self.loop.set_exception_handler(mock_handler)
2122+
inner = self.new_future(self.loop)
2123+
outer = asyncio.shield(inner)
2124+
test_utils.run_briefly(self.loop)
2125+
outer.cancel()
2126+
test_utils.run_briefly(self.loop)
2127+
inner.set_result(1)
2128+
test_utils.run_briefly(self.loop)
2129+
mock_handler.assert_not_called()
2130+
2131+
def test_shield_cancel_outer_exception(self):
2132+
mock_handler = mock.Mock()
2133+
self.loop.set_exception_handler(mock_handler)
2134+
inner = self.new_future(self.loop)
2135+
outer = asyncio.shield(inner)
2136+
test_utils.run_briefly(self.loop)
2137+
outer.cancel()
2138+
test_utils.run_briefly(self.loop)
2139+
inner.set_exception(Exception('foo'))
2140+
test_utils.run_briefly(self.loop)
2141+
mock_handler.assert_called_once()
2142+
2143+
def test_shield_duplicate_log_once(self):
2144+
mock_handler = mock.Mock()
2145+
self.loop.set_exception_handler(mock_handler)
2146+
inner = self.new_future(self.loop)
2147+
outer = asyncio.shield(inner)
2148+
test_utils.run_briefly(self.loop)
2149+
outer.cancel()
2150+
test_utils.run_briefly(self.loop)
2151+
outer = asyncio.shield(inner)
2152+
test_utils.run_briefly(self.loop)
2153+
outer.cancel()
2154+
test_utils.run_briefly(self.loop)
2155+
inner.set_exception(Exception('foo'))
2156+
test_utils.run_briefly(self.loop)
2157+
mock_handler.assert_called_once()
2158+
21192159
def test_shield_shortcut(self):
21202160
fut = self.new_future(self.loop)
21212161
fut.set_result(42)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed asyncio memory leak in cancelled shield tasks. For shielded tasks
2+
where the shield was cancelled, log potential exceptions through the
3+
exception handler. Contributed by Christian Harries.

0 commit comments

Comments
 (0)