๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
Destructor hookGC finalizer (non-deterministic)`impl Drop` (deterministic, sync)
Async cleanupGC defers, Lwt.finalizeExplicit `async fn shutdown()` before drop
RAII guardNo RAII idiom`struct Guard; impl Drop { shutdown() }`
Forced cleanupGC 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