-
-
Notifications
You must be signed in to change notification settings - Fork 346
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
CancelScope(shield=True) protects nonmain tasks from KeyboardInterrupt #1014
Comments
The short answer is: there's no special mechanism for converting However, there is a general mechanism, where if an exception is raised in some task and you don't catch it, then the exception propagation machinery will cancel that task's siblings, because that's the only way it can keep unwinding the stack and propagating the exception. And It's a special exception because it can pop into existence anywhere and anytime (almost), but once it's raised then it's just a regular exception, and it works just like any other time some code crashes with a random exception. To give some more details about your specific observations: By default, the contract is that Assuming you don't do that, then here's how it works currently: There are internally two different mechanisms that can raise Currently, the KI protection APIs only affect the first mechanism, that can raise So when you hit control-C, the flowchart is something like: Is there a task running right now? Does it have Otherwise, pick an arbitrary victim task, and try to wake it up via the cancellation machinery. When it wakes up, send it In practice, right now Trio always picks the main task (the first one running user code) as its "arbitrary" task, because this simplifies the code. This is perfectly consistent with the contract ( It's pretty messy. Some general guidelines:
|
Thanks for the detailed response! This explanation generally matches the impression I had from reading the docs. (Although in some aspects it’s clearer than the docs and the docs could possibly be improved: e. g. by adding a note that within an However, the example code I posted shows that just a shielded cancel scope makes a task always ignore all keyboard interrupts. No matter how much I try, I can’t interrupt it. Following your explanation, normally I’d expect that the interrupt will occasionally happen while the task is sleeping and in that case the nursery will attempt to cancel the task but the shielding will protect it, but most of the time the interrupt will happen while the As for my personal use case for ignoring interrupts, I want to do it only for specific tasks that don’t run all of the time. I do want the rest of the program to be interrupted and the program to quit as soon as all of these specific tasks complete. Before converting to async, I used to launch these tasks in new threads to evade the main thread’s If I understand correctly, if I were to use It occurs to me that I could have it explicitly cancel my root nursery or main task, thereby converting the keyboard interrupt into a cancellation, which would then be affected by shields as usual. To do this, I’d need to store my main nursery somewhere and make sure I do nothing outside of this nursery, or use All in all, this would require extra nontrivial code. You could say that’s the price I deserve to pay for this and maybe you’d be right*, but it would be so much nicer to be able to put the critical code in a context manager and have it all done automatically. And curiously, it seems it is possible currently, even if this is possibly unintended! just by wrapping it in a shielded cancel scope within an explicit nursery. * And another way to look at it might be that if trio didn’t have special handling for keyboard interrupts, I’d want to put the event loop in a separate thread, catch the interrupt in the main and explicitly cancel the event loop anyway. Or even that this might look better/clearer in the code regardless of trio’s ability to handle interrupts. |
That's weird. I tried running your code on my laptop (CPython 3.6.5, trio 0.11.0, Ubuntu 18.10), and my first two runs I got:
and
So it's not 100% deterministic, but out of three control-C's, two of them did interrupt the shielded code – I tried running it a bunch more times, and I never had to hit control-C more than four times to get the program to exit. Are you sure you're running exactly the code you pasted in the first issue? You don't have any stray
If you want to cleanly cancel everything in the program, you can always raise an exception, like Of course for a signal handler like this, you'll usually want to set it up at the top of your async def clean_shutdown_on_control_C():
with trio.open_signal_receiver(signal.SIGINT) as signal_receiver:
async for _ in signal_receiver:
sys.exit("Interrupted")
async def main():
async with trio.open_nursery() as nursery:
nursery.start_soon(clean_shutdown_on_control_C)
# ... main program code here ...
# and when we're done, cancel the signal handling task
nursery.cancel_scope.cancel() You could also use |
OK, I’m very sorry, it looks like I got excited too quickly! I have just retried the exact same test, and now I’m able to kill it after at most several Ctrl+C presses. Weird. I must’ve been very unlucky when I originally tested this. I did double-check that I had the So if I understand correctly, currently Thanks for the new suggestion! D’oh, of course I can just throw something and let it unwind by itself. That’s simpler than I thought it would be. I’ll take this approach, since I don’t want to rely on the current behaviour described above if it can silently change in the future. |
Using CPython 3.6.5 and trio 0.11.0 on openSUSE Leap 42.3, the following code keeps running
foo
forever even after multiple keyboard interrupts:For reference, here’s how it interacts with
@trio.hazmat.enable_ki_protection
:@trio.hazmat.enable_ki_protection
tofoo
and keeping the shielding changes nothing.@trio.hazmat.enable_ki_protection
and removing shielding makes it get aCancelled
exception fromawait trio.sleep(0)
.KeyboardInterrupt
anywhere orCancelled
fromawait trio.sleep(0)
.In my real use-case this is actually desired behaviour as I want a certain task to run to completion, including all async operations it performs, regardless of any keyboard interrupts; but it seems this behaviour is not documented.
Furthermore, it is inconsistent with the main task (is that the right term?) that is executed by
trio.run
: replacingtrio.run(main)
withtrio.run(foo)
makesfoo
always getKeyboardInterrupt
regardless of the shielding, either anywhere (without@trio.hazmat.enable_ki_protection
) or exactly fromawait trio.sleep(0)
(with@trio.hazmat.enable_ki_protection
).I tried to search for this and saw #151, but it’s not clear to me whether it says the same thing about the main task (and implies that this is the intended behaviour in descendant tasks) or that shielded scopes never protect against
KeyboardInterrupt
(which is clearly not the case).If this behaviour is intended, it would be good to make it clear in the documentation. All I saw about shielded scopes is that they protect from
Cancelled
. Meanwhile,@trio.hazmat.enable_ki_protection
didn’t make it clear whether the task would actually get aKeyboardInterrupt
fromawait <checkpoint>
or just allow aKeyboardInterrupt
to be raised after it yields control back to the event loop, in which case it might get cancelled by its parent nursery but be able to shield itself. (However, even if@trio.hazmat.enable_ki_protection
is indeed supposed to turn interrupts into cancels, it is still not clear why shielding alone protects me from interrupts.)The text was updated successfully, but these errors were encountered: