-
-
Notifications
You must be signed in to change notification settings - Fork 349
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
It should be possible to get a traceback out of a cancel scope #285
Comments
What about converting the This would also help users that want to debug where their cancellation occurred on a given cancel scope by allowing them to get some printable info from the scope. |
Never mind. The So what are the risks of storing the |
I think the simplest approach is probably to have some sort of
save_exceptions flag you have to enable, since fail_after knows well
beforehand that it will want the exceptions, it can hide the extra
fiddliness from the user, and I don't have any other use cases in mind.
Capturing by default is tricky because there are sensible use cases where
cancel scope objects are saved and may inadvertantly outlive the stack
frame they apply to. (Like when you save off a cancel scope somewhere so
another task can find it and call its cancel method.)
…On Nov 8, 2017 07:48, "Nick Malaguti" ***@***.***> wrote:
Never mind. The __cause__ must derive from BaseException.
So what are the risks of storing the Cancelled error on the scope object?
Make it an option on open_cancel_scope to save the error and make it
available? Default true or false? Make it the users problem if they keep
their scopes around with references that hold memory and don’t set
keep_exception to false?
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#285 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAlOaAGCxHTebNW3cOzSr95UthvZyNB_ks5s0bE_gaJpZM4O2NXc>
.
|
What about providing a hook for the user to transform the exception how they'd like, and with a sensible default (e.g. saving |
Do you have a use case? |
I'm thinking of the times I ran into the same or a similar issue with asyncio. By default, asyncio doesn't seem to provide access to the full stack trace when a Disclaimer: I'm not using trio currently, but I'd like to at some point in the future when it's more mature. After wrestling with a number of the asyncio issues that trio was designed to solve, trio seems like the right way to go. (So thanks for all your work in developing a better way! It will be huge I hope!) |
@cjerdonek Trio does provide full stack traces in general AFAIK – it even has some horrible code to reconstruct more accurate tracebacks in certain cases. If you run into cases where it doesn't then please file a bug :-). The issue here is more specific. In Trio, cancellation is done using "cancel scopes": you wrap a chunk of code in a In this specific situation, we know ahead of time that we want to do this, and we actually want the |
I thought of another complication: there's some subtle interaction between
Right now, So the simple way to do this would be to have our handler check the flag, and if it's set, then for each primitive Another option, a bit hackier but maybe simpler if it works, would be to say oh well, we actually only care about capturing these exceptions when we're in the first case above. So what we could do is have an explicit check where we save the original |
Funnily enough, it appears that implicit exception chaining is actually doing what we want here; there's some discussion at https://gitter.im/python-trio/general?at=5ad5753a27c509a7741cca44.
Simpler illustrative example:
This is pretty subtle, though, so probably we still want the explicit chaining or a comment in fail_after. It's also unlikely that the implicit chaining will do the right thing with MultiErrors; my guess would be that the entire MultiError gets attached rather than just the Cancelled part, but I haven't verified this. |
Relevant to python-trio#886, python-trio#606, python-trio#285, python-trio#147, python-trio#70, python-trio#58, maybe others. I was continuing my effort to shoehorn linked cancel scopes and graceful cancellation into `CancelScope` earlier today and it was feeling too much of a mess, so I decided to explore other options. This PR is the result. It makes major changes to Trio's cancellation internals, but barely any to Trio's cancellation semantics -- all tests pass except for one that is especially persnickety about `cancel_called`. No new tests or docs yet as I wanted to get feedback on the approach before polishing. An overview: * New class `CancelBinding` manages a single lexical context (a `with` block or a task) that might get a different cancellation treatment than its surroundings. "All plumbing, no policy." * Each cancel binding has an effective deadline, a _single_ task, and links to parent and child bindings. Each parent lexically encloses its children. The only cancel bindings with multiple children are the ones immediately surrounding nurseries, and they have one child binding per nursery child task plus maybe one in the nested child. * Each cancel binding calculates its effective deadline based on its parent's effective deadline and some additional data. The actual calculation is performed by an associated `CancelLogic` instance (a small ABC). * `CancelScope` now implements `CancelLogic`, providing the deadline/shield semantics we know and love. It manages potentially-multiple `CancelBinding`s. * Cancel stacks are gone. Instead, each task has an "active" (innermost) cancel binding, which changes as the task moves in and out of cancellation regions. The active cancel binding's effective deadline directly determines whether and when `Cancelled` is raised in the task. * `Runner.deadlines` stores tasks instead of cancel scopes. There is no longer a meaningful state of "deadline is in the past but scope isn't cancelled yet" (this is what the sole failing test doesn't like). If the effective deadline of a task's active cancel binding is non-infinite and in the future, it goes in Runner.deadlines. If it's in the past, the task has a pending cancellation by definition. Potential advantages: * Cancellation becomes extensible without changes to _core, via users writing their own CancelLogic and wrapping a core CancelBinding(s) around it. We could even move CancelScope out of _core if we want to make a point. * Nursery.start() is much simpler. * Splitting shielding into a separate object from cancellation becomes trivial (they'd be two kinds of CancelLogic). * Most operations that are performed frequently take constant time: checking whether you're cancelled, checking what your deadline is, entering and leaving a cancel binding. I haven't benchmarked, so it's possible we're losing on constant factors or something, but in theory this should be faster than the old approach. * Since tasks now have well-defined root cancel bindings, I think python-trio#606 becomes straightforward via providing a way to spawn a system task whose cancel binding is a child of something other than the system nursery's cancel binding. Caveats: * We call `current_time()` a lot. Not sure if this is worth worrying about, and could probably be cached if so. * There are probably bugs, because aren't there always? Current cancel logic: ``` def compute_effective_deadline( self, parent_effective_deadline, parent_extra_info, task ): incoming_deadline = inf if self._shield else parent_effective_deadline my_deadline = -inf if self._cancel_called else self._deadline return min(incoming_deadline, my_deadline), parent_extra_info ``` Want to support a grace period? I'm pretty sure it would work with something like ``` def compute_effective_deadline( self, parent_effective_deadline, parent_extra_info, task ): parent_cleanup_deadline = parent_extra_info.get("effective_cleanup_deadline", parent_effective_deadline) if self._shield: parent_effective_deadline = parent_cleanup_deadline = inf my_cleanup_start = min(self._deadline, self._cancel_called_at) merged_cleanup_deadline = min(parent_cleanup_deadline, my_cleanup_start + self._grace_period) my_extra_info = parent_extra_info.set("effective_cleanup_deadline", merged_cleanup_deadline) if self._shield_during_cleanup: effective_deadline = merged_cleanup_deadline else: effective_deadline = min(parent_effective_deadline, my_cleanup_start) return effective_deadline, my_extra_info ``` Maybe that's not quite _simple_ but it is miles better than what I was looking at before. :-)
With
fail_after
, it's a bit annoying that it can't show you where the block was cancelled. (The traceback just starts at the end of the scope.)This will be a problem in particular for test timeouts. (Well, they probably won't use
fail_after
, but they'll have the same problem.)I don't think we can capture the exceptions in cancel scope objects by default, because we don't want to pin the stacks in memory.
We could have a flag that tells it to save caught exceptions, though. Or try to do something clever with catching them ourselves inside the scope, but that's basically the same thing from the user point of view, and the cancel scope already knows how to reliably catch its own exceptions.
The text was updated successfully, but these errors were encountered: