Graceful shutdown of asyncio coroutines

What does Task was destroyed but it is pending! mean?

If at the moment your program finished some of asyncio tasks still not finished, you'll get this warning. This warning is needed because some task that are running may not correctly free some resources.

There're two common ways to solve it:

  1. You can wait while tasks finished themselves
  2. You can cancel tasks and wait while them finished

Asyncio and blocking synchronous operations

Let's look at you code:

def shutdown(proto, loop):
    print("Shutdown of DummyProtocol initialized ...")
    proto.close()

    time.sleep(2)
    # ...

time.sleep(2) - this line won't give coroutines time to finished. It'll just freeze all your program for two second. Nothing will happen during this time.

It happens because your event loop is running in same process where you call time.sleep(2). You should never call long running synchronous operations this way in your asyncio programs. Please read this answer to see how async code works.

How can we wait tasks finished

Let's try to modify shutdown function. This is not async function, you can't await something inside it. To execute some async code we need to do it manually: stop currently running loop (since it's already running), create some async function to await tasks finished, pass this function to be executed in event loop.

def shutdown(proto, loop):
    print("Shutdown of DummyProtocol initialized ...")

    # Set shutdown event: 
    proto.close()

    # Stop loop:
    loop.stop()

    # Find all running tasks:
    # For python version < 3.7 use asyncio.Task.all_tasks()
    # For python version >= 3.7 use asyncio.all_tasks()
    pending = asyncio.Task.all_tasks()

    # Run loop until tasks done:
    loop.run_until_complete(asyncio.gather(*pending))

    print("Shutdown complete ...")    

You can also just cancel tasks and await them finished. See this answer for details.

Where to place clean up operations

I'm not too familiar with signals, but do you really need it to catch CTRL-C? Whenever KeyboardInterrupt happens it will thrown at the line where you run the event loop (in you code it's loop.run_forever()). I might be wrong here, but a common way to handle this situation is to place all cleanup operations in a finally block.

For example, you can see how aiohttp does it:

try:
    loop.run_forever()
except KeyboardInterrupt:  # pragma: no branch
    pass
finally:
    srv.close()
    loop.run_until_complete(srv.wait_closed())
    loop.run_until_complete(app.shutdown())
    loop.run_until_complete(handler.finish_connections(shutdown_timeout))
    loop.run_until_complete(app.cleanup())
loop.close()

To complete the accepted answer, you can use aiorun which handles this problem for you pretty well: https://github.com/cjrh/aiorun