async_drop/lib.rs
1//! Async drop support for Rust.
2//!
3//! This crate provides [`AsyncDrop`], a trait for types that need to perform
4//! asynchronous cleanup, and [`Dropper`], a wrapper that automatically runs
5//! the async cleanup when the value is dropped.
6//!
7//! # Example
8//!
9//! ```
10//! use async_drop::{AsyncDrop, AsyncDropFuture, Dropper};
11//!
12//! struct DbConnection;
13//!
14//! impl AsyncDrop for DbConnection {
15//! fn async_drop(&mut self) -> AsyncDropFuture<'_> {
16//! Box::pin(async {
17//! // Close the connection gracefully...
18//! Ok(())
19//! })
20//! }
21//! }
22//!
23//! let conn = Dropper::new(DbConnection);
24//! // Use `conn` as if it were a `DbConnection` (via Deref).
25//! // When `conn` goes out of scope, `async_drop` runs automatically.
26//! ```
27
28use std::{
29 ops::{Deref, DerefMut},
30 pin::Pin,
31};
32
33/// The future type returned by [`AsyncDrop::async_drop`].
34pub type AsyncDropFuture<'a> = Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
35
36/// A trait for types that require asynchronous cleanup.
37///
38/// Implement this trait to define async teardown logic (e.g. flushing buffers,
39/// closing network connections, or releasing distributed locks). Use
40/// [`Dropper`] to ensure `async_drop` is called automatically when the value
41/// goes out of scope.
42pub trait AsyncDrop {
43 /// Performs asynchronous cleanup of this value.
44 ///
45 /// Returns `Ok(())` on success or `Err` with a message that will be
46 /// printed to stderr.
47 fn async_drop(&mut self) -> AsyncDropFuture<'_>;
48}
49
50/// A wrapper that calls [`AsyncDrop::async_drop`] when dropped.
51///
52/// `Dropper<T>` dereferences to `T`, so the wrapped value can be used
53/// transparently. On drop, it spawns a dedicated thread with a short-lived
54/// Tokio runtime to drive the cleanup future to completion. This avoids
55/// deadlocks when the `Dropper` is dropped inside a single-threaded async
56/// executor.
57///
58/// # Panics
59///
60/// Panics if the internal Tokio runtime cannot be created.
61pub struct Dropper<T>
62where
63 T: AsyncDrop,
64{
65 inner: Option<T>,
66}
67
68impl<T> Dropper<T>
69where
70 T: AsyncDrop,
71{
72 /// Wraps `inner` so that its [`AsyncDrop::async_drop`] implementation is
73 /// called automatically when this `Dropper` is dropped.
74 pub fn new(inner: T) -> Self {
75 Self { inner: Some(inner) }
76 }
77}
78
79impl<T> Drop for Dropper<T>
80where
81 T: AsyncDrop,
82{
83 fn drop(&mut self) {
84 let Some(mut inner) = self.inner.take() else {
85 return;
86 };
87
88 let future = inner.async_drop();
89
90 // Spawn a dedicated thread with its own tokio runtime so we don't
91 // deadlock when dropped inside a single-threaded tokio executor.
92 // Use catch_unwind to handle panics in the async drop and propagate
93 // them to the parent thread.
94 let result = std::thread::scope(|s| {
95 s.spawn(move || {
96 let rt = tokio::runtime::Builder::new_current_thread()
97 .enable_all()
98 .build()
99 .expect("failed to build async-drop runtime");
100
101 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
102 rt.block_on(future)
103 }));
104
105 match result {
106 Ok(inner) => inner.map_err(|e| format!("Async drop error: {}", e)),
107 Err(_) => Err("async_drop panicked".to_owned()),
108 }
109 })
110 .join()
111 .unwrap_or_else(|_| Err("async_drop thread panicked".to_owned()))
112 });
113
114 if let Err(e) = result {
115 panic!("{}", e);
116 }
117 }
118}
119
120impl<T> Deref for Dropper<T>
121where
122 T: AsyncDrop + 'static,
123{
124 type Target = T;
125 fn deref(&self) -> &Self::Target {
126 self.inner
127 .as_ref()
128 .expect("Dropper value has already been dropped")
129 }
130}
131
132impl<T> DerefMut for Dropper<T>
133where
134 T: AsyncDrop + 'static,
135{
136 fn deref_mut(&mut self) -> &mut Self::Target {
137 self.inner
138 .as_mut()
139 .expect("Dropper value has already been dropped")
140 }
141}