Course contentsShow
← Rust
Lesson 1306 of 1,91011. Asynchronous ProgrammingFree lesson

Handling Graceful Shutdowns

Implement a reliable shutdown sequence for complex async systems that respects in-flight requests.

In this lesson, you will learn how to orchestrate a clean shutdown of an asynchronous system by ensuring in-flight requests complete before the runtime terminates.

The Challenge of In-Flight Requests

When an application receives a termination signal (like SIGTERM), simply dropping the Tokio runtime or exiting the process immediately can lead to data corruption or lost work. Tasks currently awaiting a database write or a network response are abruptly killed. A graceful shutdown requires a coordination mechanism to signal to all tasks that they should stop accepting new work and finish what they have already started.

The Coordination Pattern: CancellationToken

The most reliable way to implement this is using a "broadcast-style" shutdown signal combined with a synchronization primitive to track active tasks.

  1. The Signal: Use a tokio_util::sync::CancellationToken. When you call cancel(), every task listening to this token receives the signal.
  2. The Tracker: Use a tokio::sync::mpsc channel or a Semaphore to keep count of active workers.

Here is a simplified pattern for a worker loop:

async fn worker(token: CancellationToken, mut rx: Receiver<Job>) {
    loop {
        tokio::select! {
            _ = token.cancelled() => break, // Gracefully exit
            job = rx.recv() => {
                if let Some(j) = job {
                    process(j).await;
                }
            }
        }
    }
    // Perform cleanup here
}

Ensuring Completion

To ensure all tasks finish, you can use a TaskTracker (from the tokio-util crate). By spawning tasks with a tracker, you can call .wait() on the tracker after the shutdown signal has been sent. This blocks the main thread from exiting until all spawned futures have completed their execution.

By combining the CancellationToken (to stop new work) with a TaskTracker (to wait for current work), you transition your system from "crashing on exit" to "cooperatively finishing its duties." This ensures that partial writes are avoided and your system maintains integrity during deployments or scaling events.

Key Takeaway: A graceful shutdown requires a two-part strategy: broadcasting a cancellation signal to stop new work and awaiting a synchronization primitive to ensure all in-flight tasks have finished their final operations.