Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 34e7a2e

Browse files
authored
Merge pull request #3112 from graingert/dont-use-f_locals-for-agen-finalization
2 parents 7334c42 + ca6e01c commit 34e7a2e

File tree

3 files changed

+94
-21
lines changed

3 files changed

+94
-21
lines changed

newsfragments/3112.bugfix.rst

Lines changed: 5 additions & 0 deletions
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

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import warnings
66
import weakref
7-
from typing import TYPE_CHECKING, NoReturn
7+
from typing import TYPE_CHECKING, NoReturn, TypeVar
88

99
import attrs
1010

@@ -16,14 +16,31 @@
1616
ASYNCGEN_LOGGER = logging.getLogger("trio.async_generator_errors")
1717

1818
if TYPE_CHECKING:
19+
from collections.abc import Callable
1920
from types import AsyncGeneratorType
2021

22+
from typing_extensions import ParamSpec
23+
24+
_P = ParamSpec("_P")
25+
2126
_WEAK_ASYNC_GEN_SET = weakref.WeakSet[AsyncGeneratorType[object, NoReturn]]
2227
_ASYNC_GEN_SET = set[AsyncGeneratorType[object, NoReturn]]
2328
else:
2429
_WEAK_ASYNC_GEN_SET = weakref.WeakSet
2530
_ASYNC_GEN_SET = set
2631

32+
_R = TypeVar("_R")
33+
34+
35+
@_core.disable_ki_protection
36+
def _call_without_ki_protection(
37+
f: Callable[_P, _R],
38+
/,
39+
*args: _P.args,
40+
**kwargs: _P.kwargs,
41+
) -> _R:
42+
return f(*args, **kwargs)
43+
2744

2845
@attrs.define(eq=False)
2946
class AsyncGenerators:
@@ -35,6 +52,11 @@ class AsyncGenerators:
3552
# regular set so we don't have to deal with GC firing at
3653
# unexpected times.
3754
alive: _WEAK_ASYNC_GEN_SET | _ASYNC_GEN_SET = attrs.Factory(_WEAK_ASYNC_GEN_SET)
55+
# The ids of foreign async generators are added to this set when first
56+
# iterated. Usually it is not safe to refer to ids like this, but because
57+
# we're using a finalizer we can ensure ids in this set do not outlive
58+
# their async generator.
59+
foreign: set[int] = attrs.Factory(set)
3860

3961
# This collects async generators that get garbage collected during
4062
# the one-tick window between the system nursery closing and the
@@ -51,10 +73,10 @@ def firstiter(agen: AsyncGeneratorType[object, NoReturn]) -> None:
5173
# An async generator first iterated outside of a Trio
5274
# task doesn't belong to Trio. Probably we're in guest
5375
# mode and the async generator belongs to our host.
54-
# The locals dictionary is the only good place to
76+
# A strong set of ids is one of the only good places to
5577
# remember this fact, at least until
56-
# https://bugs.python.org/issue40916 is implemented.
57-
agen.ag_frame.f_locals["@trio_foreign_asyncgen"] = True
78+
# https://github.com/python/cpython/issues/85093 is implemented.
79+
self.foreign.add(id(agen))
5880
if self.prev_hooks.firstiter is not None:
5981
self.prev_hooks.firstiter(agen)
6082

