Subclassing CancelledError

I’m working with real-time video. When switching avatars or scenes, I use task.cancel(), which raises a CancelledError, to stop the previous avatar/scene…Then begin rendering the next avatar/scene.

In the render loop, I would like to have 2 types of cancels. One for quickly stop the render loop when shutting down. One to stop the current iteration & continue to a new iteration.

I would find passing a subclass of CancelledError to task.cancel would be convenient…And it fits my mental model of what is supposed to happen.

However task.cancel() doesn’t accept an argument for an exception type. I could use task.set_exception but I see some info that the method has been deprecated…and it’s certainly not as natural as calling task.cancel. Another technique is to have a custom message & parse the message for the Cancel “type”.

I could use asyncio.Event, a Queue message, or a field value to cause a cancel. However, this requires frequent polling with asyncio.sleep(0.001)…or some other value with the tradeoff of spinlock vs extra latency. A simple await on a resource with different types of CancelledError would be preferable. Less logic, less latency, less CPU overhead, no spin-lock vs latency trade off, no extra watcher loops, CancelledError management in one place instead of spread throughout the program.

Multiple layers of tasks is another option. But this requires extra management of the layers & their cancellations. It becomes particularly difficult to manage when the layers call common async methods…Particularly in tracking where the CancelledError needs to be handled if the layered task was awaited from somewhere in the render loop. One would need an in-depth understanding of the ins & outs of Python’s cancellation bubbling to comprehend the architecture.

I’m exploring using Task Manager objects with some success. The Taskman objects help I still would like to cancel the managed tasks with a subclass of CancelledError…As the reason why a task was cancelled is a cross-cutting concern. I would need to create * n Taskman objects for the domain of tasks & the reason for cancellation if I were to just use .cancel without expressing why it was cancelled.

Keeping track of all the different types of cancellations spread throughout the program is challenging…without explicit types. It seems that the simplest way to manage all of this is with a subclass of CancelledError. Where the different subtypes of CancelledError bubble up & be handled in the render loop.

This is similar to how processors have multiple interrupt ops. Effectively the CancelledError is the “interrupt” of a coroutine.

Anyways, there is support for subclasses of CancelledError in CPython error handling.

And there’s been discussion on topics relating to custom types using set_exception. I think the use case I have is a real-world example where subclasses of CancelledError would simplify the program…but I may be wrong.

If anyone has any effective patterns to cleanly solve the issue I’m facing, please let me know.

This has been vexing me for a while. I had an experiment where I created Exception types for a called method to “interrupt” the caller’s loop. This works from inside the method, but not from task.cancel. I don’t use these exceptions anymore…It was a half-baked solution to a problem…which would be elegantly solved by subclassing CancelledError. Note that I didn’t subclass CancelledError here b/c I read that we aren’t supposed to. But it sure would be nice if subclassing CancelledError were explicitly supported.

class Break(Exception):
    pass


class Continue(Exception):
    pass


class Return(Exception):
    pass


class Cancel(Exception):
    pass

Something like this could be helpful in breaking up some large loops that have control flow…via function decomposition. It wouldn’t always be the best pattern, but would sometimes be useful. Note I wouldn’t use one of these for the problem I stated in the beginning of the post. But these Exceptions could be useful for some cases of decomposing a loop into functions. Though it feels weird to raise an asyncio.CancelledError in a synchronous call stack.

class Break(CancelledError):
    pass


class Continue(CancelledError):
    pass


class Return(CancelledError):
    pass

How about hand rolling a simple event loop, and breaking out of it for the hard stop?

It’s clear that CancelledError is being used internally to cancel a Future. And the docs even give an example of catching and reraising Cancelled Errors (or even suppressing them and calling uncancel). But combining exception handling with async could easily lead to tricky bugs, so it would definitely not be my first choice.

you should use two cancel scopes - one for the iteration and one for the full loop:

t1 = None
t2 = None
async def render(t1, t2):
    nonlocal t1, t2
    async with asyncio.timeout() as t1:
        while True:
            try:
                async with asyncio.timeout() as t2:
                    await work()
            except TimeoutError:
                pass

tg.create_task(render())

if cancel_iteration:
    t2.reschedule(-1)
if cancel_everything:
    t1.reschedule(-1)