Subprocesses behave differently on asyncio and Trio #828
gschaffner
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
I have been doing some work with subprocesses recently and encountered some backend-dependent behavioral differences. I'm opening a discussion thread first rather than issue threads because each of these/the potential fix in AnyIO for each of these is related to at least one of the others, and for some of them it's not clear whether it's worth the effort to do anything about them in AnyIO.
On Trio,
anyio.abc.Process.[send_signal | terminate | kill]
on an exited process is a no-op, but on asyncio it can raiseProcessLookupError
(fromBaseSubprocessTransport._check_proc
). Reproducer:I think AnyIO should match Trio's behavior on asyncio, or if not that document the
ProcessLookupError
.On Trio,
anyio.abc.Process.returncode
polls the process and thus can never returnNone
for an exited process, but on asyncio it does not poll the process and can thus returnNone
for an exited process.Process.returncode
currently saysThat it can also be
None
if the process has terminated may surprise people (and their code), especially if they have assumed that AnyIO has the same semantics as Trio. While AnyIO does not document.returncode
as performing a poll, it also does not document that.returncode
may return a stale value. Compare tosubprocess.Popen.returncode
, which documents that it does not poll and thus can return a stale value, and compare totrio.Process.returncode
, which documents that it polls and thus will not return a stale value.It would be useful if AnyIO matched Trio's behavior on asyncio. It is useful to be able to poll a process;
subprocess.Popen
andtrio.Process
for instance expose API to let one poll.Reproducer:
On asyncio, we can get stuck (cancellation-invulnerable in some cases) waiting for a child process that is already dead, depending on the child's behavior. Reproducer:
In both reproducers, note that our child gets sent
SIGKILL
, but even after it's been force-killed itsProcess.wait
still gets stuck.Note that in both of these reproducers the stuck
wait()
is cancellation-invulnerable, but an unshieldedwait()
can get stuck too.(Also note that in this reproducer, if you
KeyboardInterrupt
the stuckwait()
call, asyncio will typically also hitBaseSubprocessTransport.__del__
fails if the event loop is already closed, which can leak an orphan process python/cpython#114177 and fail to perform the last-chance cleanup it wanted to perform. That is not really relevant though, other than that it may serve to further confuse people who hit this bug.)Note that this is different than #757, because in this case the process is actually dead, whereas in #757 the process is alive but in a pathological state. Also, #757 is for pathological child behavior under both backends, whereas this is for non-pathological cases and works fine on the Trio backend.
This is really an upstream asyncio bug/feature(?): asyncio
proc.kill()
andproc.wait()
are counter intuitive python/cpython#119710. If asyncio deems this an asyncio feature, then I think that this is an AnyIO bug, as the behavior is different on the two backends and the behavior on the asyncio backend violates theanyio.abc.Process.wait
docs. I think that AnyIO should behave in the way that people expect here, i.e. behave in the way that the Trio backend andsubprocess.Popen
both behave.The following two are also backend-dependent behavioral differences, but these ones are caused by upstream asyncio bugs, not bugs in AnyIO itself. Unfortunately they still present to users as AnyIO bugs (i.e. they affect code that doesn't
import asyncio
), so an AnyIO-based library affected significantly by any of these bugs or needingpoll
support ((2)) can find themselves needing to door something similar until it's fixed upstream (2027 or later (3.11 EoL), depending on upstream backports). And even so, having Trio-only API in an otherwise AnyIO-only package can impact users.
Note that if (3) is declared to be an asyncio bug rather than an asyncio feature then (3) would join (4) and (5) in this category.
On asyncio on Python >= 3.13,
anyio.abc.Process.[send_signal | terminate | kill]
can kill an unrelated process on Unix-likes. Trio does not have this issue.Under the hood this is an upstream asyncio bug:
asyncio.subprocess.Process
race condition kills an unrelated process on Unix python/cpython#127049.On asyncio on Python < 3.13,
anyio.abc.Process.[send_signal | terminate | kill]
can causeProcess.returncode
to be incorrect (with a warning) on Unix-likes. Trio does not have this issue.Under the hood this is an upstream asyncio bug: With asyncio subprocess, send_signal() and the child process watcher will both call waitpid() python/cpython#87744.
(This one I have not currently encountered, but I mention it here still as it is currently XOR with (4).)
Which of these are within the scope of AnyIO to address?
Beta Was this translation helpful? Give feedback.
All reactions