ExamplesBy LevelBy TopicLearning Paths
345 Advanced

345: Async Drop

Functional Programming

Tutorial

The Problem

Resource cleanup (closing files, flushing buffers, notifying peers) often requires async operations — but Rust's Drop trait is synchronous. If an async task holding a database connection is cancelled, its Drop runs synchronously on the async runtime, potentially blocking the executor thread. This mismatch is a known pain point: Rust doesn't yet have AsyncDrop in stable (RFC 3541 is in progress). The workaround is RAII guards with synchronous Drop that signal cleanup flags, deferring actual async cleanup to explicit close() methods or defer!-like patterns that run before the future is abandoned.

🎯 Learning Outcomes

  • • Implement Drop to run cleanup logic when a value goes out of scope
  • • Use Arc<AtomicBool> as a cleanup witness to verify Drop ran
  • • Implement RAII guards with disarm() to skip cleanup on success paths
  • • Understand why Drop cannot be async and the implications for async code
  • • Use the guard pattern to ensure cleanup even on panics or early returns
  • • Recognize where explicit close() methods are necessary for async cleanup
  • Code Example

    #![allow(clippy::all)]
    //! # Async Drop
    //! Cleanup resources when async tasks are cancelled or complete.
    
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    
    pub struct Resource {
        id: usize,
        cleaned_up: Arc<AtomicBool>,
    }
    
    impl Resource {
        pub fn new(id: usize) -> (Self, Arc<AtomicBool>) {
            let flag = Arc::new(AtomicBool::new(false));
            (
                Self {
                    id,
                    cleaned_up: Arc::clone(&flag),
                },
                flag,
            )
        }
        pub fn id(&self) -> usize {
            self.id
        }
    }
    
    impl Drop for Resource {
        fn drop(&mut self) {
            self.cleaned_up.store(true, Ordering::SeqCst);
        }
    }
    
    pub struct Guard<F: FnOnce()> {
        cleanup: Option<F>,
    }
    
    impl<F: FnOnce()> Guard<F> {
        pub fn new(cleanup: F) -> Self {
            Self {
                cleanup: Some(cleanup),
            }
        }
        pub fn disarm(mut self) {
            self.cleanup = None;
        }
    }
    
    impl<F: FnOnce()> Drop for Guard<F> {
        fn drop(&mut self) {
            if let Some(f) = self.cleanup.take() {
                f();
            }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn resource_cleanup_on_drop() {
            let flag;
            {
                let (r, f) = Resource::new(1);
                flag = f;
                assert_eq!(r.id(), 1);
            }
            assert!(flag.load(Ordering::SeqCst));
        }
        #[test]
        fn guard_runs_cleanup() {
            let called = Arc::new(AtomicBool::new(false));
            let c = Arc::clone(&called);
            {
                let _g = Guard::new(move || c.store(true, Ordering::SeqCst));
            }
            assert!(called.load(Ordering::SeqCst));
        }
    }

    Key Differences

    AspectRust DropOCaml Fun.protect
    TriggerAutomatic when value leaves scopeMust explicitly wrap with protect
    Async cleanupNot supported in DropLwt.finalize handles async
    Panic safetyDrop runs even on panic (usually)finally runs even on exception
    Zero-costYes — no runtime overheadMinor overhead for exception handling
    Guard patternOption<F> + disarm()Return value or flag from finally

    OCaml Approach

    OCaml lacks RAII (no destructors). Cleanup is managed explicitly through Fun.protect:

    let with_resource id f =
      let cleaned_up = ref false in
      Fun.protect
        ~finally:(fun () -> cleaned_up := true)
        (fun () -> f id)
    

    Fun.protect ~finally guarantees finally runs even if f raises an exception — the functional equivalent of RAII. For Lwt async cleanup: Lwt.finalize runs a cleanup promise whether the main promise succeeds or fails.

    Full Source

    #![allow(clippy::all)]
    //! # Async Drop
    //! Cleanup resources when async tasks are cancelled or complete.
    
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    
    pub struct Resource {
        id: usize,
        cleaned_up: Arc<AtomicBool>,
    }
    
    impl Resource {
        pub fn new(id: usize) -> (Self, Arc<AtomicBool>) {
            let flag = Arc::new(AtomicBool::new(false));
            (
                Self {
                    id,
                    cleaned_up: Arc::clone(&flag),
                },
                flag,
            )
        }
        pub fn id(&self) -> usize {
            self.id
        }
    }
    
    impl Drop for Resource {
        fn drop(&mut self) {
            self.cleaned_up.store(true, Ordering::SeqCst);
        }
    }
    
    pub struct Guard<F: FnOnce()> {
        cleanup: Option<F>,
    }
    
    impl<F: FnOnce()> Guard<F> {
        pub fn new(cleanup: F) -> Self {
            Self {
                cleanup: Some(cleanup),
            }
        }
        pub fn disarm(mut self) {
            self.cleanup = None;
        }
    }
    
    impl<F: FnOnce()> Drop for Guard<F> {
        fn drop(&mut self) {
            if let Some(f) = self.cleanup.take() {
                f();
            }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn resource_cleanup_on_drop() {
            let flag;
            {
                let (r, f) = Resource::new(1);
                flag = f;
                assert_eq!(r.id(), 1);
            }
            assert!(flag.load(Ordering::SeqCst));
        }
        #[test]
        fn guard_runs_cleanup() {
            let called = Arc::new(AtomicBool::new(false));
            let c = Arc::clone(&called);
            {
                let _g = Guard::new(move || c.store(true, Ordering::SeqCst));
            }
            assert!(called.load(Ordering::SeqCst));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn resource_cleanup_on_drop() {
            let flag;
            {
                let (r, f) = Resource::new(1);
                flag = f;
                assert_eq!(r.id(), 1);
            }
            assert!(flag.load(Ordering::SeqCst));
        }
        #[test]
        fn guard_runs_cleanup() {
            let called = Arc::new(AtomicBool::new(false));
            let c = Arc::clone(&called);
            {
                let _g = Guard::new(move || c.store(true, Ordering::SeqCst));
            }
            assert!(called.load(Ordering::SeqCst));
        }
    }

    Deep Comparison

    OCaml vs Rust: Async Drop

    Overview

    See the example.rs and example.ml files for detailed implementations.

    Key Differences

    AspectOCamlRust
    Type systemHindley-MilnerOwnership + traits
    MemoryGCZero-cost abstractions
    MutabilityExplicit refmut keyword
    Error handlingOption/ResultResult<T, E>

    See README.md for detailed comparison.

    Exercises

  • File flush guard: Implement a FlushGuard that wraps a BufWriter<File> and calls flush() in Drop; verify that partial writes are flushed even if the function panics midway.
  • Disarm test: Write a test that creates a Guard with a counter, calls disarm(), lets it drop, and verifies the counter wasn't incremented; then write a complementary test without disarm().
  • Async cleanup workaround: In a Tokio context, implement a Resource that has a close(self) -> impl Future method for async cleanup; wrap it in a sync Drop that logs a warning if close() was never called.
  • Open Source Repos