Graceful Shutdown and Resource Cleanup

Proper shutdown handling ensures your async applications clean up resources gracefully and don’t lose data. Let’s explore shutdown patterns and cleanup strategies.

Signal Handling

Handle shutdown signals properly:

import asyncio
import signal
import logging
from typing import Set

class GracefulShutdown:
    def __init__(self):
        self.shutdown_event = asyncio.Event()
        self.running_tasks: Set[asyncio.Task] = set()
        self.cleanup_callbacks = []

The shutdown handler tracks running tasks and cleanup callbacks. The event signals when shutdown should begin.

Set up signal handlers to catch termination signals:

    def setup_signal_handlers(self):
        """Setup signal handlers for graceful shutdown"""
        for sig in (signal.SIGTERM, signal.SIGINT):
            signal.signal(sig, self._signal_handler)
    
    def _signal_handler(self, signum, frame):
        """Handle shutdown signals"""
        logging.info(f"Received signal {signum}, initiating graceful shutdown...")
        self.shutdown_event.set()
    
    async def wait_for_shutdown(self):
        """Wait for shutdown signal"""
        await self.shutdown_event.wait()

SIGTERM comes from process managers like systemd, while SIGINT comes from Ctrl+C. Both trigger the same graceful shutdown process.

Add cleanup callbacks and manage tasks:

    def add_cleanup_callback(self, callback):
        """Add cleanup callback to be called during shutdown"""
        self.cleanup_callbacks.append(callback)
    
    async def cleanup(self):
        """Perform cleanup operations"""
        logging.info("Starting cleanup process...")
        
        # Cancel running tasks
        if self.running_tasks:
            logging.info(f"Cancelling {len(self.running_tasks)} running tasks...")
            for task in self.running_tasks:
                task.cancel()
            
            # Wait for tasks to complete cancellation
            await asyncio.gather(*self.running_tasks, return_exceptions=True)
        
        # Run cleanup callbacks
        for callback in self.cleanup_callbacks:
            try:
                if asyncio.iscoroutinefunction(callback):
                    await callback()
                else:
                    callback()
            except Exception as e:
                logging.error(f"Error in cleanup callback: {e}")
        
        logging.info("Cleanup completed")

# Global shutdown handler
shutdown_handler = GracefulShutdown()

FastAPI Graceful Shutdown

Implement graceful shutdown in FastAPI applications:

from fastapi import FastAPI
from contextlib import asynccontextmanager
import asyncio
import logging

# Application state
app_state = {
    "database_pool": None,
    "redis_connection": None,
    "background_tasks": set()
}

FastAPI’s lifespan context manager handles startup and shutdown events. We track resources in app_state for proper cleanup.

The lifespan function manages the application lifecycle:

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    logging.info("Starting application...")
    
    # Initialize resources
    app_state["database_pool"] = await create_database_pool()
    app_state["redis_connection"] = await create_redis_connection()
    
    # Start background tasks
    task = asyncio.create_task(background_worker())
    app_state["background_tasks"].add(task)
    
    yield
    
    # Shutdown
    logging.info("Shutting down application...")
    
    # Cancel background tasks
    for task in app_state["background_tasks"]:
        task.cancel()
    
    await asyncio.gather(*app_state["background_tasks"], return_exceptions=True)
    
    # Close connections
    if app_state["database_pool"]:
        await app_state["database_pool"].close()
    
    if app_state["redis_connection"]:
        await app_state["redis_connection"].close()

app = FastAPI(lifespan=lifespan)

Everything before yield runs at startup, everything after runs at shutdown. This ensures proper resource management throughout the application lifecycle.

Background tasks need to handle cancellation gracefully:

async def background_worker():
    """Background task that needs graceful shutdown"""
    try:
        while True:
            await asyncio.sleep(1)
            logging.info("Background worker running...")
    except asyncio.CancelledError:
        logging.info("Background worker cancelled, cleaning up...")
        raise

Database Connection Cleanup

Handle database connections properly:

import asyncio
import asyncpg
from contextlib import asynccontextmanager

class DatabaseManager:
    def __init__(self, database_url: str):
        self.database_url = database_url
        self.pool = None
    
    async def initialize(self):
        """Initialize database pool"""
        self.pool = await asyncpg.create_pool(
            self.database_url,
            min_size=5,
            max_size=20
        )

Connection pools are essential for async applications. They manage multiple database connections efficiently and handle connection lifecycle automatically.

Provide safe connection access with automatic cleanup:

    @asynccontextmanager
    async def get_connection(self):
        """Get database connection with automatic cleanup"""
        connection = await self.pool.acquire()
        try:
            yield connection
        finally:
            await self.pool.release(connection)

The context manager ensures connections are always returned to the pool, even if exceptions occur during database operations.

Handle shutdown cleanup:

    async def close_all_connections(self):
        """Close all database connections"""
        if self.pool:
            await self.pool.close()
            logging.info("Database pool closed")

# Usage
db_manager = DatabaseManager("postgresql://user:pass@localhost/db")
shutdown_handler.add_cleanup_callback(db_manager.close_all_connections)

File and Resource Cleanup

Clean up files and other resources:

import asyncio
import aiofiles
import tempfile
import os

class ResourceManager:
    def __init__(self):
        self.temp_files = set()
        self.open_files = set()

Track temporary files and open file handles to ensure they’re cleaned up during shutdown.

Create and track temporary files:

    async def create_temp_file(self, suffix=".tmp"):
        """Create temporary file with automatic cleanup"""
        temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
        temp_file.close()
        self.temp_files.add(temp_file.name)
        return temp_file.name
    
    async def open_file(self, filename: str, mode: str = 'r'):
        """Open file with tracking for cleanup"""
        file_handle = await aiofiles.open(filename, mode)
        self.open_files.add(file_handle)
        return file_handle

By tracking file operations, we can ensure proper cleanup even if the application terminates unexpectedly.

Perform comprehensive resource cleanup:

    async def cleanup_resources(self):
        """Clean up all managed resources"""
        # Close open files
        for file_handle in list(self.open_files):
            try:
                await file_handle.close()
            except Exception as e:
                logging.error(f"Error closing file: {e}")
        
        # Remove temporary files
        for temp_file in self.temp_files:
            try:
                if os.path.exists(temp_file):
                    os.unlink(temp_file)
            except Exception as e:
                logging.error(f"Error removing temp file: {e}")

# Global resource manager
resource_manager = ResourceManager()
shutdown_handler.add_cleanup_callback(resource_manager.cleanup_resources)

Best Practices

Key principles for graceful shutdown:

Signal Handling:

  • Handle SIGTERM and SIGINT signals
  • Set shutdown flags instead of immediate exit
  • Log shutdown initiation

Task Management:

  • Track all running tasks
  • Cancel tasks gracefully
  • Wait for task completion with timeout

Resource Cleanup:

  • Close database connections
  • Clean up temporary files
  • Release network connections
  • Save application state if needed

Error Handling:

  • Handle cleanup errors gracefully
  • Log cleanup progress
  • Don’t let cleanup errors prevent shutdown

Summary

Graceful shutdown essentials:

  • Implement proper signal handling for shutdown events
  • Track and cancel running tasks during shutdown
  • Clean up resources (databases, files, connections) properly
  • Use FastAPI lifespan events for web applications
  • Log shutdown progress for debugging
  • Handle cleanup errors without blocking shutdown

Proper shutdown handling ensures data integrity and resource cleanup in production async applications.

In Part 21, we’ll explore containerization and deployment strategies.