Skip to content

Commit 83b96a3

Browse files
ThreadCache workers leaking spawner's Context on py314+ free-threaded builds (#3473)
* Fix ThreadCache workers leaking spawner's Context on py314+ free-threaded builds * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix typo * add newsfragments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix typing and reformat newfragments entry * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add a regression test * update newfragments entry * update _thread_cache * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update the test to be skipped on pypi * update the test * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * simply regression test --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 976c11b commit 83b96a3

3 files changed

Lines changed: 36 additions & 2 deletions

File tree

newsfragments/3472.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed ``trio.to_thread.run_sync``` workers leaking spawner's ``Context`` on ``py314t+`` free-threaded builds.

src/trio/_core/_thread_cache.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import sys
77
import traceback
8+
from contextvars import Context
89
from functools import partial
910
from itertools import count
1011
from threading import Lock, Thread
@@ -151,8 +152,17 @@ def __init__(self, thread_cache: ThreadCache) -> None:
151152
self._worker_lock = Lock()
152153
self._worker_lock.acquire()
153154
self._default_name = f"Trio thread {next(name_counter)}"
154-
155-
self._thread = Thread(target=self._work, name=self._default_name, daemon=True)
155+
if sys.version_info >= (3, 14):
156+
self._thread = Thread(
157+
target=self._work,
158+
name=self._default_name,
159+
daemon=True,
160+
context=Context(),
161+
)
162+
else:
163+
self._thread = Thread(
164+
target=self._work, name=self._default_name, daemon=True
165+
)
156166

157167
if set_os_thread_name:
158168
set_os_thread_name(self._thread.ident, self._default_name)

src/trio/_tests/test_threads.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import contextvars
4+
import gc
45
import queue as stdlib_queue
56
import re
67
import sys
@@ -687,6 +688,28 @@ def g() -> tuple[str, str, threading.Thread]:
687688
assert sniffio.current_async_library() == "trio"
688689

689690

691+
async def test_worker_thread_context_not_leaked() -> None:
692+
# Regression test for: https://github.com/python-trio/trio/issues/3472
693+
694+
class Foo:
695+
pass
696+
697+
def sync_fn() -> None:
698+
pass
699+
700+
cvar: contextvars.ContextVar[Foo] = contextvars.ContextVar("cvar")
701+
contextval = Foo()
702+
ref = weakref.ref(contextval)
703+
cvar.set(contextval)
704+
await to_thread_run_sync(sync_fn)
705+
cvar.set(Foo())
706+
707+
del contextval
708+
gc.collect()
709+
710+
assert ref() is None
711+
712+
690713
async def test_trio_from_thread_run_sync() -> None:
691714
# Test that to_thread_run_sync correctly "hands off" the trio token to
692715
# trio.from_thread.run_sync()

0 commit comments

Comments
 (0)