arthur-tacca / aioresult

Capture the result of a Trio or anyio task
https://aioresult.readthedocs.io/en/stable/index.html
Boost Software License 1.0
11 stars 1 forks source link

Add per-task cancellation? #2

Open arthur-tacca opened 8 months ago

arthur-tacca commented 8 months ago

I deliberately did not include a cancel scope with every ResultCapture object because cancel scopes are not free and, anyway, it goes against the spirit of the library: aioresult is intended for where you're happy with Trio's control flow, but just want to make a note of a return value. But I've realised it would be easy to add a cancel scope to a task by deriving a new class from ResultCapture, which would only be a few lines of code. It would even work with the start_soon() function because this is a class method.

Most importantly, it would be optional for callers, so it wouldn't burden those that don't need it, so I could be persuaded to add it to aioresult. Would it be of interest to anyone?

class CancellableCapture(ResultCapture):
    def __init__(self, routine, *args, suppress_exception: bool = False):
        super().__init__(routine, suppress_exception)
        self._cancel_scope = trio.CancelScope()

    async def run(self, **kwargs):
        with self._cancel_scope:
            await super().run(**kwargs)

    def cancel(self):
        self._cancel_scope.cancel()

    @property
    def cancel_scope(self):
        return self._cancel_scope

This does not attempt to deal with automatic cancellation where there are complex interdependencies between tasks (e.g. as discussed in python-trio/trio#892 and elsewhere) – that is definitely out of scope for this library. But at least it gives you a tool to help solve that manually at the application level.

One difficult aspect to the interface is what result() and exception() should return when a task has been cancelled. With the above implementation you would get the Cancelled exception (assuming that it successfully propagated all the way out of the routine). This is a bit odd because the exception would not be propagated to the containing nursery. The caller could check whether the exception was due to their own cancellation by inspecting my_capture.cancel_scope.cancelled_caught.

(An earlier draft of this code also optionally allowed you to pass in an existing cancel scope to the constructor, perhaps so that you could use the same cancel scope in multiple tasks. But then I remembered that cancel scopes are not reusable in Trio: see python-trio/trio#886.)

davidbrochart commented 4 months ago

I think it would be interesting. I suggested something similar in AnyIO, and Asphat v5.0 will have a TaskHandle that allows cancelling a single task.