Graceful shutdown and testing
Shut down a Tokio TCP server gracefully and test async code with controlled time. Examples cover cancellation and task cleanup.
Graceful TCP shutdown#
Stop accepting, request cancellation, and wait for active connections before exiting.
Ctrl+C cancels a shared token. The listener checks cancellation while waiting for a connection slot or socket, and the task tracker waits for active connections. Add tokio-util as shown in Cancellation and task lifetimes.
use std::{sync::Arc, time::Duration};
use tokio::{io::{self, AsyncWriteExt}, net::TcpListener, sync::Semaphore};
use tokio_util::{sync::CancellationToken, task::TaskTracker};
#[tokio::main]
async fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:7003").await?;
let stop = CancellationToken::new();
let tracker = TaskTracker::new();
let slots = Arc::new(Semaphore::new(100));
let on_signal = stop.clone();
tokio::spawn(async move {
if let Err(error) = tokio::signal::ctrl_c().await {
eprintln!("signal: {error}");
}
on_signal.cancel();
});
loop {
let permit = tokio::select! {
_ = stop.cancelled() => break,
result = Arc::clone(&slots).acquire_owned() =>
result.expect("semaphore is open"),
};
let stream = tokio::select! {
_ = stop.cancelled() => break,
accepted = listener.accept() => accepted?.0,
};
let child_stop = stop.clone();
tracker.spawn(async move {
let _permit = permit;
let (mut read, mut write) = stream.into_split();
tokio::select! {
_ = child_stop.cancelled() => {
let _ = write.shutdown().await;
}
result = io::copy(&mut read, &mut write) => {
if let Err(error) = result { eprintln!("client: {error}"); }
}
}
});
}
stop.cancel();
tracker.close();
if tokio::time::timeout(Duration::from_secs(5), tracker.wait())
.await.is_err()
{
eprintln!("connections did not close before the deadline");
}
Ok(())
}main drops the runtime and cancels remaining tasks. If clients need to finish critical writes, track that state and choose a longer deadline.Async tests and controlled time#
Use #[tokio::test] to create a runtime per test. Pause virtual time to test timers quickly and deterministically.
Add test-util to the Tokio features for start_paused and advance. Tests can use temporary loopback ports (127.0.0.1:0) to avoid collisions; assert protocol behavior rather than relying on sleep timing.
[dev-dependencies]
tokio = { version = "1", features = ["full", "test-util"] }use std::time::Duration;
#[tokio::test(start_paused = true)]
async fn retries_wait_before_repeating() {
let task = tokio::spawn(async {
tokio::time::sleep(Duration::from_secs(10)).await;
42
});
tokio::task::yield_now().await; // register the sleep
tokio::time::advance(Duration::from_secs(10)).await;
assert_eq!(task.await.unwrap(), 42);
}use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}};
#[tokio::test]
async fn echo_round_trip() -> std::io::Result<()> {
let server = TcpListener::bind("127.0.0.1:0").await?;
let addr = server.local_addr()?;
let task = tokio::spawn(async move {
let (mut socket, _) = server.accept().await.unwrap();
let mut bytes = [0u8; 4];
socket.read_exact(&mut bytes).await.unwrap();
socket.write_all(&bytes).await.unwrap();
});
let mut client = TcpStream::connect(addr).await?;
client.write_all(b"ping").await?;
let mut reply = [0u8; 4];
client.read_exact(&mut reply).await?;
assert_eq!(&reply, b"ping");
task.await.unwrap();
Ok(())
}cargo test
cargo test echo_round_trip -- --nocaptureadvance() jumps the clock; let spawned tasks register their timers first, as the yield above does.Help improve CodeRef
Allow Google Analytics to measure page visits and use of the guides? It uses cookies after you accept. Your choice won’t affect access to the site. Privacy notice