I have an event loop that runs some co-routines as part of a command line tool. The user may interrupt the tool with the usual Ctrl + C, at which point I want to clean up properly after the interrupted event loop.
Here's what I tried.
import asyncio
@asyncio.coroutine
def shleepy_time(seconds):
print("Shleeping for {s} seconds...".format(s=seconds))
yield from asyncio.sleep(seconds)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
# Side note: Apparently, async() will be deprecated in 3.4.4.
# See: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
tasks = [
asyncio.async(shleepy_time(seconds=5)),
asyncio.async(shleepy_time(seconds=10))
]
try:
loop.run_until_complete(asyncio.gather(*tasks))
except KeyboardInterrupt as e:
print("Caught keyboard interrupt. Canceling tasks...")
# This doesn't seem to be the correct solution.
for t in tasks:
t.cancel()
finally:
loop.close()
Running this and hitting Ctrl + C yields:
$ python3 asyncio-keyboardinterrupt-example.py
Shleeping for 5 seconds...
Shleeping for 10 seconds...
^CCaught keyboard interrupt. Canceling tasks...
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(1)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(0)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>
Clearly, I didn't clean up correctly. I thought perhaps calling cancel()
on the tasks would be the way to do it.
What's the correct way to clean up after an interrupted event loop?
When you CTRL+C, the event loop gets stopped, so your calls to
t.cancel()
don't actually take effect. For the tasks to be cancelled, you need to start the loop back up again.Here's how you can handle it:
Once we catch
KeyboardInterrupt
, we calltasks.cancel()
and then start theloop
up again.run_forever
will actually exit as soon astasks
gets cancelled (note that cancelling theFuture
returned byasyncio.gather
also cancels all theFutures
inside of it), because the interruptedloop.run_until_complete
call added adone_callback
totasks
that stops the loop. So, when we canceltasks
, that callback fires, and the loop stops. At that point we calltasks.exception
, just to avoid getting a warning about not fetching the exception from the_GatheringFuture
.