You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Should our strategy for handling KeyboardInterrupt be usable for other signals too? #134
The Python default is that SIGINT does a clean(ish) shutdown, and SIGTERM blows away the process. I'm not really sure why that makes sense, but that's how it works, and ATM trio copies that. The it-just-works-like-Python factor is nice.
One alternative someone might conceivably want would be to raise an exception in a similar way to what SIGINT does by default (possibly even KeyboardInterrupt, though that's a bit dirty since SIGTERM has nothing to do with the keyboard! It doesn't even have "interrupt" in the name...). In regular Python this is just a matter of installing a one-line signal handler. In trio it's... currently impossible. But I don't think it would be too hard to convert the current SIGINT machinery to a generic "deliver signal as an exception" machinery, so you could do, like, signal.signal(signal.SIGINT, trio.raising_signal_handler(KeyboardInterrupt)). Is this what people really want in production though? Maybe if combined with the restrict_keyboard_interrupt_to_checkpoints option... which then becomes somewhat misnamed. But if that's the only combination that makes sense, then maybe it shouldn't require multiple obscure switches to enable.
...there's also an argument that trio's default behavior should be to raise trio.TerminatedError or something on SIGTERM, which would be Another Weird Thing you have to learn when switching from regular python to trio, but would simplify this case to just setting the restrict_signal_exceptions_to_checkpoints option.
A potential downside would be if people expect SIGTERM to reliably kill wedged and looping processes, after SIGINT fails. I'm not sure how true this is: SIGINT/SIGTERM vs SIGKILL makes more sense to me as a distinction than SIGINT vs SIGTERM/SIGKILL.
Another idea that might be worth throwing into the soup is one that I thought a bit about before implementing trio's current SIGINT system, and someone on reddit reminded me of: having the Python-level handler keep track of how often it's been called, and if it gets called more than some threshold, switch to just killing the process (because it appears to be wedged), maybe after printing a message. This is probably only really useful for SIGINT though, since SIGTERM isn't generally used interactively...
I would be interested in seeing the equivalent for Windows as well, though admittedly I have next to no recollection how Windows notifies running processes of imminent death, so it may very well be more trouble than it's worth. But that's something missing from hypergolix, which may eventually transition over to Trio for reliability reasons.
Ah, Windows is an interesting point. AFAICT Windows doesn't have any equivalent of catching SIGTERM; for ordinary process termination there's just TerminateProcess, and that's the equivalent of SIGKILL. But, they do have "services", which are the equivalent of Unix daemons, and the service API does have a way for a "service manager" (their equivalent or runit or supervisord or whatever) to send a request to a service to exit cleanly.
So this is making me think that maybe what we really want here is some helpful high-level APIs for writing daemon-like programs, that make it easy to handle the three main cases of run in foreground, run daemonized, run as windows service, and abstract over things like SIGTERM versus SERVICE_CONTROL_STOP.
For what it's worth, I think a lot of things fire SIGTERM for a graceful shutdown, and SIGKILL for a "kill it now, with fire". A quick look around tells me Kubernetes and systemd both fire SIGTERM, wait a bit, then fire SIGKILL.
Reacted by Ivan Shapovalov, Ryan Hiebert, Jim Brännlund and Sascha Desch
changed the title [-]clean shutdown on SIGTERM[/-][+]Should our strategy for handling KeyboardInterrupt be usable for other signals too?[/+]on Mar 12, 2019
Docker also sends SIGTERM when stopping a container, waits for a grace period, then sends SIGKILL if needed.
...there's also an argument that trio's default behavior should be to raise trio.TerminatedError or something on SIGTERM
This is my favored approach. But I understand if others think this is too magical.
having the Python-level handler keep track of how often it's been called, and if it gets called more than some threshold, switch to just killing the process (because it appears to be wedged), maybe after printing a message. This is probably only really useful for SIGINT though, since SIGTERM isn't generally used interactively...
I like this idea, especially if the first SIGINT is restricted to checkpoints. Then the nth SIGINT (I would expect n=2) can be used to break out of loops that don't have any checkpoints. Best of both worlds.
Anything other than the process just disappearing would be better. A small signal receiver block in "application running" state is handy but if a SIGTERM appears anywhere else, finally and __exit__ don't get run.
Wrapping the entire application into a signal receiver is problematic because then it should be guaranteed that at any phase during startup and shutdown the signals actually get received. Otherwise the application may become very hard to terminate.
Wrapping the entire application into a signal receiver is problematic because then it should be guaranteed that at any phase during startup and shutdown the signals actually get received. Otherwise the application may become very hard to terminate.
I'm not sure I'm understanding. During startup or shutdown, when the signal receiver isn't running, then you get the default signal behavior, of killing the process. Which is probably what you want. (If you haven't started up yet, or have already shut down, then there's no need to do any more cleanup.)
There is a race condition around unregistering the signal handler, where a signal could arrive just while you're in the middle of unregistering it and potentially get lost. I believe that Trio is the only Python async library that doesn't lose signals in this case. We take some special care, where if a signal arrives during the critical period, we wait until we've finished restoring the old signal handlers and then re-deliver it, so that it acts like it arrived a few milliseconds later:
I intended for KeyboardInterrupt style handling of interrupts during application_init and application_shutdown -- but SIGTERM just terminates the process. Moving the signal receiver with block outside everything else makes the application ignore signals during init and shutdown (because no-one is receiving), making it impossible to terminate.
This is somewhat more refined in attempting to support multiple levels of graceful termination, and obscures the main application logic a lot less with signal handling.
The idea is to cancel the innermost scope (that has added itself to cancel_scopes) on each interrupt. The first one cancels acceptor but allows gracefully finishing any running tasks -- unless that takes too long and another signal is received, in which case application_shutdown is still attempted, and so on, until finally cancel_scope is canceled and the signal handler code should exit before monitor_signals crashes trying to pop from empty list.
I am not entirely happy with this construct either, mainly because cancel_scopes needs to be passed around and fiddled with manually (I did consider my own context managers or a wrapper around open_nursery instead of append/pop -- but they'd still be very clumsy/invasive).
Additionally, there is nothing to be done if the application code is stuck somewhere without scheduling points. The signal receiver never runs and the interrupts sit in Trio's internal queue for eternity.
The ideal would be:
Raise KeyboardInterrupt for all signals that would otherwise instantly terminate the process (i.e. have SIG_DFL handler and no receiver).
Whenever a signal is received and being appended to receiver queue, and there are signals older than one second in that queue, raise KeyboardInterrupt in the frame that was running.
Part 1 provides useful defaults for all applications that rely on context managers or try-finally style shutdown. Part 2 takes care of situations where the signal receiver loop cannot run in a timely manner.
Please pardon my ignorance — but why won't a simple solution suffice: how about a trio.run(..., extra_signals=[SIGTERM], extra_signals_exception=SomethingBaseException)? Then it's just a matter of handling [SIGINT] + extra_signals where you would only catch [SIGINT] before, and a condition on which exception to raise.
The restrict_keyboard_interrupt_to_checkpoints can be then renamed to something like restrict_signals_to_checkpoints, with the old name accepted as a compatibility alias.
...that is, I just convert SIGTERMs to SIGINTs and let internal Trio magic deal with the signal in its fancy race-free way. Ugly, but as close to a proper solution as I can imagine.
Did I miss anything?
Activity
Badg commentedon Apr 21, 2017
I would be interested in seeing the equivalent for Windows as well, though admittedly I have next to no recollection how Windows notifies running processes of imminent death, so it may very well be more trouble than it's worth. But that's something missing from hypergolix, which may eventually transition over to Trio for reliability reasons.
njsmith commentedon Apr 23, 2017
Ah, Windows is an interesting point. AFAICT Windows doesn't have any equivalent of catching SIGTERM; for ordinary process termination there's just
TerminateProcess, and that's the equivalent ofSIGKILL. But, they do have "services", which are the equivalent of Unix daemons, and the service API does have a way for a "service manager" (their equivalent or runit or supervisord or whatever) to send a request to a service to exit cleanly.So this is making me think that maybe what we really want here is some helpful high-level APIs for writing daemon-like programs, that make it easy to handle the three main cases of run in foreground, run daemonized, run as windows service, and abstract over things like SIGTERM versus SERVICE_CONTROL_STOP.
Prior art:
dstufft commentedon Jul 20, 2018
For what it's worth, I think a lot of things fire
SIGTERMfor a graceful shutdown, andSIGKILLfor a "kill it now, with fire". A quick look around tells me Kubernetes and systemd both fireSIGTERM, wait a bit, then fireSIGKILL.[-]clean shutdown on SIGTERM[/-][+]Should our strategy for handling KeyboardInterrupt be usable for other signals too?[/+]mehaase commentedon Jun 27, 2019
Docker also sends
SIGTERMwhen stopping a container, waits for a grace period, then sendsSIGKILLif needed.This is my favored approach. But I understand if others think this is too magical.
I like this idea, especially if the first SIGINT is restricted to checkpoints. Then the nth SIGINT (I would expect n=2) can be used to break out of loops that don't have any checkpoints. Best of both worlds.
Tronic commentedon Sep 9, 2019
Anything other than the process just disappearing would be better. A small signal receiver block in "application running" state is handy but if a SIGTERM appears anywhere else,
finallyand__exit__don't get run.Wrapping the entire application into a signal receiver is problematic because then it should be guaranteed that at any phase during startup and shutdown the signals actually get received. Otherwise the application may become very hard to terminate.
njsmith commentedon Sep 12, 2019
I'm not sure I'm understanding. During startup or shutdown, when the signal receiver isn't running, then you get the default signal behavior, of killing the process. Which is probably what you want. (If you haven't started up yet, or have already shut down, then there's no need to do any more cleanup.)
There is a race condition around unregistering the signal handler, where a signal could arrive just while you're in the middle of unregistering it and potentially get lost. I believe that Trio is the only Python async library that doesn't lose signals in this case. We take some special care, where if a signal arrives during the critical period, we wait until we've finished restoring the old signal handlers and then re-deliver it, so that it acts like it arrived a few milliseconds later:
trio/trio/_signals.py
Lines 77 to 93 in 6df291b
Tronic commentedon Sep 12, 2019
My first attempt was this kind of construct:
I intended for KeyboardInterrupt style handling of interrupts during application_init and application_shutdown -- but SIGTERM just terminates the process. Moving the signal receiver
withblock outside everything else makes the application ignore signals during init and shutdown (because no-one is receiving), making it impossible to terminate.The current revision:
This is somewhat more refined in attempting to support multiple levels of graceful termination, and obscures the main application logic a lot less with signal handling.
The idea is to cancel the innermost scope (that has added itself to cancel_scopes) on each interrupt. The first one cancels acceptor but allows gracefully finishing any running tasks -- unless that takes too long and another signal is received, in which case application_shutdown is still attempted, and so on, until finally cancel_scope is canceled and the signal handler code should exit before monitor_signals crashes trying to pop from empty list.
I am not entirely happy with this construct either, mainly because cancel_scopes needs to be passed around and fiddled with manually (I did consider my own context managers or a wrapper around open_nursery instead of append/pop -- but they'd still be very clumsy/invasive).
Additionally, there is nothing to be done if the application code is stuck somewhere without scheduling points. The signal receiver never runs and the interrupts sit in Trio's internal queue for eternity.
The ideal would be:
Part 1 provides useful defaults for all applications that rely on context managers or try-finally style shutdown. Part 2 takes care of situations where the signal receiver loop cannot run in a timely manner.
intelfx commentedon Feb 24, 2020
Please pardon my ignorance — but why won't a simple solution suffice: how about a
trio.run(..., extra_signals=[SIGTERM], extra_signals_exception=SomethingBaseException)? Then it's just a matter of handling[SIGINT] + extra_signalswhere you would only catch[SIGINT]before, and a condition on which exception to raise.The
restrict_keyboard_interrupt_to_checkpointscan be then renamed to something likerestrict_signals_to_checkpoints, with the old name accepted as a compatibility alias.As of now, I just did this:
...that is, I just convert SIGTERMs to SIGINTs and let internal Trio magic deal with the signal in its fancy race-free way. Ugly, but as close to a proper solution as I can imagine.
Did I miss anything?