-
-
Notifications
You must be signed in to change notification settings - Fork 31k
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
asyncio.create_subprocess_exec does not respond properly to asyncio.CancelledError #103847
Comments
Curiously this doesn't seem to affect Python 3.6.8 (running on 3.6 requires a change to the Instead of an indefinite hang, a message is printed akin to |
That looks like a nasty problem. Do you want to help by checking the logic of asyncio signals and subprocess creation? There might even be a simple fix. |
I don't currently have the bandwidth personally or professionally at the moment unfortunately. :( I took a quick look through the |
I’m in a similar situation, so we‘ll have to leave this open for a while. |
Copying my analysis from: #125502 (comment) the problem occurs when asyncio.runners._cancel_all_tasks is run at an inopportune instant when connecting pipes: This task gets cancelled: cpython/Lib/asyncio/base_subprocess.py Line 56 in 92af191
which means self._pending_calls is never run: cpython/Lib/asyncio/base_subprocess.py Lines 199 to 202 in 92af191
so when _try_finish appends self._call_connection_lost to self._pending_calls: cpython/Lib/asyncio/base_subprocess.py Line 257 in 92af191
call_connection_lost is never called, which means self._exit_waiters are never woken: cpython/Lib/asyncio/base_subprocess.py Lines 259 to 270 in 92af191
Here's a demo that hangs every time for me: import sys
import inspect
import asyncio
from subprocess import PIPE
async def run_sleep():
proc = await asyncio.create_subprocess_exec(
"sleep",
"0.002",
stdout=PIPE,
)
await proc.communicate()
async def amain():
loop = asyncio.get_running_loop()
task = asyncio.current_task(loop)
coro = task.get_coro()
called_cancel = False
def cancel_eventually():
my_coro = coro
while inspect.iscoroutine(my_coro.cr_await):
my_coro = my_coro.cr_await
if my_coro.cr_code is loop._make_subprocess_transport.__code__:
print("_cancel_all_tasks")
tasks = asyncio.all_tasks()
for task in tasks:
task.cancel()
else:
loop.call_soon(cancel_eventually)
loop.call_soon(cancel_eventually)
await run_sleep()
def main():
asyncio.run(amain())
if __name__ == "__main__":
sys.exit(main()) |
@kumaraditya303 I think the fix is to use: self._waiter = waiter
self._task = task = self._loop.create_task(self._connect_pipes())
task.add_done_callback(self._wake_waiter_and_call_pending_calls_or_close) Then in I've spent a little time fiddling around but can't get the tests to pass and not spray a bunch of errors! |
@graingert Happy to take a look at this if no one else has started! |
I think the same although I don't think it's that simple, we possibly need to rework and audit whole cancelation of subprocess, I am pretty sure it is broken at more places than this. |
I'm not so sure. This might fix the problem now, but IMO lead to code that's hard to maintain. If, say, In general, I think that separating logic between async/await code and done callbacks is a big antipattern. Instead I'd think about shielding the connect_pipes/init code from cancellation, or handling it explicitly. |
Bug report
asyncio programs that call
proc = await asyncio.create_subprocess_exec
but do not reach the call toawait proc.communicate
are not properly cancelled.This can be observed in the following script (it may take a few runs to observe):
When the signal handler cancels the tasks, any task that hasn't made it to
await proc.communicate()
will never complete.A subsequent SIGTERM to the script can then actually terminate the task; however, I'd expect the first call to
cancel()
to disrupt the coroutine.Your environment
The text was updated successfully, but these errors were encountered: