liveBook · Manning

Shutdown logic

As explained in Python Concurrency with asyncio

Shutdown logic is application behavior that coordinates stopping work and releasing resources when an application is terminated.

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

How Python Concurrency with asyncio covers it

How deep it goes
Explains signal registration, task cancellation, timeout waiting, exception control flow, and loop closure; stops short of production-ready cross-platform handling.
Examples
Uses Ctrl-C, kill commands, delayed coroutines, and a concurrent socket echo server with pending client tasks.
What it assumes
Assumes familiarity with asyncio tasks, coroutines, event loops, and socket-based echo servers.
What you can do after
Operate an asyncio application that responds to Unix termination signals and gives pending tasks a bounded completion period.
How it connects
Connects Unix signals, the asyncio event loop, tasks, cancellation, wait_for, TimeoutError, and resource cleanup.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Python Concurrency with asyncio. The section links point to the parts of the book it draws on.