Skip to content

Commit 0b28ea4

Browse files
authored
gh-124958: Revert "gh-125472: Revert "gh-124958: fix asyncio.TaskGroup and _PyFuture refcycles ... (#125486)
* Revert "gh-125472: Revert "gh-124958: fix asyncio.TaskGroup and _PyFuture refcycles (#12… (#125476)" This reverts commit e99650b. * fix incompatability with gh-124392
1 parent 1bffd7a commit 0b28ea4

File tree

5 files changed

+157
-15
lines changed

5 files changed

+157
-15
lines changed

Lib/asyncio/futures.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,7 @@ def result(self):
190190
the future is done and has an exception set, this exception is raised.
191191
"""
192192
if self._state == _CANCELLED:
193-
exc = self._make_cancelled_error()
194-
raise exc
193+
raise self._make_cancelled_error()
195194
if self._state != _FINISHED:
196195
raise exceptions.InvalidStateError('Result is not ready.')
197196
self.__log_traceback = False
@@ -208,8 +207,7 @@ def exception(self):
208207
InvalidStateError.
209208
"""
210209
if self._state == _CANCELLED:
211-
exc = self._make_cancelled_error()
212-
raise exc
210+
raise self._make_cancelled_error()
213211
if self._state != _FINISHED:
214212
raise exceptions.InvalidStateError('Exception is not set.')
215213
self.__log_traceback = False

Lib/asyncio/taskgroups.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ async def __aenter__(self):
6666
return self
6767

6868
async def __aexit__(self, et, exc, tb):
69+
tb = None
70+
try:
71+
return await self._aexit(et, exc)
72+
finally:
73+
# Exceptions are heavy objects that can have object
74+
# cycles (bad for GC); let's not keep a reference to
75+
# a bunch of them. It would be nicer to use a try/finally
76+
# in __aexit__ directly but that introduced some diff noise
77+
self._parent_task = None
78+
self._errors = None
79+
self._base_error = None
80+
exc = None
81+
82+
async def _aexit(self, et, exc):
6983
self._exiting = True
7084

7185
if (exc is not None and
@@ -122,7 +136,10 @@ async def __aexit__(self, et, exc, tb):
122136
assert not self._tasks
123137

124138
if self._base_error is not None:
125-
raise self._base_error
139+
try:
140+
raise self._base_error
141+
finally:
142+
exc = None
126143

127144
if self._parent_cancel_requested:
128145
# If this flag is set we *must* call uncancel().
@@ -133,8 +150,14 @@ async def __aexit__(self, et, exc, tb):
133150

134151
# Propagate CancelledError if there is one, except if there
135152
# are other errors -- those have priority.
136-
if propagate_cancellation_error is not None and not self._errors:
137-
raise propagate_cancellation_error
153+
try:
154+
if propagate_cancellation_error is not None and not self._errors:
155+
try:
156+
raise propagate_cancellation_error
157+
finally:
158+
exc = None
159+
finally:
160+
propagate_cancellation_error = None
138161

139162
if et is not None and not issubclass(et, exceptions.CancelledError):
140163
self._errors.append(exc)
@@ -146,14 +169,14 @@ async def __aexit__(self, et, exc, tb):
146169
if self._parent_task.cancelling():
147170
self._parent_task.uncancel()
148171
self._parent_task.cancel()
149-
# Exceptions are heavy objects that can have object
150-
# cycles (bad for GC); let's not keep a reference to
151-
# a bunch of them.
152172
try:
153-
me = BaseExceptionGroup('unhandled errors in a TaskGroup', self._errors)
154-
raise me from None
173+
raise BaseExceptionGroup(
174+
'unhandled errors in a TaskGroup',
175+
self._errors,
176+
) from None
155177
finally:
156-
self._errors = None
178+
exc = None
179+
157180

158181
def create_task(self, coro, *, name=None, context=None):
159182
"""Create a new task in this group and return it.

Lib/test/test_asyncio/test_futures.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,28 @@ def __del__(self):
659659
fut = self._new_future(loop=self.loop)
660660
fut.set_result(Evil())
661661

662+
def test_future_cancelled_result_refcycles(self):
663+
f = self._new_future(loop=self.loop)
664+
f.cancel()
665+
exc = None
666+
try:
667+
f.result()
668+
except asyncio.CancelledError as e:
669+
exc = e
670+
self.assertIsNotNone(exc)
671+
self.assertListEqual(gc.get_referrers(exc), [])
672+
673+
def test_future_cancelled_exception_refcycles(self):
674+
f = self._new_future(loop=self.loop)
675+
f.cancel()
676+
exc = None
677+
try:
678+
f.exception()
679+
except asyncio.CancelledError as e:
680+
exc = e
681+
self.assertIsNotNone(exc)
682+
self.assertListEqual(gc.get_referrers(exc), [])
683+
662684

663685
@unittest.skipUnless(hasattr(futures, '_CFuture'),
664686
'requires the C _asyncio module')

Lib/test/test_asyncio/test_taskgroups.py

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Adapted with permission from the EdgeDB project;
22
# license: PSFL.
33

4-
4+
import sys
5+
import gc
56
import asyncio
67
import contextvars
78
import contextlib
@@ -11,7 +12,6 @@
1112

1213
from test.test_asyncio.utils import await_without_task
1314

14-
1515
# To prevent a warning "test altered the execution environment"
1616
def tearDownModule():
1717
asyncio.set_event_loop_policy(None)
@@ -29,6 +29,15 @@ def get_error_types(eg):
2929
return {type(exc) for exc in eg.exceptions}
3030

3131

32+
def no_other_refs():
33+
# due to gh-124392 coroutines now refer to their locals
34+
coro = asyncio.current_task().get_coro()
35+
frame = sys._getframe(1)
36+
while coro.cr_frame != frame:
37+
coro = coro.cr_await
38+
return [coro]
39+
40+
3241
class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
3342

3443
async def test_taskgroup_01(self):
@@ -899,6 +908,95 @@ async def outer():
899908

900909
await outer()
901910

911+
async def test_exception_refcycles_direct(self):
912+
"""Test that TaskGroup doesn't keep a reference to the raised ExceptionGroup"""
913+
tg = asyncio.TaskGroup()
914+
exc = None
915+
916+
class _Done(Exception):
917+
pass
918+
919+
try:
920+
async with tg:
921+
raise _Done
922+
except ExceptionGroup as e:
923+
exc = e
924+
925+
self.assertIsNotNone(exc)
926+
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
927+
928+
929+
async def test_exception_refcycles_errors(self):
930+
"""Test that TaskGroup deletes self._errors, and __aexit__ args"""
931+
tg = asyncio.TaskGroup()
932+
exc = None
933+
934+
class _Done(Exception):
935+
pass
936+
937+
try:
938+
async with tg:
939+
raise _Done
940+
except* _Done as excs:
941+
exc = excs.exceptions[0]
942+
943+
self.assertIsInstance(exc, _Done)
944+
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
945+
946+
947+
async def test_exception_refcycles_parent_task(self):
948+
"""Test that TaskGroup deletes self._parent_task"""
949+
tg = asyncio.TaskGroup()
950+
exc = None
951+
952+
class _Done(Exception):
953+
pass
954+
955+
async def coro_fn():
956+
async with tg:
957+
raise _Done
958+
959+
try:
960+
async with asyncio.TaskGroup() as tg2:
961+
tg2.create_task(coro_fn())
962+
except* _Done as excs:
963+
exc = excs.exceptions[0].exceptions[0]
964+
965+
self.assertIsInstance(exc, _Done)
966+
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
967+
968+
async def test_exception_refcycles_propagate_cancellation_error(self):
969+
"""Test that TaskGroup deletes propagate_cancellation_error"""
970+
tg = asyncio.TaskGroup()
971+
exc = None
972+
973+
try:
974+
async with asyncio.timeout(-1):
975+
async with tg:
976+
await asyncio.sleep(0)
977+
except TimeoutError as e:
978+
exc = e.__cause__
979+
980+
self.assertIsInstance(exc, asyncio.CancelledError)
981+
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
982+
983+
async def test_exception_refcycles_base_error(self):
984+
"""Test that TaskGroup deletes self._base_error"""
985+
class MyKeyboardInterrupt(KeyboardInterrupt):
986+
pass
987+
988+
tg = asyncio.TaskGroup()
989+
exc = None
990+
991+
try:
992+
async with tg:
993+
raise MyKeyboardInterrupt
994+
except MyKeyboardInterrupt as e:
995+
exc = e
996+
997+
self.assertIsNotNone(exc)
998+
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
999+
9021000

9031001
if __name__ == "__main__":
9041002
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix refcycles in exceptions raised from :class:`asyncio.TaskGroup` and the python implementation of :class:`asyncio.Future`

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy