Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f695eca

Browse filesBrowse files
ChristianHrsambv
andauthored
gh-86802: Fix asyncio memory leak; shielded task exceptions log once through the exception handler (gh-134331)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent f3acbb7 commit f695eca
Copy full SHA for f695eca

File tree

3 files changed

+72
-7
lines changed
Filter options

3 files changed

+72
-7
lines changed

‎Lib/asyncio/tasks.py

Copy file name to clipboardExpand all lines: Lib/asyncio/tasks.py
+29-7Lines 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

Copy file name to clipboardExpand all lines: Lib/test/test_asyncio/test_tasks.py
+40Lines 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)
+3Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.