maybe_once/
blocking.rs

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