maybe_once/
tokio.rs

1use std::ops::Deref;
2use std::sync::Arc;
3
4use core::pin::Pin;
5use log::info;
6use parking_lot::{Mutex, RwLock};
7use std::future::Future;
8use tokio::sync::{RwLock as AsyncRwLock, RwLockReadGuard, RwLockWriteGuard};
9
10/// A `MaybeOnce` object is a variation of the `OnceLock` object that keeps track of the number of references to the internal data
11/// and drops it every time the references counter goes to 0. When the data is accessed,
12/// it will be created if it does not exist, or it will recreated if it was previously dropped.
13///
14/// This object is to be used to inizialize a shared resource that must be dropped when it is no longer used or when
15/// the process terminates. This mechanism is used as a workaround for the fact that rust does not drop static items at the end of the program.
16///
17/// A typical example is to initialize an expensive object, for example to start a docker container, to be used by a set of integration tests,
18/// with the guarantee that it is properly dropped when the tests terminates.
19///
20/// Please note that, if this object is used in a single thread context, then it will drop the data after each access.
21/// This is caused by the fact that the internal reference counter will always be 0 once the data is dropped.
22///
23/// Example:
24/// ```rust
25/// mod test {
26///
27///     use std::sync::OnceLock;
28///     use maybe_once::tokio::{Data, MaybeOnceAsync};
29///
30///
31/// /// A data initializer function. This can be called more than once.
32/// /// If everything goes as expected, it should only be called once.
33/// async fn init() -> String {
34///     // Expensive initialization logic here.
35///     // For example, you can start here a docker container (e.g. by using testcontainers),
36///     // when there will be no more references to the data, 
37///     // the data will be dropped and the container will be stopped.
38///     "hello".to_string()    
39/// }
40///
41/// /// A function that holds a static reference to the `MaybeOnceAsync` object 
42/// /// and returns a `Data` object.
43/// pub async fn data(serial: bool) -> Data<'static, String> {
44///     static DATA: OnceLock<MaybeOnceAsync<String>> = OnceLock::new();
45///     DATA.get_or_init(|| MaybeOnceAsync::new(|| Box::pin(init())))
46///         .data(serial)
47///         .await
48/// }
49///
50///     /// This test, and all the others, uses the data function to access the shared data.
51///     /// The same data instance is shared between all the threads exactly like OnceLock does,
52///     /// but when the all tests finish, the data will be dropped before the process terminates.
53///     #[tokio::test]
54///     async fn test1() {
55///         let data = data(false).await;
56///         println!("{}", *data);
57///     }
58///
59///     #[tokio::test]
60///     async fn test2() {
61///         let data = data(false).await;
62///         println!("{}", *data);
63///     }
64///
65///     #[tokio::test]
66///     async fn test3() {
67///         let data = data(false).await;
68///         println!("{}", *data);
69///     }
70///
71/// }
72/// ```
73pub struct MaybeOnceAsync<T> {
74    data: Arc<RwLock<Option<Arc<T>>>>,
75    lock_mutex: Arc<AsyncRwLock<()>>,
76    init: fn() -> Pin<Box<dyn Send + Future<Output = T>>>,
77    callers: Arc<Mutex<usize>>,
78}
79
80impl<T> MaybeOnceAsync<T> {
81    /// Creates a new `MaybeOnceAsync` object with the given `init` function.
82    ///
83    /// `init` is a function that creates a new `T` object. It is lazily called the first time
84    /// `data` is called and every time after the data is dropped.
85    ///
86    /// The returned `MaybeOnceAsync` object is then used to access the shared data with the
87    /// `data` method.
88    pub fn new(init: fn() -> Pin<Box<dyn Send + Future<Output = T>>>) -> Self {
89        MaybeOnceAsync {
90            data: Arc::new(RwLock::new(None)),
91            init,
92            lock_mutex: Arc::new(AsyncRwLock::new(())),
93            callers: Arc::new(Mutex::new(0)),
94        }
95    }
96
97    /// This function returns a `Data` object, which allows you to access the shared data.
98    ///
99    /// The `serial` parameter allows you to control whether the data is accessed in a serial
100    /// or parallel manner. If `serial` is `true`, the data will be accessed in a serial manner,
101    /// meaning that no other thread can access the data until the returned `Data` is dropped.
102    /// If `serial` is `false`, the data will be accessed in a parallel manner, meaning that
103    /// any number of threads can access the data at the same time.
104    ///
105    /// The returned `Data` object implements `Deref` and `AsRef`, so you can use it like a reference.
106    ///
107    /// The `Data` object also implements `Drop`, so when it goes out of scope, the lock is released.
108    pub async fn data(&self, serial: bool) -> Data<'_, T> {
109        {
110            let mut lock = self.callers.lock();
111            let callers = *lock + 1;
112            *lock = callers;
113        }
114
115        let data_arc = {
116            let is_none = { self.data.read().is_none() };
117
118            if is_none {
119                let _lock_mutex = self.lock_mutex.write().await;
120
121                let is_none = { self.data.read().is_none() };
122
123                if is_none {
124                    let init = { (self.init)().await };
125                    let mut write_lock = self.data.write();
126                    if write_lock.is_none() {
127                        *write_lock = Some(Arc::new(init));
128                    }
129                }
130            };
131
132            let lock = self.data.read();
133
134            match lock.as_ref() {
135                Some(data) => data.clone(),
136                None => panic!("There should always be data here!"),
137            }
138        };
139
140        let (read_lock, write_lock) = if serial {
141            (None, Some(self.lock_mutex.write().await))
142        } else {
143            (Some(self.lock_mutex.read().await), None)
144        };
145
146        Data {
147            data_arc,
148            data: self.data.clone(),
149            callers: self.callers.clone(),
150            read_lock,
151            write_lock,
152        }
153    }
154}
155
156/// A struct that allows you to access the shared data.
157pub struct Data<'a, T> {
158    data_arc: Arc<T>,
159    data: Arc<RwLock<Option<Arc<T>>>>,
160    #[allow(dead_code)]
161    read_lock: Option<RwLockReadGuard<'a, ()>>,
162    #[allow(dead_code)]
163    write_lock: Option<RwLockWriteGuard<'a, ()>>,
164    callers: Arc<Mutex<usize>>,
165}
166
167impl<T> Drop for Data<'_, T> {
168    fn drop(&mut self) {
169        let mut lock = self.callers.lock();
170        // Here the lock cannot be less than 1
171        let callers = *lock - 1;
172        *lock = callers;
173
174        if callers == 0 {
175            info!("MaybeOnceAsync --- Dropping DATA ---");
176            let mut data = self.data.write();
177            *data = None;
178        }
179    }
180}
181
182impl<T> Deref for Data<'_, T> {
183    type Target = T;
184
185    fn deref(&self) -> &Self::Target {
186        self.data_arc.as_ref()
187    }
188}
189
190impl<T> AsRef<T> for Data<'_, T> {
191    fn as_ref(&self) -> &T {
192        self.data_arc.as_ref()
193    }
194}
195
196#[cfg(test)]
197mod test {
198
199    use super::*;
200    use rand::random_range;
201    use std::time::Duration;
202    use tokio::sync::Mutex as AsyncMutex;
203
204    #[test]
205    fn maybe_should_be_send() {
206        let maybe = MaybeOnceAsync::new(|| Box::pin(async {}));
207        need_send(maybe);
208    }
209
210    fn need_send<T: Send>(_t: T) {}
211    fn need_sync<T: Sync>(_t: T) {}
212
213    #[test]
214    fn maybe_should_be_sync() {
215        let maybe = MaybeOnceAsync::new(|| Box::pin(async {}));
216        need_sync(maybe);
217    }
218
219    #[tokio::test]
220    async fn async_should_execute_in_parallel() {
221        let maybe = MaybeOnceAsync::new(|| Box::pin(async {}));
222        let maybe = Arc::new(maybe);
223
224        let responses = Arc::new(AsyncMutex::new(vec![]));
225
226        let mut handles = vec![];
227
228        for i in 0..100 {
229            let maybe = maybe.clone();
230            let responses = responses.clone();
231            let sleep_for = random_range(0..1000);
232            handles.push(tokio::spawn(async move {
233                let _data = maybe.data(false).await;
234                assert!(maybe.data.read().is_some());
235                println!(" exec {} start", i);
236                tokio::time::sleep(Duration::from_nanos(sleep_for)).await;
237                println!(" exec {} end", i);
238                let mut responses_lock = responses.lock().await;
239                responses_lock.push(i);
240            }));
241        }
242
243        for handle in handles {
244            let _s = handle.await; // maybe consider handling errors propagated from the thread here
245        }
246
247        let responses_lock = responses.lock().await;
248        assert_eq!(100, responses_lock.len());
249
250        assert!(maybe.data.read().is_none());
251    }
252
253    #[tokio::test]
254    async fn async_should_execute_serially() {
255        let maybe = MaybeOnceAsync::new(|| Box::pin(async {}));
256        let maybe = Arc::new(maybe);
257
258        let responses = Arc::new(AsyncMutex::new(vec![]));
259
260        let mut handles = vec![];
261
262        for i in 0..100 {
263            let maybe = maybe.clone();
264            let responses = responses.clone();
265            let sleep_for = random_range(0..10);
266
267            handles.push(tokio::spawn(async move {
268                let _data = maybe.data(true).await;
269                assert!(maybe.data.read().is_some());
270                println!(" exec {} start", i);
271                tokio::time::sleep(Duration::from_nanos(sleep_for)).await;
272                println!(" exec {} end", i);
273                let mut responses_lock = responses.lock().await;
274                responses_lock.push(i);
275            }));
276        }
277
278        for handle in handles {
279            let _ = handle.await; // maybe consider handling errors propagated from the thread here
280        }
281
282        let responses_lock = responses.lock().await;
283        assert_eq!(100, responses_lock.len());
284
285        assert!(maybe.data.read().is_none());
286    }
287}