From bbd4f856bc5821d90b78c75b14bc142a3032b692 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Fri, 8 Jan 2021 21:54:09 +0900 Subject: [PATCH] Avoid creating reference cycles in case of nursery cancel --- trio/_core/_multierror.py | 2 +- trio/_core/_run.py | 7 ++++++- trio/_core/tests/test_run.py | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/trio/_core/_multierror.py b/trio/_core/_multierror.py index 08cc22cc63..13fc3f3d0f 100644 --- a/trio/_core/_multierror.py +++ b/trio/_core/_multierror.py @@ -114,7 +114,7 @@ def push_tb_down(tb, exc, preserved): preserved = set() new_root_exc = filter_tree(root_exc, preserved) push_tb_down(None, root_exc, preserved) - # Delete the local functions avoid a reference cycle (see + # Delete the local functions to avoid a reference cycle (see # test_simple_cancel_scope_usage_doesnt_create_cyclic_garbage) del filter_tree, push_tb_down return new_root_exc diff --git a/trio/_core/_run.py b/trio/_core/_run.py index fe51f7bfc8..2dc6d43f5e 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -937,7 +937,12 @@ def aborted(raise_cancel): popped = self._parent_task._child_nurseries.pop() assert popped is self if self._pending_excs: - return MultiError(self._pending_excs) + try: + return MultiError(self._pending_excs) + finally: + # avoid a garbage cycle + # (see test_nursery_cancel_doesnt_create_cyclic_garbage) + del self._pending_excs def start_soon(self, async_fn, *args, name=None): """Creates a child task, scheduling ``await async_fn(*args)``. diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index 060b0b0c93..4c4e12b5df 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -2226,3 +2226,26 @@ async def do_a_cancel(): finally: gc.set_debug(old_flags) gc.garbage.clear() + + +@pytest.mark.skipif( + sys.implementation.name != "cpython", reason="Only makes sense with refcounting GC" +) +async def test_nursery_cancel_doesnt_create_cyclic_garbage(): + # https://github.com/python-trio/trio/issues/1770#issuecomment-730229423 + gc.collect() + + old_flags = gc.get_debug() + try: + for i in range(3): + async with _core.open_nursery() as nursery: + gc.collect() + gc.set_debug(gc.DEBUG_LEAK) + nursery.cancel_scope.cancel() + + gc.collect() + gc.set_debug(0) + assert not gc.garbage + finally: + gc.set_debug(old_flags) + gc.garbage.clear()