I propose asyncio.cancel(task, *, msg=None)
a novel utility for helping in cancelling tasks/futures.
Why? I have seen too much code that calls task.cancel() and assumes immediate cancellation. With no await of the cancelled task or even a callback (yuck)
The first thing one needs to do after calling cancel() on the task is to await the task. Which when its finished cancelling will result in a CancelledError being thrown.
We can see in the docs this pattern under Task.cancel
# https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel
task.cancel()
try:
await task
except asyncio.CancelledError:
print("...")
BUT… What if the await task
took some time and something cancelled that task doing the cancelling? It is going to eat the CancelledError
which goes against the contract of cancellation. So we need something a little more complicated to tell the difference between a cancellation from below and one coming from above.
All of this makes task.cancel() pretty non-trivial to use properly. if you need even a short set of steps to do it correctly then we should(imho) have a utility in asyncio
that will take care of it for us. And it should be a coroutine so we can await on its completion.
I have an example of this later/task.py at main · facebookincubator/later · GitHub which is what I have had people use @ Meta to insure something is actually cancelled. I’m sure the Asyncio experts can figure a better way to do what I’m doing with shielding but you get the idea. I wrote some tests for that code also later/test_task.py at main · facebookincubator/later · GitHub
The questions come up what about tasks the don’t cancel, that’s “actively discouraged” in the docs. And to keep the implementation simple if you want to wait a limited amount of time you can wrap the call in a Timeout
. Also with the new uncancel
system i’m sure we could raise an exception if the task you try to cancel is ignoring cancellation.
You could also support the cancel of multiple tasks at one time, but that requires the ExceptionGroup which I didn’t have when I originally wrote later.cancel
, it also complicates the code a little bit.
Also I could be completely off my rocker with this idea, but it has been helpful when handling the orderly shutdown of services at Meta without having to rely on asyncio.run
to cancel all the outstanding tasks when ordering could be a concern.