diff --git a/newsfragments/3472.bugfix.rst b/newsfragments/3472.bugfix.rst new file mode 100644 index 0000000000..a3803690e1 --- /dev/null +++ b/newsfragments/3472.bugfix.rst @@ -0,0 +1 @@ +Fixed ``trio.to_thread.run_sync``` workers leaking spawner's ``Context`` on ``py314t+`` free-threaded builds. diff --git a/src/trio/_core/_thread_cache.py b/src/trio/_core/_thread_cache.py index c2b2315bd3..f48aa8eab4 100644 --- a/src/trio/_core/_thread_cache.py +++ b/src/trio/_core/_thread_cache.py @@ -5,6 +5,7 @@ import os import sys import traceback +from contextvars import Context from functools import partial from itertools import count from threading import Lock, Thread @@ -151,8 +152,17 @@ def __init__(self, thread_cache: ThreadCache) -> None: self._worker_lock = Lock() self._worker_lock.acquire() self._default_name = f"Trio thread {next(name_counter)}" - - self._thread = Thread(target=self._work, name=self._default_name, daemon=True) + if sys.version_info >= (3, 14): + self._thread = Thread( + target=self._work, + name=self._default_name, + daemon=True, + context=Context(), + ) + else: + self._thread = Thread( + target=self._work, name=self._default_name, daemon=True + ) if set_os_thread_name: set_os_thread_name(self._thread.ident, self._default_name) diff --git a/src/trio/_tests/test_threads.py b/src/trio/_tests/test_threads.py index 0ad1163011..532a20d36f 100644 --- a/src/trio/_tests/test_threads.py +++ b/src/trio/_tests/test_threads.py @@ -1,6 +1,7 @@ from __future__ import annotations import contextvars +import gc import queue as stdlib_queue import re import sys @@ -687,6 +688,28 @@ def g() -> tuple[str, str, threading.Thread]: assert sniffio.current_async_library() == "trio" +async def test_worker_thread_context_not_leaked() -> None: + # Regression test for: https://github.com/python-trio/trio/issues/3472 + + class Foo: + pass + + def sync_fn() -> None: + pass + + cvar: contextvars.ContextVar[Foo] = contextvars.ContextVar("cvar") + contextval = Foo() + ref = weakref.ref(contextval) + cvar.set(contextval) + await to_thread_run_sync(sync_fn) + cvar.set(Foo()) + + del contextval + gc.collect() + + assert ref() is None + + async def test_trio_from_thread_run_sync() -> None: # Test that to_thread_run_sync correctly "hands off" the trio token to # trio.from_thread.run_sync()