Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
25bfa7f
Fix ThreadCache workers leaking spawner's Context on py314+ free-thre…
EmmanuelNiyonshuti Jul 15, 2026
df6fdaa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 15, 2026
7bfb77b
fix typo
EmmanuelNiyonshuti Jul 15, 2026
b43e448
add newsfragments
EmmanuelNiyonshuti Jul 15, 2026
31aa1b3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 15, 2026
e3881ce
fix typing and reformat newfragments entry
EmmanuelNiyonshuti Jul 15, 2026
9d8f97b
Merge remote-tracking branch 'origin/free-threaded-context-leak-threa…
EmmanuelNiyonshuti Jul 15, 2026
306edad
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 15, 2026
0cb179c
add a regression test
EmmanuelNiyonshuti Jul 15, 2026
f6c01df
update newfragments entry
EmmanuelNiyonshuti Jul 15, 2026
eec08bb
update _thread_cache
EmmanuelNiyonshuti Jul 15, 2026
e232d35
Merge remote-tracking branch 'origin/free-threaded-context-leak-threa…
EmmanuelNiyonshuti Jul 15, 2026
e114fc0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 15, 2026
114eb57
update the test to be skipped on pypi
EmmanuelNiyonshuti Jul 15, 2026
0baa9ac
Merge remote-tracking branch 'origin/free-threaded-context-leak-threa…
EmmanuelNiyonshuti Jul 15, 2026
edf43b6
update the test
EmmanuelNiyonshuti Jul 15, 2026
1c527cf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 15, 2026
72aa082
simply regression test
EmmanuelNiyonshuti Jul 15, 2026
eb8320f
Merge remote-tracking branch 'origin/free-threaded-context-leak-threa…
EmmanuelNiyonshuti Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/3472.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``trio.to_thread.run_sync``` workers leaking spawner's ``Context`` on ``py314t+`` free-threaded builds.
14 changes: 12 additions & 2 deletions src/trio/_core/_thread_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions src/trio/_tests/test_threads.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import contextvars
import gc
import queue as stdlib_queue
import re
import sys
Expand Down Expand Up @@ -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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO you should just check whether context gets carried over, no need for GC or whatever. Technically there could be a bug where we clear it in the frame before handing it to the user but store it elsewhere -- and you could test for that using weakref (to ensure it works on PyPy) -- but IMO not necessary.

@EmmanuelNiyonshuti EmmanuelNiyonshuti Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we could use weakref too. I leaned more towards how anyio did it.

# 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()
Expand Down
Loading