Skip to content

Commit 0b28ea4

Browse files
authored
pythongh-124958: Revert "pythongh-125472: Revert "pythongh-124958: fix asyncio.TaskGroup and _PyFuture refcycles ... (python#125486)
* Revert "pythongh-125472: Revert "pythongh-124958: fix asyncio.TaskGroup and _PyFuture refcycles (#12… (python#125476)" This reverts commit e99650b. * fix incompatability with pythongh-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)