-
-
Notifications
You must be signed in to change notification settings - Fork 31.3k
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
Handle KeyboardInterrupt in asyncio #83803
Comments
As shown in the code, when 0 is passed to asyncio.sleep function, sometimes the program does not exit when I press <Ctrl-C>. I must press <Ctrl-C> again to close the program. However, when a number, such as 0.01, which is bigger than 0, is passed to the sleep function, the program will exit as expected when I press <Ctrl-C> just once. Is there any bug or just a wrong way to use? Thanks. |
On which OS did that happen? |
Ubuntu 18.04.4 with kernel 5.3.0-28-generic. Additionally Python version is 3.7.3. |
This behaved similarly on my machine, also ubuntu. But it also happened (less often) with small numbers, like sleep(0.0000000000000000000001). Also, I tried it on my windows 10 and experienced another unexpected behavior - only when using sleep(0), Ctrl-C would not work *at all* from time to time. Most of the times it would stop the program immediately. This also happened when using 0.0000000000000000000001 instead of 0. Unless 0.0000000000000000000001 turns into 0 somewhere along the way, I'd suspect that there is a piece of code inside asyncio's core that doesn't handle interrupting correctly (both on linux and windows), and using sleep with small numbers just makes it much more probable for this piece of code to be running when the Ctrl-C signal is sent. I'll try to dig in more. |
I wrote a small script that checks the behavior of asyncio.sleep when called with small amounts of time (starting from 0). For each amount I checked 500 times whether SleepTest has stopped when interrupted with SIGINT. Below are the results: SLEEP TIME SIGINT FAILURE PERCENT It is worth mentioning that the failure amount increased when my CPU was busier. Maybe it is because of false positives in my script (although I think I used relatively generous timeouts when waiting for the ioloop to start running and for the process to die after a SIGINT) and maybe it is because of the nature of the bug in asyncio. I didn't put a lot of effort into distinguishing between the two, as increasing the timeouts also made my CPU less busy. Anyway, there is definitely a weird behavior here, and I still think there is a specific piece of code in asyncio that's causing it. Maybe I'll look at the code a bit, or try to think of other ways to approach this. |
Thank you for your investigation, I will also try to see what went wrong. |
After digging into asyncio, I stumbled upon this particularly suspicious block in BaseEventLoop._run_once: https://github.com/python/cpython/blob/v3.9.0a3/Lib/asyncio/base_events.py#L1873 handle = self._ready.popleft()
if handle._cancelled:
continue
if self._debug:
...
handle._run()
...
else:
handle._run() As you can see, a callback is popped from the dequeue of ready callbacks, and only after a couple of lines that callback is called. The question arises, what happens if an exception is raised in between? Or more specifically, What happens to that callback if a KeyboardInterrupt is raised before it is called? This is how the bug we've been experiencing came to life: I see two possible solutions:
I find the second solution much easier to implement well, and I think it makes more sense. I think python's default SIGINT handler fits normal single-threaded applications very well, but not so much an event loop. When using an event loop it makes sense to handle a signal as an event an process it along with the other running tasks. This is fully supported by the ioloop with the help of signal.set_wakeup_fd. |
After opening the PR, I see that add_signal_handler is not supported on non-unix event loop. After looking at the code, it seems there is no problem to implement it for other platforms, as it relies on signal.set_wakeup_fd() which seems to support all platforms. Should I open an enhancement issue for implementing add_signal_handler for all platforms? |
The second solution doesn't help with breaking infinite loops like We need a more robust idea. |
Yes, I think you are right. In the second solution, the callback of SIGINT will be append to the end of one task loop. If that code is defined in a task by some user, KeyboardInterrupt will not be raised. |
Damn, good catch. How about the following idea - register a normal signal handler (using signal.signal) that does something like: def sigint_handler():
get_running_loop().interrupt()
# in class BaseEventLoop
def interrupt(self):
# Might be a generally useful thread-safe way to interrupt a loop
if self._is_inside_callback():
_thread.interrupt_main() # All this behavior is only relevant to the main thread anyway
else:
self._interrupted = True And inside BaseEventLoop._run_once() add the following check: # in class BaseEventLoop
def _check_interrupted(self):
# This will be called by BaseEventLoop._run_once() before calling select,
# and before popping any handle from the ready queue
if self._interrupted:
raise KeyboardInterrupt |
While looking into this proposal, I realized that this will not wake up the loop from the select call. I think the safest solution would be to use the wakeup fd mechanism. An additional easy, but less secure solution would be to define an internal 'signal safe' context manager that will be used only in the critical parts. What do you think? |
I have pushed an update to my PR. While implementing the new solution I made the following list: Examples for code segments where KeyboardInterrupt must not be raised:
Code segments where KeyboardInterrupt must be raised:
I think that while the loop is running, the signal handler should not raise a KeyboardInterrupt by default, as there are at least 3 unsafe code segments, and more might be added in the future. Currently, the only two segments that have to be directly interrupted by a SIGINT are the ones listed above, and I think this behavior should be allowed explicitly. |
Hey there, it's been more than a week since I pushed the last changes, can anyone review them? |
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: apache#19260
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: #19260
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: #19260 (cherry picked from commit 6bdbed6)
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: #19260 (cherry picked from commit 6bdbed6)
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: #19260 (cherry picked from commit 6bdbed6c43df3c5473b168a75c50e0139cc13e88) GitOrigin-RevId: a0a2ddf60da6bb65865fd2bdc2260f342ac11c7d
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: #19260 (cherry picked from commit 6bdbed6c43df3c5473b168a75c50e0139cc13e88) GitOrigin-RevId: a0a2ddf60da6bb65865fd2bdc2260f342ac11c7d
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: #19260 GitOrigin-RevId: 6bdbed6c43df3c5473b168a75c50e0139cc13e88
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: #19260 (cherry picked from commit 6bdbed6c43df3c5473b168a75c50e0139cc13e88) GitOrigin-RevId: a0a2ddf60da6bb65865fd2bdc2260f342ac11c7d
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: #19260 GitOrigin-RevId: 6bdbed6c43df3c5473b168a75c50e0139cc13e88
There is a bug in CPython (fixed in March 2022 but not yet released) that makes async.io handle SIGTERM improperly by using async unsafe functions and hanging the triggerer receive SIGPIPE while handling SIGTERN/SIGINT and deadlocking itself. Until the bug is handled we should rather rely on standard handling of the signals rather than adding our own signal handlers. Seems that even if our signal handler just run exit(0) - it caused a race condition that led to the hanging. More details: * https://bugs.python.org/issue39622 * python/cpython#83803 Fixes: #19260 GitOrigin-RevId: 6bdbed6c43df3c5473b168a75c50e0139cc13e88
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: