345: Async Cleanup and Drop
Difficulty: 4 Level: Expert Run cleanup code before a resource is released โ but Rust's `Drop` is synchronous, so async shutdown needs an explicit contract.The Problem This Solves
Many resources โ database connections, file handles, network sockets โ need graceful shutdown: flush pending writes, send a `FIN`, release a lock. In an async system you'd `await` these teardown steps. But Rust's `Drop` trait is called synchronously by the destructor; you cannot `.await` inside it. This gap creates a real hazard. If you rely on `Drop` for async cleanup, the cleanup silently becomes fire-and-forget at best, or panics at worst. Production codebases that ignore this lose connections, leave locks held, or corrupt state during shutdown. The standard Rust solution is an explicit contract: callers must call `async fn shutdown()` (or `close()`) before dropping the value. The `Drop` implementation serves as a safety net โ it detects the "dropped without shutdown" case and at minimum logs a warning.The Intuition
Think of a database session. Properly closing it sends a goodbye packet so the server reclaims the slot immediately. If the client just disconnects, the server waits for a TCP timeout. `Drop` is the TCP-level disconnect โ it works, but it's ugly. `shutdown()` is the clean goodbye. An RAII guard wraps the resource and calls `shutdown()` automatically in its own `Drop`, so callers who use the guard never have to remember.How It Works in Rust
1. `AsyncConnection` โ wraps a resource plus an `Arc<AtomicBool>` marking whether it is open. 2. `fn shutdown(&mut self)` โ sets the atomic to `false`, runs cleanup. In real async code this would be `async fn shutdown()`. 3. `impl Drop` โ checks if still open; if so, logs a warning and force-closes. This is the fallback, not the happy path. 4. `ConnectionGuard` โ a newtype wrapper whose `Drop` calls `shutdown()`. Callers get clean teardown for free.impl Drop for ConnectionGuard {
fn drop(&mut self) {
self.0.shutdown(); // always closes, even on panic
}
}
The atomic flag ensures `shutdown()` is idempotent โ calling it twice is safe.
What This Unlocks
- Async-safe resource teardown โ explicit `shutdown()` composes with `.await`; `Drop` cannot.
- Leak detection โ the `Drop` fallback makes "dropped without cleanup" visible as a warning instead of a silent bug.
- RAII guards โ scope-based cleanup works for sync shutdown; pair with `drop(guard)` when you need early exit.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Destructor hook | GC finalizer (non-deterministic) | `impl Drop` (deterministic, sync) |
| Async cleanup | GC defers, Lwt.finalize | Explicit `async fn shutdown()` before drop |
| RAII guard | No RAII idiom | `struct Guard; impl Drop { shutdown() }` |
| Forced cleanup | GC eventually calls finalizer | `Drop` runs when value goes out of scope |
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
// Connection that MUST be explicitly closed (async-style)
struct AsyncConnection {
id: u32,
open: Arc<AtomicBool>,
}
impl AsyncConnection {
fn open(id: u32) -> Self {
println!("Opening connection {id}");
Self { id, open: Arc::new(AtomicBool::new(true)) }
}
fn execute(&self, query: &str) -> Result<String, String> {
if !self.open.load(Ordering::Acquire) {
return Err("connection is closed".into());
}
Ok(format!("result of [{query}] on conn {}", self.id))
}
// Explicit async shutdown โ call BEFORE dropping
// In real async code: async fn shutdown(&mut self)
fn shutdown(&mut self) {
if self.open.swap(false, Ordering::Release) {
println!("Connection {} shut down cleanly", self.id);
}
}
}
impl Drop for AsyncConnection {
fn drop(&mut self) {
// Synchronous fallback: warn if not shut down properly
if self.open.load(Ordering::Acquire) {
eprintln!("WARNING: Connection {} dropped without shutdown!", self.id);
// In production: log, record telemetry, or leak the connection
self.open.store(false, Ordering::Release);
}
}
}
// RAII scope guard
struct ConnectionGuard(AsyncConnection);
impl ConnectionGuard {
fn new(id: u32) -> Self {
Self(AsyncConnection::open(id))
}
fn conn(&self) -> &AsyncConnection { &self.0 }
}
impl Drop for ConnectionGuard {
fn drop(&mut self) {
self.0.shutdown(); // always closes
}
}
fn main() {
// Explicit shutdown
let mut conn = AsyncConnection::open(1);
println!("{}", conn.execute("SELECT 1").unwrap());
conn.shutdown(); // explicit
drop(conn); // Drop impl sees it's already closed
// RAII guard โ shutdown on scope exit
{
let guard = ConnectionGuard::new(2);
println!("{}", guard.conn().execute("SELECT 2").unwrap());
} // guard drops here, shutdown called
// Forgot to shutdown โ Drop warns
let _conn = AsyncConnection::open(3);
// drop happens here, warning printed
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn execute_before_shutdown() {
let conn = AsyncConnection::open(10);
assert!(conn.execute("q").is_ok());
}
#[test]
fn execute_after_shutdown() {
let mut conn = AsyncConnection::open(11);
conn.shutdown();
assert!(conn.execute("q").is_err());
}
#[test]
fn guard_shuts_down_on_drop() {
let id = 12;
{
let mut g = ConnectionGuard::new(id);
g.0.execute("x").unwrap();
}
// If we get here without panic, shutdown worked
}
}
(* OCaml: resource cleanup with finalizers *)
type connection = {
id : int;
mutable open_ : bool;
}
let open_connection id =
Printf.printf "Opening connection %d\n" id;
{ id; open_ = true }
let close_connection conn =
if conn.open_ then begin
Printf.printf "Closing connection %d\n" conn.id;
conn.open_ <- false
end
let with_connection id f =
let conn = open_connection id in
let result = (try Ok (f conn) with e -> Error e) in
close_connection conn;
match result with
| Ok v -> v
| Error e -> raise e
let () =
with_connection 1 (fun conn ->
Printf.printf "Using connection %d\n" conn.id;
"done"
) |> ignore