Skip to content

Commit 6bf1421

Browse files
committed
Merge branch 'dont-use-f_locals-for-agen-finalization' of github.com:graingert/trio into avoid-refcycles-in-run-exc
2 parents 1e10e62 + c4010fc commit 6bf1421

File tree

3 files changed

+69
-18
lines changed

3 files changed

+69
-18
lines changed

newsfragments/3112.bugfix.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Rework foreign async generator finalization to track async generator
2+
ids rather than mutating ``ag_frame.f_locals``. This fixes an issue
3+
with the previous implementation: locals' lifetimes will no longer be
4+
extended by materialization in the ``ag_frame.f_locals`` dictionary that
5+
the previous finalization dispatcher logic needed to access to do its work.

src/trio/_core/_asyncgens.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class AsyncGenerators:
3535
# regular set so we don't have to deal with GC firing at
3636
# unexpected times.
3737
alive: _WEAK_ASYNC_GEN_SET | _ASYNC_GEN_SET = attrs.Factory(_WEAK_ASYNC_GEN_SET)
38+
# The ids of foreign async generators are added to this set when first
39+
# iterated. Usually it is not safe to refer to ids like this, but because
40+
# we're using a finalizer we can ensure ids in this set do not outlive
41+
# their async generator.
42+
foreign: set[int] = attrs.Factory(set)
3843

3944
# This collects async generators that get garbage collected during
4045
# the one-tick window between the system nursery closing and the
@@ -51,10 +56,7 @@ def firstiter(agen: AsyncGeneratorType[object, NoReturn]) -> None:
5156
# An async generator first iterated outside of a Trio
5257
# task doesn't belong to Trio. Probably we're in guest
5358
# mode and the async generator belongs to our host.
54-
# The locals dictionary is the only good place to
55-
# remember this fact, at least until
56-
# https://bugs.python.org/issue40916 is implemented.
57-
agen.ag_frame.f_locals["@trio_foreign_asyncgen"] = True
59+
self.foreign.add(id(agen))
5860
if self.prev_hooks.firstiter is not None:
5961
self.prev_hooks.firstiter(agen)
6062

@@ -77,12 +79,14 @@ def finalize_in_trio_context(
7779
self.trailing_needs_finalize.add(agen)
7880

7981
def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None:
80-
agen_name = name_asyncgen(agen)
8182
try:
82-
is_ours = not agen.ag_frame.f_locals.get("@trio_foreign_asyncgen")
83-
except AttributeError: # pragma: no cover
83+
self.foreign.remove(id(agen))
84+
except KeyError:
8485
is_ours = True
86+
else:
87+
is_ours = False
8588

89+
agen_name = name_asyncgen(agen)
8690
if is_ours:
8791
runner.entry_queue.run_sync_soon(
8892
finalize_in_trio_context,

src/trio/_core/_tests/test_guest_mode.py

+53-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import asyncio
44
import contextlib
5-
import contextvars
65
import queue
76
import signal
87
import socket
@@ -11,6 +10,7 @@
1110
import time
1211
import traceback
1312
import warnings
13+
import weakref
1414
from collections.abc import AsyncGenerator, Awaitable, Callable
1515
from functools import partial
1616
from math import inf
@@ -22,6 +22,7 @@
2222
)
2323

2424
import pytest
25+
import sniffio
2526
from outcome import Outcome
2627

2728
import trio
@@ -221,7 +222,8 @@ async def trio_main(in_host: InHost) -> str:
221222

222223

223224
def test_guest_mode_sniffio_integration() -> None:
224-
from sniffio import current_async_library, thread_local as sniffio_library
225+
current_async_library = sniffio.current_async_library
226+
sniffio_library = sniffio.thread_local
225227

226228
async def trio_main(in_host: InHost) -> str:
227229
async def synchronize() -> None:
@@ -439,9 +441,9 @@ def aiotrio_run(
439441
loop = asyncio.new_event_loop()
440442

441443
async def aio_main() -> T:
442-
trio_done_fut = loop.create_future()
444+
trio_done_fut: asyncio.Future[Outcome[T]] = loop.create_future()
443445

444-
def trio_done_callback(main_outcome: Outcome[object]) -> None:
446+
def trio_done_callback(main_outcome: Outcome[T]) -> None:
445447
print(f"trio_fn finished: {main_outcome!r}")
446448
trio_done_fut.set_result(main_outcome)
447449

@@ -455,9 +457,11 @@ def trio_done_callback(main_outcome: Outcome[object]) -> None:
455457
**start_guest_run_kwargs,
456458
)
457459

458-
return (await trio_done_fut).unwrap() # type: ignore[no-any-return]
460+
return (await trio_done_fut).unwrap()
459461

460462
try:
463+
# can't use asyncio.run because that fails on Windows (3.8, x64, with
464+
# Komodia LSP)
461465
return loop.run_until_complete(aio_main())
462466
finally:
463467
loop.close()
@@ -628,8 +632,6 @@ async def trio_main(in_host: InHost) -> None:
628632

629633
@restore_unraisablehook()
630634
def test_guest_mode_asyncgens() -> None:
631-
import sniffio
632-
633635
record = set()
634636

635637
async def agen(label: str) -> AsyncGenerator[int, None]:
@@ -656,9 +658,49 @@ async def trio_main() -> None:
656658

657659
gc_collect_harder()
658660

659-
# Ensure we don't pollute the thread-level context if run under
660-
# an asyncio without contextvars support (3.6)
661-
context = contextvars.copy_context()
662-
context.run(aiotrio_run, trio_main, host_uses_signal_set_wakeup_fd=True)
661+
aiotrio_run(trio_main, host_uses_signal_set_wakeup_fd=True)
663662

664663
assert record == {("asyncio", "asyncio"), ("trio", "trio")}
664+
665+
666+
@restore_unraisablehook()
667+
def test_guest_mode_asyncgens_garbage_collection() -> None:
668+
record: set[tuple[str, str, bool]] = set()
669+
670+
async def agen(label: str) -> AsyncGenerator[int, None]:
671+
class A:
672+
pass
673+
674+
a = A()
675+
a_wr = weakref.ref(a)
676+
assert sniffio.current_async_library() == label
677+
try:
678+
yield 1
679+
finally:
680+
library = sniffio.current_async_library()
681+
with contextlib.suppress(trio.Cancelled):
682+
await sys.modules[library].sleep(0)
683+
684+
del a
685+
if sys.implementation.name == "pypy":
686+
gc_collect_harder()
687+
688+
record.add((label, library, a_wr() is None))
689+
690+
async def iterate_in_aio() -> None:
691+
await agen("asyncio").asend(None)
692+
693+
async def trio_main() -> None:
694+
task = asyncio.ensure_future(iterate_in_aio())
695+
done_evt = trio.Event()
696+
task.add_done_callback(lambda _: done_evt.set())
697+
with trio.fail_after(1):
698+
await done_evt.wait()
699+
700+
await agen("trio").asend(None)
701+
702+
gc_collect_harder()
703+
704+
aiotrio_run(trio_main, host_uses_signal_set_wakeup_fd=True)
705+
706+
assert record == {("asyncio", "asyncio", True), ("trio", "trio", True)}

0 commit comments

Comments
 (0)