Skip to content

Commit 0d79042

Browse files
committed
avoid refcycles in trio.run, especially if an Exception is raised
1 parent 6bf1421 commit 0d79042

File tree

2 files changed

+85
-6
lines changed

2 files changed

+85
-6
lines changed

demo.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import trio
2+
3+
4+
async def main():
5+
err = None
6+
with trio.CancelScope() as scope:
7+
scope.cancel()
8+
try:
9+
await trio.sleep_forever()
10+
except BaseException as e:
11+
err = e
12+
raise
13+
breakpoint()
14+
15+
16+
# trio.run(main)
17+
18+
import gc
19+
20+
import objgraph
21+
from anyio import CancelScope, get_cancelled_exc_class
22+
23+
24+
async def test_exception_refcycles_propagate_cancellation_error() -> None:
25+
"""Test that TaskGroup deletes cancelled_exc"""
26+
exc = None
27+
28+
with CancelScope() as cs:
29+
cs.cancel()
30+
try:
31+
await trio.sleep_forever()
32+
except get_cancelled_exc_class() as e:
33+
exc = e
34+
raise
35+
36+
assert isinstance(exc, get_cancelled_exc_class())
37+
gc.collect()
38+
objgraph.show_chain(
39+
objgraph.find_backref_chain(
40+
gc.get_referrers(exc)[0],
41+
objgraph.is_proper_module,
42+
),
43+
)
44+
45+
46+
# trio.run(test_exception_refcycles_propagate_cancellation_error)
47+
48+
49+
class MyException(Exception):
50+
pass
51+
52+
53+
async def main():
54+
raise MyException
55+
56+
57+
def inner():
58+
try:
59+
trio.run(main)
60+
except MyException:
61+
pass
62+
63+
64+
import refcycle
65+
66+
gc.disable()
67+
gc.collect()
68+
inner()
69+
garbage = refcycle.garbage()
70+
for i, component in enumerate(garbage.source_components()):
71+
component.export_image(f"{i}_example.svg")
72+
garbage.export_image("example.svg")

src/trio/_core/_run.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -1706,6 +1706,7 @@ def close(self) -> None:
17061706
self.asyncgens.close()
17071707
if "after_run" in self.instruments:
17081708
self.instruments.call("after_run")
1709+
self.system_nursery: Nursery | None = None
17091710
# This is where KI protection gets disabled, so we do it last
17101711
self.ki_manager.close()
17111712

@@ -1920,6 +1921,7 @@ def task_exited(self, task: Task, outcome: Outcome[Any]) -> None:
19201921
task._activate_cancel_status(None)
19211922
self.tasks.remove(task)
19221923
if task is self.init_task:
1924+
self.init_task = None
19231925
# If the init task crashed, then something is very wrong and we
19241926
# let the error propagate. (It'll eventually be wrapped in a
19251927
# TrioInternalError.)
@@ -1930,6 +1932,7 @@ def task_exited(self, task: Task, outcome: Outcome[Any]) -> None:
19301932
raise TrioInternalError
19311933
else:
19321934
if task is self.main_task:
1935+
self.main_task = None
19331936
self.main_task_outcome = outcome
19341937
outcome = Value(None)
19351938
assert task._parent_nursery is not None, task
@@ -2394,12 +2397,15 @@ def run(
23942397
sniffio_library.name = prev_library
23952398
# Inlined copy of runner.main_task_outcome.unwrap() to avoid
23962399
# cluttering every single Trio traceback with an extra frame.
2397-
if isinstance(runner.main_task_outcome, Value):
2398-
return cast(RetT, runner.main_task_outcome.value)
2399-
elif isinstance(runner.main_task_outcome, Error):
2400-
raise runner.main_task_outcome.error
2401-
else: # pragma: no cover
2402-
raise AssertionError(runner.main_task_outcome)
2400+
try:
2401+
if isinstance(runner.main_task_outcome, Value):
2402+
return cast(RetT, runner.main_task_outcome.value)
2403+
elif isinstance(runner.main_task_outcome, Error):
2404+
raise runner.main_task_outcome.error
2405+
else: # pragma: no cover
2406+
raise AssertionError(runner.main_task_outcome)
2407+
finally:
2408+
del runner
24032409

24042410

24052411
def start_guest_run(
@@ -2808,6 +2814,7 @@ def unrolled_run(
28082814
if isinstance(runner.main_task_outcome, Error):
28092815
ki.__context__ = runner.main_task_outcome.error
28102816
runner.main_task_outcome = Error(ki)
2817+
del runner
28112818

28122819

28132820
################################################################

0 commit comments

Comments
 (0)