-
-
Notifications
You must be signed in to change notification settings - Fork 30.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cannot cleanly kill a subprocess using high-level asyncio APIs #88050
Comments
There doesn't appear to be a way to prematurely kill a subprocess using the high-level asyncio subprocess APIs (https://docs.python.org/3.9/library/asyncio-subprocess.html) without getting a traceback on exit. On exit, the attached program writes the following to stderr: $ python3.9 kill_subprocess.py Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x1065f0dc0>
Traceback (most recent call last):
...
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed If I uncomment |
Reproducing the program here: import asyncio
async def test():
process = await asyncio.create_subprocess_shell(
"sleep 2 && echo done",
stdout=asyncio.subprocess.PIPE,
)
await asyncio.sleep(1)
process.kill()
await process.wait()
# process._transport.close()
asyncio.run(test()) Can I use the high-level API to kill a subprocess cleanly without having to access the protected member process._transport? Seems like an oversight perhaps? |
Running kill_subprocess.py on Windows 10, I get these results: Python 3.7.2 (tags/v3.7.2:9a3ffc0492) What is your OS? |
I see this on MacOS and Linux, but I suspect any Unix-like system would have the same behavior. |
I'm also experiencing this, with virtually identical code, on macOS 10.15.7. The given fix (process._transport.close()) also works for me, so I'm just using that workaround for the time being. |
I'm still trying to get my head around the issue here. If I replace the shell command in the repro script with |
Here is my investigation which led to the fix as in PR #32073. Without fix:
With my fix things are consistent and there is no race with calling callbacks:
|
…rocess is blocked (pythonGH-32073) (cherry picked from commit 7015e13) Co-authored-by: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com>
Looks like the 3.10 backport has broken tests. @kumaraditya303 can you look into this? If it's too much effort we can just not backport. |
Skipping 3.10 has it has diverged too much from main and not worth investing more time into this, 3.11 and main is fixed. Thanks @gvanrossum for reviews! |
Check for None when iterating over `self._pipes.values()`.
…nGH-97951) Check for None when iterating over `self._pipes.values()`. (cherry picked from commit e2e6b95) Co-authored-by: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com>
* main: (66 commits) pythongh-65961: Raise `DeprecationWarning` when `__package__` differs from `__spec__.parent` (python#97879) docs(typing): add "see PEP 675" to LiteralString (python#97926) pythongh-97850: Remove all known instances of module_repr() (python#97876) I changed my surname early this year (python#96671) pythongh-93738: Documentation C syntax (:c:type:<C type> -> :c:expr:<C type>) (python#97768) pythongh-91539: improve performance of get_proxies_environment (python#91566) build(deps): bump actions/stale from 5 to 6 (python#97701) pythonGH-95172 Make the same version `versionadded` oneline (python#95172) pythongh-88050: Fix asyncio subprocess to kill process cleanly when process is blocked (python#32073) pythongh-93738: Documentation C syntax (Function glob patterns -> literal markup) (python#97774) pythongh-93357: Port test cases to IsolatedAsyncioTestCase, part 2 (python#97896) pythongh-95196: Disable incorrect pickling of the C implemented classmethod descriptors (pythonGH-96383) pythongh-97758: Fix a crash in getpath_joinpath() called without arguments (pythonGH-97759) pythongh-74696: Pass root_dir to custom archivers which support it (pythonGH-94251) pythongh-97661: Improve accuracy of sqlite3.Cursor.fetchone docs (python#97662) pythongh-87092: bring compiler code closer to a preprocessing-opt-assembler organisation (pythonGH-97644) pythonGH-96704: Add {Task,Handle}.get_context(), use it in call_exception_handler() (python#96756) pythongh-93738: Documentation C syntax (:c:type:`PyTypeObject*` -> :c:expr:`PyTypeObject*`) (python#97778) pythongh-97825: fix AttributeError when calling subprocess.check_output(input=None) with encoding or errors args (python#97826) Add re.VERBOSE flag documentation example (python#97678) ...
* main: pythonGH-88050: fix race in closing subprocess pipe in asyncio (python#97951) pythongh-93738: Disallow pre-v3 syntax in the C domain (python#97962) pythongh-95986: Fix the example using match keyword (python#95989) pythongh-97897: Prevent os.mkfifo and os.mknod segfaults with macOS 13 SDK (pythonGH-97944) pythongh-94808: Cover `PyUnicode_Count` in CAPI (python#96929) pythongh-94808: Cover `PyObject_PyBytes` case with custom `__bytes__` method (python#96610) pythongh-95691: Doc BufferedWriter and BufferedReader (python#95703) pythonGH-88968: Add notes about socket ownership transfers (python#97936) pythongh-96865: [Enum] fix Flag to use CONFORM boundary (pythonGH-97528)
…n#97951) Check for None when iterating over `self._pipes.values()`.
* main: (53 commits) pythongh-94808: Coverage: Test that maximum indentation level is handled (python#95926) pythonGH-88050: fix race in closing subprocess pipe in asyncio (python#97951) pythongh-93738: Disallow pre-v3 syntax in the C domain (python#97962) pythongh-95986: Fix the example using match keyword (python#95989) pythongh-97897: Prevent os.mkfifo and os.mknod segfaults with macOS 13 SDK (pythonGH-97944) pythongh-94808: Cover `PyUnicode_Count` in CAPI (python#96929) pythongh-94808: Cover `PyObject_PyBytes` case with custom `__bytes__` method (python#96610) pythongh-95691: Doc BufferedWriter and BufferedReader (python#95703) pythonGH-88968: Add notes about socket ownership transfers (python#97936) pythongh-96865: [Enum] fix Flag to use CONFORM boundary (pythonGH-97528) pythongh-65961: Raise `DeprecationWarning` when `__package__` differs from `__spec__.parent` (python#97879) docs(typing): add "see PEP 675" to LiteralString (python#97926) pythongh-97850: Remove all known instances of module_repr() (python#97876) I changed my surname early this year (python#96671) pythongh-93738: Documentation C syntax (:c:type:<C type> -> :c:expr:<C type>) (python#97768) pythongh-91539: improve performance of get_proxies_environment (python#91566) build(deps): bump actions/stale from 5 to 6 (python#97701) pythonGH-95172 Make the same version `versionadded` oneline (python#95172) pythongh-88050: Fix asyncio subprocess to kill process cleanly when process is blocked (python#32073) pythongh-93738: Documentation C syntax (Function glob patterns -> literal markup) (python#97774) ...
…rocess is blocked (python#32073)
…n#97951) Check for None when iterating over `self._pipes.values()`.
Is this meant to be fixed in 3.11+? |
Yes |
Which patch? Not in 3.11.0 as far as I can tell. |
#32073 and a followup are present in 3.11.1. They may not be present in 3.11.0. |
Hmm. I came here using Google while I'm on 3.11.2 already. This happens sometimes for me on a subprocess wrapped inside a task and stdin/stderr connected. It also only triggers when I am terminating a process myself, instead killing it or letting the loop killing it. Found this when hitting random test failures on 3.11.2 and created the minimal reproducer below. import asyncio
import logging
async def myfunc_inner() -> None:
subprocess_task = asyncio.create_task(
asyncio.create_subprocess_exec(
"sleep",
"10",
# seems to be relevant; unable to trigger without stdout/stderr connected
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
)
proc = await subprocess_task
try:
await asyncio.wait_for(proc.communicate(), timeout=0.02)
except (asyncio.TimeoutError, asyncio.CancelledError):
logging.info("Timeout on subprocess command, terminating.")
# unable to trigger with proc.kill() or without terminating it.
proc.terminate()
return
else:
logging.info(f"process exited with {proc.returncode}]")
# unable to trigger without nested task.
async def myfunc_outer() -> None:
await asyncio.wait_for(myfunc_inner(), timeout=1.0)
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
loop = asyncio.get_event_loop_policy().new_event_loop()
loop.run_until_complete(myfunc_outer())
# This fixes it, but hides the underlying problem I think.
# loop.run_until_complete(asyncio.sleep(0.01))
loop.close() Running this in a shell loop 200 times, this gives ~ 1-10 failures per loop, looks random. $ for i in `seq 1 200`; do python proctimeout.py; done
DEBUG:asyncio:Using selector: EpollSelector
Timeout on subprocess command, terminating.
DEBUG:asyncio:Using selector: EpollSelector
Timeout on subprocess command, terminating.
DEBUG:asyncio:Using selector: EpollSelector
Timeout on subprocess command, terminating.
Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x7f5bff42a2a0>
Traceback (most recent call last):
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/base_subprocess.py", line 126, in __del__
self.close()
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/base_subprocess.py", line 104, in close
proto.pipe.close()
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/unix_events.py", line 558, in close
self._close(None)
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/unix_events.py", line 582, in _close
self._loop.call_soon(self._call_connection_lost, exc)
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/base_events.py", line 761, in call_soon
self._check_closed()
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/base_events.py", line 519, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
DEBUG:asyncio:Using selector: EpollSelector
Timeout on subprocess command, terminating.
WARNING:asyncio:Loop <_UnixSelectorEventLoop running=False closed=True debug=False> that handles pid 419211 is closed
Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x7f7dffc162a0>
Traceback (most recent call last):
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/base_subprocess.py", line 126, in __del__
self.close()
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/base_subprocess.py", line 104, in close
proto.pipe.close()
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/unix_events.py", line 558, in close
self._close(None)
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/unix_events.py", line 582, in _close
self._loop.call_soon(self._call_connection_lost, exc)
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/base_events.py", line 761, in call_soon
self._check_closed()
File "/home/gert/.pyenv/versions/3.11.2/lib/python3.11/asyncio/base_events.py", line 519, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
DEBUG:asyncio:Using selector: EpollSelector
Timeout on subprocess command, terminating.
DEBUG:asyncio:Using selector: EpollSelector
Timeout on subprocess command, terminating.
DEBUG:asyncio:Using selector: EpollSelector
Timeout on subprocess command, terminating.
[...] Either removing Also notice the (unrelated) error:
Further relevant system info: Am I doing something wrong here or hitting a corner case that's not tackled by this bugfix? Thanks! 🙏🏼 |
You are missing |
Are you sure I should? It's also not included in Here’s an example of how asyncio can run a shell command and obtain its result, here: https://docs.python.org/3/library/asyncio-subprocess.html# Note that I'm already awaiting the process result with
Sorry for the noise then, but to me it sounds exactly the same still. |
You are wrapping |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: