trio icon indicating copy to clipboard operation
trio copied to clipboard

run_process ignores thread context and spawns from threadpool

Open tik-stbuehler opened this issue 1 month ago • 2 comments

Hi,

run_process uses trio.to_thread.run_sync to spawn the subprocess; this means that the spawned process will inherit various context from a different thread. In my case this means it spawns in the wrong (network) namespace (see setns(2) - "reassociate thread with a namespace").

This might affect other things as well (pthreads(7) lists (among other things) capabilities and CPU affinity).

  1. the docs should mention this behavior
  2. it would be nice to have a flag to "spawn from current thread"

I'm also open to good suggestions how to work around this problem :) (I might try prefixing the target command with ["/usr/bin/nsenter", "--target", threading.get_native_id(), "-n", "--"], or copy the trio run_process/open_process functions).

cheers, Stefan

tik-stbuehler avatar Nov 26 '25 16:11 tik-stbuehler

Looking at the docs, it sounds like we would want to use clone(2) to get a thread for sync stuff?

Not all of the attributes that can be shared when a new thread is created using clone(2) can be changed using setns().

Probably also the thread pool has some capacity limiter (I forgot!) so any change would have to beware... but maybe we can just call clone(2) if it's available to make a thread? I haven't looked at its manpage.

(Disclaimer: I don't know my way around the syscalls available)

A5rocks avatar Nov 26 '25 17:11 A5rocks

Looking at the docs, it sounds like we would want to use clone(2) to get a thread for sync stuff?

I strongly recommend against (even temporarily) cloning a new thread for every subprocess, and that would be the only way to make sure the subprocess inherits the current context.

Actually, in my case a worker thread(pool) per "normal thread"/"trio loop" would do, because changing the namespace is the first thing I do, i.e. before calling trio.run; but I have multiple threads doing so with different namespaces. See code example at the bottom.

Not all of the attributes that can be shared when a new thread is created using clone(2) can be changed using setns().

No; I think this just means "not every CLONE_* flag is a namespace flag" - i.e. not every CLONE_* flag can be used with setns() (but I think all CLONE_NEW* flags actually can be used).

Probably also the thread pool has some capacity limiter (I forgot!) so any change would have to beware... but maybe we can just call clone(2) if it's available to make a thread? I haven't looked at its manpage.

Any high-level capacity limiter of a thread pool is unrelated to clone(2) and potential OS resource limits. But if you hit such an OS limit creating a new process is very likely to fail too.


Attaching possible workaround for using a (thread-)local threadpool where needed (call use_local_thread_pool at least once in such threads):

from __future__ import annotations

from trio._core._thread_cache import ThreadCache, THREAD_CACHE as ORIG_CACHE
import trio._core._thread_cache
import threading
import typing

if typing.TYPE_CHECKING:
    import outcome


_THREAD_LOCALS = threading.local()


def use_local_thread_pool() -> None:
    # activate workaround for https://github.com/python-trio/trio/issues/3360
    if not hasattr(_THREAD_LOCALS, "pool"):
        _THREAD_LOCALS.pool = ThreadCache()


class _StubCache:
    _idle_workers = ORIG_CACHE._idle_workers

    def start_thread_soon(
        self,
        fn: typing.Callable[[], trio._core._thread_cache.RetT],
        deliver: typing.Callable[[outcome.Outcome[trio._core._thread_cache.RetT]], object],
        name: str | None = None,
    ) -> None:
        if hasattr(_THREAD_LOCALS, "pool"):
            _THREAD_LOCALS.pool.start_thread_soon(fn, deliver, name)
        else:
            ORIG_CACHE.start_thread_soon(fn, deliver, name)


trio._core._thread_cache.THREAD_CACHE = _StubCache()  # type: ignore

tik-stbuehler avatar Nov 27 '25 08:11 tik-stbuehler