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
.Updated for Python 3.6+: Add call to
loop.shutdown_asyncgens
to avoid memory leaks by asynchronous generators that weren't fully used. Additionallyasyncio.new_event_loop
is now used rather thenasyncio.get_event_loop
to ensure that the finalloop.close
call does not interfere with possible other uses of the loop.The following solution, inspired by some of the other answers, should work in almost all cases and does not depend on you manually keeping track of tasks that need to be cleaned up on Ctrl+C:
The above code will obtain all currently tasks from the event loop using
asyncio.Task.all_tasks
and place them in a single combined future usingasyncio.gather
. All tasks in that future (which are all currently running tasks) are then canceled using the future's.cancel()
method. Thereturn_exceptions=True
then ensures that all the receivedasyncio.CancelledError
exceptions are stored instead of causing the future to become errored.The above code will also override the default exception handler to prevent the generated
asyncio.CancelledError
exceptions from being logged.Unless you are on Windows, set up event-loop based signal handlers for SIGINT (and also SIGTERM so you can run it as a service). In these handlers, you may either exit the event loop immediately, or initiate some kind of cleanup sequence and exit later.
Example in official Python documentation: https://docs.python.org/3.4/library/asyncio-eventloop.html#set-signal-handlers-for-sigint-and-sigterm