Shutdown logic coordinates stopping work and releasing resources when an asyncio application terminates. A graceful shutdown lets in-flight echo tasks finish briefly before the process stops, rather than canceling everything immediately. section 3.6 section 3.7
A practical shutdown sequence can:
- Listen for SIGINT from Ctrl-C and SIGTERM from a process-termination command. section 3.6.1
- Use the event loop’s add_signal_handler method so signal callbacks can interact safely with asyncio. section 3.6.1
- Give pending tasks a fixed interval, such as two seconds, by wrapping them with asyncio.wait_for. section 3.6.2
- Catch expected TimeoutError exceptions, then close the event loop after shutdown work completes. section 3.6.2
Gracefully stopping an echo server
A signal handler can raise a custom exception, allowing the main coroutine to stop without asyncio.run canceling the remaining tasks. The surrounding runner catches that exception, awaits close_echo_tasks, gives each echo task two seconds to finish, and finally closes the loop. section 3.6.2
Listing 3.10 A graceful shutdown
import asyncio
from asyncio import AbstractEventLoop
import socket
import logging
import signal
from typing import List
async def echo(connection: socket,
loop: AbstractEventLoop) -> None:
try:
while data := await loop.sock_recv(connection, 1024):
print('got data!')
if data == b'boom\r\n':
raise Exception("Unexpected network error")
await loop.sock_sendall(connection, data)
except Exception as ex:
logging.exception(ex)
finally:
connection.close()
echo_tasks = []
async def connection_listener(server_socket, loop):
while True:
connection, address = await loop.sock_accept(server_socket)
connection.setblocking(False)
print(f"Got a connection from {address}")
echo_task = asyncio.create_task(echo(connection, loop))
echo_tasks.append(echo_task)
class GracefulExit(SystemExit):
pass
def shutdown():
raise GracefulExit()
async def close_echo_tasks(echo_tasks: List[asyncio.Task]):
waiters = [asyncio.wait_for(task, 2) for task in echo_tasks]
for task in waiters:
try:
await task
except asyncio.exceptions.TimeoutError:
# We expect a timeout error here
pass
async def main():
server_socket = socket.socket()
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_address = ('127.0.0.1', 8000)
server_socket.setblocking(False)
server_socket.bind(server_address)
server_socket.listen()
for signame in {'SIGINT', 'SIGTERM'}:
loop.add_signal_handler(getattr(signal, signame), shutdown)
await connection_listener(server_socket, loop)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(main())
except GracefulExit:
loop.run_until_complete(close_echo_tasks(echo_tasks))
finally:
loop.close()
Inspect a complete graceful-shutdown echo server that combines signal handling, task tracking, exception handling, and connection cleanup. section 3.6.2