@@ -76,13 +98,16 @@ def finalize_in_trio_context(
7698
# have hit it.
7799
self.trailing_needs_finalize.add(agen)
78100

101+
@_core.enable_ki_protection
79102
def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None:
80-
agen_name = name_asyncgen(agen)
81103
try:
82-
is_ours = not agen.ag_frame.f_locals.get("@trio_foreign_asyncgen")
83-
except AttributeError: # pragma: no cover
104+
self.foreign.remove(id(agen))
105+
except KeyError:
84106
is_ours = True
107+
else:
108+
is_ours = False
85109

110+
agen_name = name_asyncgen(agen)
86111
if is_ours:
87112
runner.entry_queue.run_sync_soon(
88113
finalize_in_trio_context,
@@ -105,8 +130,9 @@ def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None:
105130
)
106131
else:
107132
# Not ours -> forward to the host loop's async generator finalizer
108-
if self.prev_hooks.finalizer is not None:
109-
self.prev_hooks.finalizer(agen)
133+
finalizer = self.prev_hooks.finalizer
134+
if finalizer is not None:
135+
_call_without_ki_protection(finalizer, agen)
110136
else:
111137
# Host has no finalizer. Reimplement the default
112138
# Python behavior with no hooks installed: throw in
@@ -116,7 +142,7 @@ def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None:
116142
try:
117143
# If the next thing is a yield, this will raise RuntimeError
118144
# which we allow to propagate
119-
closer.send(None)
145+
_call_without_ki_protection(closer.send, None)
120146
except StopIteration:
121147
pass
122148
else:

src/trio/_core/_tests/test_guest_mode.py

Lines changed: 53 additions & 11 deletions
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, Sequence
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
@@ -234,7 +235,8 @@ async def trio_main(in_host: InHost) -> str:
234235

235236

236237
def test_guest_mode_sniffio_integration() -> None:
237-
from sniffio import current_async_library, thread_local as sniffio_library
238+
current_async_library = sniffio.current_async_library
239+
sniffio_library = sniffio.thread_local
238240

239241
async def trio_main(in_host: InHost) -> str:
240242
async def synchronize() -> None:
@@ -458,9 +460,9 @@ def aiotrio_run(
458460

459461
async def aio_main() -> T:
460462
nonlocal run_sync_soon_not_threadsafe
461-
trio_done_fut = loop.create_future()
463+
trio_done_fut: asyncio.Future[Outcome[T]] = loop.create_future()
462464

463-
def trio_done_callback(main_outcome: Outcome[object]) -> None:
465+
def trio_done_callback(main_outcome: Outcome[T]) -> None:
464466
print(f"trio_fn finished: {main_outcome!r}")
465467
trio_done_fut.set_result(main_outcome)
466468

@@ -479,9 +481,11 @@ def trio_done_callback(main_outcome: Outcome[object]) -> None:
479481
strict_exception_groups=strict_exception_groups,
480482
)
481483

482-
return (await trio_done_fut).unwrap() # type: ignore[no-any-return]
484+
return (await trio_done_fut).unwrap()
483485

484486
try:
487+
# can't use asyncio.run because that fails on Windows (3.8, x64, with
488+
# Komodia LSP) and segfaults on Windows (3.9, x64, with Komodia LSP)
485489
return loop.run_until_complete(aio_main())
486490
finally:
487491
loop.close()
@@ -655,8 +659,6 @@ async def trio_main(in_host: InHost) -> None:
655659

656660
@restore_unraisablehook()
657661
def test_guest_mode_asyncgens() -> None:
658-
import sniffio
659-
660662
record = set()
661663

662664
async def agen(label: str) -> AsyncGenerator[int, None]:
@@ -683,9 +685,49 @@ async def trio_main() -> None:
683685

684686
gc_collect_harder()
685687

686-
# Ensure we don't pollute the thread-level context if run under
687-
# an asyncio without contextvars support (3.6)
688-
context = contextvars.copy_context()
689-
context.run(aiotrio_run, trio_main, host_uses_signal_set_wakeup_fd=True)
688+
aiotrio_run(trio_main, host_uses_signal_set_wakeup_fd=True)
690689

691690
assert record == {("asyncio", "asyncio"), ("trio", "trio")}
691+
692+
693+
@restore_unraisablehook()
694+
def test_guest_mode_asyncgens_garbage_collection() -> None:
695+
record: set[tuple[str, str, bool]] = set()
696+
697+
async def agen(label: str) -> AsyncGenerator[int, None]:
698+
class A:
699+
pass
700+
701+
a = A()
702+
a_wr = weakref.ref(a)
703+
assert sniffio.current_async_library() == label
704+
try:
705+
yield 1
706+
finally:
707+
library = sniffio.current_async_library()
708+
with contextlib.suppress(trio.Cancelled):
709+
await sys.modules[library].sleep(0)
710+
711+
del a
712+
if sys.implementation.name == "pypy":
713+
gc_collect_harder()
714+
715+
record.add((label, library, a_wr() is None))
716+
717+
async def iterate_in_aio() -> None:
718+
await agen("asyncio").asend(None)
719+
720+
async def trio_main() -> None:
721+
task = asyncio.ensure_future(iterate_in_aio())
722+
done_evt = trio.Event()
723+
task.add_done_callback(lambda _: done_evt.set())
724+
with trio.fail_after(1):
725+
await done_evt.wait()
726+
727+
await agen("trio").asend(None)
728+
729+
gc_collect_harder()
730+
731+
aiotrio_run(trio_main, host_uses_signal_set_wakeup_fd=True)
732+
733+
assert record == {("asyncio", "asyncio", True), ("trio", "trio", True)}

0 commit comments

Comments
 (0)