This is a split-out thread from a prior thread.
From the perspective of a task, cancellation is observed as asyncio.CancelledError
raised from an await
statement or async
structs such as async for
or async with
.
I had some hard-to-debug errors with long-running tasks silently cancelled in production systems. While debugging, I wanted to keep track of the source of cancellation – e.g., is it from mis-triggered shutdown signals or mis-propagation of internal cancellations in the task tree?
Another case is to distinguish whether the task is cancelled by its own reason or by a taskgroup when a sibling task has propagated an unhandled exception to the taskgroup.
Usually when I write a “cancellable” task, I often use the following pattern:
async def mytask():
try:
async with some_resource():
...
except Exception:
log.exception("unexpected error in mytask")
except asyncio.CancelledError:
pass
This makes it hard to debug the siutations mentioned above, because the task “silently” does nothing and terminates upon any cancellation error from the inside.
I’d like to change it something like:
async def mytask():
try:
async with some_resource():
...
except Exception:
log.exception("unexpected error in mytask")
except asyncio.CancelledError as e:
log.debug("mytask is cancelled by %s", e.source)
By inspecting the exception object and traceback, we can determine which await
or async
-struct statement in the body has raised the cancellation (i.e., where it is cancelled), but not who ultimately triggered the cancellation. The information “where it is cancelled” is less important as long as all the resources are properly released upon cancellation, and Python asyncio provides good means to this: context managers. When debugging, the information “who triggered cancellation” is often much more important.
My initial idea is to add a source
attribute to asyncio.CancelledError
which represent the name of module/function path which called Task.cancel()
, e.g., "async_timeout.timeout"
, "myapp.server.shutdown"
, etc. Of course, I think the core devs would have better realization ideas.
We may need to consider stacking the source of cancellations like exception’s __cause__
attribute, when cancellation triggers another cancellation.
What about your thoughts?