diff --git a/newsfragments/1864.bugfix.rst b/newsfragments/1864.bugfix.rst new file mode 100644 index 0000000000..440984a091 --- /dev/null +++ b/newsfragments/1864.bugfix.rst @@ -0,0 +1,2 @@ +The event loop now holds on to references of coroutine frames for only +the minimum necessary period of time. \ No newline at end of file diff --git a/trio/_core/_run.py b/trio/_core/_run.py index e56977f386..807e330c1d 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -2176,6 +2176,10 @@ def unrolled_run(runner, async_fn, args, host_uses_signal_set_wakeup_fd=False): for _ in range(CONTEXT_RUN_TB_FRAMES): tb = tb.tb_next final_outcome = Error(task_exc.with_traceback(tb)) + # Remove local refs so that e.g. cancelled coroutine locals + # are not kept alive by this frame until another exception + # comes along. + del tb if final_outcome is not None: # We can't call this directly inside the except: blocks @@ -2183,6 +2187,10 @@ def unrolled_run(runner, async_fn, args, host_uses_signal_set_wakeup_fd=False): # themselves to other exceptions as __context__ in # unwanted ways. runner.task_exited(task, final_outcome) + # final_outcome may contain a traceback ref. It's not as + # crucial compared to the above, but this will allow more + # prompt release of resources in coroutine locals. + final_outcome = None else: task._schedule_points += 1 if msg is CancelShieldedCheckpoint: @@ -2211,10 +2219,16 @@ def unrolled_run(runner, async_fn, args, host_uses_signal_set_wakeup_fd=False): # which works for at least asyncio and curio. runner.reschedule(task, exc) task._next_send_fn = task.coro.throw + # prevent long-lived reference + # TODO: develop test for this deletion + del msg if "after_task_step" in runner.instruments: runner.instruments.call("after_task_step", task) del GLOBAL_RUN_CONTEXT.task + # prevent long-lived references + # TODO: develop test for these deletions + del task, next_send, next_send_fn except GeneratorExit: # The run-loop generator has been garbage collected without finishing diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index 4c4e12b5df..d2bcdfd740 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -6,6 +6,7 @@ import time import types import warnings +import weakref from contextlib import contextmanager, ExitStack from math import inf from textwrap import dedent @@ -2249,3 +2250,27 @@ async def test_nursery_cancel_doesnt_create_cyclic_garbage(): 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_locals_destroyed_promptly_on_cancel(): + destroyed = False + + def finalizer(): + nonlocal destroyed + destroyed = True + + class A: + pass + + async def task(): + a = A() + weakref.finalize(a, finalizer) + await _core.checkpoint() + + async with _core.open_nursery() as nursery: + nursery.start_soon(task) + nursery.cancel_scope.cancel() + assert destroyed