Files
hermes-agent/tools/daemon_pool.py
ethernet b48cc47a4c fix(tools): use Python 3.14 daemon worker contexts
Python 3.14 moves initializer state into a worker context. The old worker arguments fail on the first submission to a fresh executor.

Use the current stdlib worker signature while preserving daemon shutdown and per-submission profile isolation. Cover worker initialization and context isolation on reused threads.

Verified on Python 3.14.7: 41 focused tests pass through scripts/run_tests.sh. Ruff and diff checks pass. Packaged release verification remains pending.
2026-09-08 17:04:44 -04:00

55 lines
2.2 KiB
Python

"""Shared daemon-thread ThreadPoolExecutor.
Stdlib workers are non-daemon AND registered in ``_threads_queues``, whose atexit
hook joins every worker even after ``shutdown(wait=False)`` — one wedged worker
(tool blocked on network I/O, hung provider, stuck subagent) blocks interpreter
exit forever. This variant spawns daemon workers and skips that registration.
Use it for best-effort/interruptible work that must never hold the process open;
NOT for work that must complete before exit (durable writes belong on foreground
threads with explicit bounded joins).
"""
from __future__ import annotations
import threading
import weakref
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures.thread import _worker
from contextvars import copy_context
__all__ = ["DaemonThreadPoolExecutor"]
class DaemonThreadPoolExecutor(ThreadPoolExecutor):
"""ThreadPoolExecutor variant whose workers do not block process exit."""
def submit(self, fn, /, *args, **kwargs):
"""Keep each task in its caller's profile scope, even on a reused worker.
Thread-start context cannot track later submissions from other profiles
(#54937). The stdlib worker context manages initialization, not contextvars.
"""
ctx = copy_context()
def _run_with_context(*call_args, **call_kwargs):
return ctx.run(fn, *call_args, **call_kwargs)
return super().submit(_run_with_context, *args, **kwargs)
def _adjust_thread_count(self) -> None:
# Match CPython 3.14 worker startup, but keep abandoned work out of the
# interpreter's atexit joins: daemon=True and no _threads_queues entry.
if self._idle_semaphore.acquire(timeout=0):
return
def weakref_cb(_, q=self._work_queue):
q.put(None)
num_threads = len(self._threads)
if num_threads < self._max_workers:
thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads)
t = threading.Thread(
name=thread_name, target=_worker, daemon=True,
args=(weakref.ref(self, weakref_cb), self._create_worker_context(), self._work_queue),
)
t.start()
self._threads.add(t)