-
-
Notifications
You must be signed in to change notification settings - Fork 178
Confusing error handling for KeyboardInterrupt #341
Comments
There is a bit of explanation in BaseEventLoop.run_until_complete: try:
self.run_forever()
except:
if new_task and future.done() and not future.cancelled():
# The coroutine raised a BaseException. Consume the exception
# to not log a warning, the caller doesn't have access to the
# local task.
future.exception()
raise If the exception is an instance of The problem you reported could easily be fixed by removing try:
self.run_forever()
except BaseException as exc:
if future._exception is exc \
# Is this line still necessary?
or new_task and future.done() and not future.cancelled():
# The coroutine raised a BaseException. Consume the exception
# to not log a warning, the caller doesn't have access to the
# local task.
future.exception()
raise However, this part of the code is very central, so I guess any change here is a pretty big deal. |
Thank you. We manage to live with it, it's just debugging will confuse the hell out of new comers. Asyncio is pretty hard to understand, and even more to debug, so I believe we should do everything to make it easier. I have been working on an asyncio project for some month now, and I must say there is no way an average programmer would have the patience to dig into all the debugging sessions we went through. |
It has been suggested that asyncio should properly catch and deliver all
exceptions, even BaseException. I am beginning to think that we should
really do that! There's even an old pull request to do it:
#305
Probably needs updating and I worry that it would break other things
(that's why the PR hasn't been merged yet). But still sounds like it would
generally be an improvement. @asvetlov What do you think? Would aiohttp
need changes if we did this?
|
@gvanrossum But doesn't it make sense for a I'm thinking about how the PR #305 might break the servers relying on |
That's the kind of thing we need to solve before accepting the PR. Do you On Sunday, July 31, 2016, Vincent Michel notifications@github.com wrote:
--Guido (mobile) |
@gvanrossum The default SIGINT handler could stop the loop, and make sure that a You mentioned the fact that a SIGINT won't be able to interrupt a tight CPU loop, but I don't really see that as a problem. |
@gvanrossum I don't see any harm from #305 |
@vxgmichel This is a big no-no. In the first version of uvloop I did exactly this -- handle SIGINT and let the loop to handle it asynchronously. It was completely unusable. Turns out people write tight loops quite frequently, and inability to stop your Python program with Ctrl-C is something they aren't prepared to handle at all. Now I'm using |
I think the problem with the TCP server example can be fixed, with some
care. The example hangs with a similar error if the handler raises a
non-blocking exception (without the BaseException PR), and I think the
problem is that asyncio.start_server() is a bit naive: it creates a Task
and then forgets about it. It should at the very least collect errors from
the Tasks it has created, and that error-catching code could special-case
KeyboardInterrupt.
Also, assuming you're not being DoS'ed with continued connect requests,
hitting ^C again _will_ exit the loop.
|
I crafted another example to demonstrate that some programs might get non-interruptible after PR #305. In this example, the background task is most likely to catch and ignore the first
That is also what gets me worried about PR #305. I'd sum it up this way:
|
Then I would like to evolve #305 into something that always stops the loop
on ^C, unless the application takes specific measures (e.g. installing a
SIGINT handler, or perhaps a try/except that catches KeyboardInterrupt).
|
There is an alternative possibility:
Having a generic middleware system to handle exception and decide which one stop the loop, which one you log and which one you silent would be a nice thing anyway. |
This sounds right.
I'm not sure I like this. What's the use case? What if you have two asyncio libs requiring different configs? |
Two async libs can already change the event loop, the policy and some the signal handlers. There is nothing to stop conflicting libs to screw with each others except documentation and good will. The use case for this is the creation of a framework, and the definition of the error handling policy for this framework : logging, recovery, debugging, etc. E.G: On the project we are working now, we want to make debugging in dev mode very easy, especially for beginners. So we go to great lengths to detect common or weird errors and ensure the user can easily spot them, understand the problem and choose a fix. Unfortunately, with the current architecture, this is complicated, we had to:
And yet, while I think 30% of our unit tests are for those (https://github.com/Tygs/tygs/tree/master/tests), we have still some problems such as some errors being silent in unit tests with pytest. And after all that, and 100% code coverage, we are maybe at 40% of the feature we expect for v0.1, as this took us many more iterations than we expected to get right. Not complaining, I'm glad we have asyncio in Python, and I'm super excited about being able to code this, but let's use the opportunity to make the next people's life easier. Error handling, life cycle and debugging are really a key component to get right in asyncio, especially since the handling of the loop is manual, explicit and open to wind. NodeJS dev don't have to worry about half of that. |
@sametmax Thanks for the explanation. I'll take a closer look at #305 (probably will have to be rewritten) in a few days.
Yeah, I saw that with pytest. Was wondering what's going on but didn't have time to investigate in detail. |
I ran into similar issues as @sametmax. The principles I used, in short:
|
The more I think about it, the more I realise the event loop is kinda half a state machine. And each of us a reinventing the rest, including defined states, event propagation, error handling, etc., so we can have a clean use of asyncio. |
This thread is running off the rails. Asyncio is a much lower level than nodejs, like it or not. Somebody should probably write an opinionated framework for writing servers on top of it, but such a framework does not belong in the stdlib -- just like a web framework does not belong in the stdlib even though sockets are. Different groups may write, promote and adopt different frameworks, just like for web frameworks we have Pyramid, Django, Flask, and many others. (And if you want to discuss this more, please start a new issue so it doesn't add noise to this issue. Or go to python-ideas.) Re-focusing on KeyboardInterrupt, I think the deep problem is due to the way signals work and are expected to work. When you have a callback that somehow doesn't return, e.g. because it contains But when the event loop or some other thing (e.g. a future or queue) is updating its own state we probably don't want the signal to interrupt, and there the behavior of loop.add_signal_handler() is desirable. (Which, BTW, at the scale of callbacks, does the same thing that Python itself does at the scale of opcodes -- delaying the "user" code to handle the signal until a clean breaking point.) Maybe we should mask SIGINT except when callbacks are running or when we're blocked in the selector? And maybe Task._setup() should not call set_exception() when it catches BaseException? (Perhaps instead it should cancel the task?) Then SIGINT will still interrupt callbacks, and it will also interrupt I/O waits, and it can then just be raised out of run_forever() or run_until_complete() without changing any internal state. That's still thinking aloud, but with a focus on the known use cases for ^C. |
Would it be possible to get an update on this issue? |
I reported this issue on the Python tracker but got no answer.
If you trigger
KeyboardInterrupt
in a coroutine and catch it, the program terminates cleanly:This outputs:
It's ok
However, if you wrap the coroutine in a
Task
, you will get a mixed behavior:This outputs:
We have several contradictory behaviors: the
KeyboardInterrupt
is raised, and captured by the future (since your can do task.exception() to suppress the stracktrace) but also catched by except while the program is allowed to continue, yet still the stack trace is displayed and eventually the program return code will be 0.It's very confusing.
I believe it's because exceptions don't break out the event loop, but KeyboardInterrupt is having some kind special treatment.
The text was updated successfully, but these errors were encountered: