In essence my question is when and where is the asyncio.CancelledError
exception raised in the coroutine being cancelled?
I have an application with a couple of async tasks that run in a loop. At some point I start those tasks like this:
async def connect(self);
...
t1 = asyncio.create_tasks(task1())
t2 = asyncio.create_task(task2())
...
self._workers = [t1, t2, ...]
When disconnecting, I cancel the tasks like this:
async def disconnect(self):
for task in self._workers:
task.cancel()
This has been working fine. The documentation of Task.cancel
says
The coroutine then has a chance to clean up or even deny the request by suppressing the exception with a
try … … except CancelledError … finally
block. Therefore, unlikeFuture.cancel()
,Task.cancel()
does not guarantee that the Task will be cancelled, although suppressing cancellation completely is not common and is actively discouraged.
so in my workers I avoid doing stuff like this:
async def worker():
while True:
...
try:
some work
except:
continue
but that means that now I have to explicitly put asyncio.CancelledError
in the
except
statement:
async def worker():
while True:
...
try:
some work
except asyncio.CancelledError:
raise
except:
continue
which can be tedious and I also have to make sure that anything that I call from my worker obliges by this rule.
So now I'm not sure if this is a good practice at all. Now that I'm thinking about it, I don't even know when exactly the exception is raised. I was searching for a similar case here in SO and found this question which also raised the same question "When will this exception be thrown? And where?". The answer says
This exception is thrown after
task.cancel()
is called. It is thrown inside the coroutine, where it is caught in the example, and it is then re-raised to be thrown and caught in the awaiting routine.
And while it make sense, this got me thinking: this is async scheduling, the
tasks are not interrupted at any arbitrary place like with threads but they only
"give back control" to the event loop when a task does an await
. Right?
So that means that checking everywhere whether
asyncio.CancelledError
was raised might not be necessary. For example, let's
consider this example:
def worker(interval=1):
while True:
try:
# doing some work and no await is called in this block
sync_call1()
sync_call2()
sync_call3()
except asyncio.CancelledError:
raise
except:
# deal with error
pass
await asyncio.sleep(interval)
So I think here the except asyncio.CancelledError
is unnecessary because this
error cannot "physically" be raised in the try/block
at all since the thread
in the try
block will never be interrupted by the event loop. The only place
where this task gives back the control to the event loop is at the sleep
call,
that is not even in a try/block
and hence it doesn't suppress the exception. Is
my train of though correct? If so, does that mean that I only have to account
for asyncio.CancelledError
when I have an await
in the try
block? So would
this also be OK, knowing that worker()
can be cancelled?
def worker(interval=1):
while True:
try:
# doing some work and no await is called in this block
sync_call1()
sync_call2()
sync_call3()
except:
# deal with error
pass
await asyncio.sleep(interval)
And after reading the answer of the other SO question, I think I should also
wait for the cancelled tasks in my disconnect()
function, do I? Like this?
async def disconnect(self):
for task in self._workers:
task.cancel()
await asyncio.gather(*self._workers)
Is this correct?