asyncio catch TimeoutError in Task

17.1k views Asked by At

I have an asyncio.Task that I need to cancel after some amount of time. Before cancelling, the task needs to do some cleanup. According to the docs, I should just be able to call task.cancel or asyncio.wait_for(coroutine, delay) and intercept an asyncio.TimeoutError in the coroutine, but the following example is not working. I've tried intercepting other errors, and calling task.cancel instead, but neither have worked. Am I misunderstanding how cancelling a task works?

@asyncio.coroutine
def toTimeout():
  try:
    i = 0
    while True:
      print("iteration ", i, "......"); i += 1
      yield from asyncio.sleep(1)
  except asyncio.TimeoutError:
    print("timed out")

def main():
  #... do some stuff
  yield from asyncio.wait_for(toTimeout(), 10)
  #... do some more stuff

asyncio.get_event_loop().run_until_complete(main())
asyncio.get_event_loop().run_forever()
1

There are 1 answers

0
dano On BEST ANSWER

The documentation for asyncio.wait_for specifies that it will cancel the underlying task, and then raise TimeoutError from the wait_for call itself:

Returns result of the Future or coroutine. When a timeout occurs, it cancels the task and raises asyncio.TimeoutError.

And you are correct that task cancellation can indeed be intercepted:

[Task.cancel] arranges for a CancelledError to be thrown into the wrapped coroutine on the next cycle through the event loop. The coroutine then has a chance to clean up or even deny the request using try/except/finally.

Note that the docs specify that CancelledError is thrown into the coroutine, not TimeoutError.

If you make that adjustment, things work the way you expect:

import asyncio

@asyncio.coroutine
def toTimeout():
  try:
    i = 0
    while True:
      print("iteration ", i, "......"); i += 1
      yield from asyncio.sleep(1)
  except asyncio.CancelledError:
    print("timed out")

def main():
  #... do some stuff
  yield from asyncio.wait_for(toTimeout(), 3)
  #... do some more stuff

asyncio.get_event_loop().run_until_complete(main())

Output:

iteration  0 ......
iteration  1 ......
iteration  2 ......
timed out
Traceback (most recent call last):
  File "aio.py", line 18, in <module>
    asyncio.get_event_loop().run_until_complete(main())
  File "/usr/lib/python3.4/asyncio/base_events.py", line 316, in run_until_complete
    return future.result()
  File "/usr/lib/python3.4/asyncio/futures.py", line 275, in result
    raise self._exception
  File "/usr/lib/python3.4/asyncio/tasks.py", line 238, in _step
    result = next(coro)
  File "aio.py", line 15, in main
    yield from asyncio.wait_for(toTimeout(), 3)
  File "/usr/lib/python3.4/asyncio/tasks.py", line 381, in wait_for
    raise futures.TimeoutError()
concurrent.futures._base.TimeoutError

As you can see, now 'timed out' gets printed before the TimeoutError is raised by wait_for.