Skip to main content

imago/storage/
drivers.rs

1//! Internal functionality for storage drivers.
2
3use crate::misc_helpers::Overlaps;
4#[cfg(feature = "async")]
5use futures::stream::{FuturesUnordered, StreamExt};
6use maybe_async::maybe_async;
7use std::ops::Range;
8use std::sync::atomic::{AtomicUsize, Ordering};
9#[cfg(feature = "sync")]
10use std::sync::mpsc::{self, Sender};
11use std::sync::Arc;
12#[cfg(feature = "async")]
13use tokio::sync::oneshot::{self, Sender};
14
15/// Helper object for the [`StorageExt`](crate::StorageExt) implementation.
16///
17/// State such as write blockers needs to be kept somewhere, and instead of introducing a wrapper
18/// (that might be bypassed), we store it directly in the [`Storage`](crate::Storage) objects so it
19/// cannot be bypassed (at least when using the [`StorageExt`](crate::StorageExt) methods).
20///
21/// Async note: Overlapping write blockers from different async tasks are safe (the awaiting task
22/// yields, letting the holding task make progress).  Same-task overlap still deadlocks, as the
23/// task cannot drop its guard while suspended.
24///
25/// Sync note: Unlike in async mode, write blocker acquisition must not be nested on the same
26/// thread.  Acquiring a blocker while already holding an overlapping one will deadlock on
27/// `recv()`, because the existing blocker can only be released by the same (now blocked) thread.
28#[derive(Debug, Default)]
29pub struct CommonStorageHelper {
30    /// Current in-flight write that allow concurrent writes to the same region.
31    ///
32    /// Normal non-async RwLock, so do not await while locked!
33    weak_write_blockers: std::sync::RwLock<RangeBlockedList>,
34
35    /// Current in-flight write that do not allow concurrent writes to the same region.
36    strong_write_blockers: std::sync::RwLock<RangeBlockedList>,
37}
38
39/// A list of ranges blocked for some kind of concurrent access.
40///
41/// Depending on the use, some will block all concurrent access (i.e. serializing writes will block
42/// both serializing and non-serializing writes (strong blockers)), while others will only block a
43/// subset (non-serializing writes will only block serializing writes (weak blockers)).
44#[derive(Debug, Default)]
45struct RangeBlockedList {
46    /// The list of ranges.
47    ///
48    /// Serializing writes (strong write blockers) are supposed to be rare, so it is important that
49    /// entering and removing items into/from this list is cheap, not that iterating it is.
50    blocked: Vec<Arc<RangeBlocked>>,
51}
52
53/// A range blocked for some kind of concurrent access.
54#[derive(Debug)]
55struct RangeBlocked {
56    /// The range.
57    range: Range<u64>,
58
59    /// List of requests awaiting the range to become unblocked.
60    ///
61    /// When the corresponding `RangeBlockedGuard` is dropped, these will all be awoken (via
62    /// `oneshot::Sender::send(())`).
63    ///
64    /// Normal non-async mutex, so do not await while locked!
65    waitlist: std::sync::Mutex<Vec<Sender<()>>>,
66
67    /// Index in the corresponding `RangeBlockedList.blocked` list, so it can be dropped quickly.
68    ///
69    /// (When the corresponding `RangeBlockedGuard` is dropped, this entry is swap-removed from the
70    /// `blocked` list, and the other entry taking its place has its `index` updated.)
71    ///
72    /// Only access under `blocked` lock!
73    index: AtomicUsize,
74
75    /// For debugging only: Thread that created this blocker, for reentrancy detection.
76    #[cfg(all(feature = "sync", debug_assertions))]
77    owner_thread: std::thread::ThreadId,
78}
79
80/// Keeps a `RangeBlocked` alive.
81///
82/// When dropped, removes the `RangeBlocked` from its list, and wakes all requests in the `waitlist`.
83#[derive(Debug)]
84pub struct RangeBlockedGuard<'a> {
85    /// List where this blocker resides.
86    list: &'a std::sync::RwLock<RangeBlockedList>,
87
88    /// `Option`, so `drop()` can `take()` it and unwrap the `Arc`.
89    ///
90    /// Consequently, do not clone: Must have refcount 1 when dropped.  (The only clone must be in
91    /// `self.list.blocked`, under index `self.block.index`.)
92    block: Option<Arc<RangeBlocked>>,
93}
94
95#[maybe_async]
96impl CommonStorageHelper {
97    /// Await concurrent strong write blockers for the given range.
98    ///
99    /// Strong write blockers are set up for writes that must not be intersected by any other
100    /// write.  Await such intersecting concurrent write requests, and return a guard that will
101    /// delay such new writes until the guard is dropped.
102    pub async fn weak_write_blocker(&self, range: Range<u64>) -> RangeBlockedGuard<'_> {
103        #[cfg(all(feature = "sync", debug_assertions))]
104        Self::assert_no_same_thread_overlap(&self.strong_write_blockers, &range);
105
106        #[cfg(feature = "async")]
107        let mut intersecting = FuturesUnordered::new();
108        #[cfg(feature = "sync")]
109        let mut intersecting = Vec::new();
110
111        // Create `RangeBlockedGuard` before the `await` below, so if the future is dropped,
112        // `RangeBlockedGuard::drop()` will run, removing the blocker from the list
113        let guard = {
114            // Consistent ordering to avoid deadlock: Always acquire weak before strong
115            let mut weak = self.weak_write_blockers.write().unwrap();
116            let strong = self.strong_write_blockers.read().unwrap();
117
118            strong.collect_intersecting(&range, &mut intersecting);
119
120            RangeBlockedGuard {
121                list: &self.weak_write_blockers,
122                block: Some(weak.block(range)),
123            }
124        };
125
126        // `RecvError` means the blocker's guard was dropped without signaling, so the blocking
127        // operation is gone, and thus waiting for it is pointless.  We must still wait for all
128        // other overlapping blockers, so drain until all are actually done, ignoring errors.
129        #[cfg(feature = "async")]
130        while intersecting.next().await.is_some() {}
131        #[cfg(feature = "sync")]
132        for rx in intersecting {
133            let _ = rx.recv();
134        }
135
136        guard
137    }
138
139    /// Await any concurrent write request for the given range.
140    ///
141    /// Block the given range for any concurrent write requests until the returned guard object is
142    /// dropped.  Existing requests are awaited, and new ones will be delayed.
143    pub async fn strong_write_blocker(&self, range: Range<u64>) -> RangeBlockedGuard<'_> {
144        #[cfg(all(feature = "sync", debug_assertions))]
145        {
146            Self::assert_no_same_thread_overlap(&self.weak_write_blockers, &range);
147            Self::assert_no_same_thread_overlap(&self.strong_write_blockers, &range);
148        }
149
150        #[cfg(feature = "async")]
151        let mut intersecting = FuturesUnordered::new();
152        #[cfg(feature = "sync")]
153        let mut intersecting = Vec::new();
154
155        // Create `RangeBlockedGuard` before the `await` below, so if the future is dropped,
156        // `RangeBlockedGuard::drop()` will run, removing the blocker from the list
157        let guard = {
158            // Consistent ordering to avoid deadlock: Always acquire weak before strong
159            let weak = self.weak_write_blockers.read().unwrap();
160            let mut strong = self.strong_write_blockers.write().unwrap();
161
162            weak.collect_intersecting(&range, &mut intersecting);
163            strong.collect_intersecting(&range, &mut intersecting);
164
165            RangeBlockedGuard {
166                list: &self.strong_write_blockers,
167                block: Some(strong.block(range)),
168            }
169        };
170
171        // `RecvError` means the blocker's guard was dropped without signaling, so the blocking
172        // operation is gone, and thus waiting for it is pointless.  We must still wait for all
173        // other overlapping blockers, so drain until all are actually done, ignoring errors.
174        #[cfg(feature = "async")]
175        while intersecting.next().await.is_some() {}
176        #[cfg(feature = "sync")]
177        for rx in intersecting {
178            let _ = rx.recv();
179        }
180
181        guard
182    }
183
184    /// Panic if the current thread already holds a blocker in `list` that overlaps `range`.
185    ///
186    /// In sync mode, blocking on `recv()` to wait for an overlapping blocker held by the same
187    /// thread would deadlock, because that blocker can only be released by this (now blocked)
188    /// thread.  This check runs before any locks are acquired so that a panic does not poison
189    /// them, allowing already-held guards to drop cleanly during unwinding.
190    #[cfg(all(feature = "sync", debug_assertions))]
191    fn assert_no_same_thread_overlap(
192        list: &std::sync::RwLock<RangeBlockedList>,
193        range: &Range<u64>,
194    ) {
195        let list = list.read().unwrap();
196        let current = std::thread::current().id();
197        for rb in &list.blocked {
198            if rb.range.overlaps(range) && rb.owner_thread == current {
199                panic!(
200                    "Same-thread reentrancy: already holding write blocker for {:?}, \
201                     acquiring overlapping blocker for {range:?} would deadlock",
202                    rb.range,
203                );
204            }
205        }
206    }
207}
208
209impl RangeBlockedList {
210    /// Collects futures/receivers to await intersecting request.
211    ///
212    /// Creates a channel for every intersecting request; blocking on the receiver will wait for
213    /// the request to complete.
214    fn collect_intersecting(
215        &self,
216        check_range: &Range<u64>,
217        #[cfg(feature = "async")] intersecting: &mut FuturesUnordered<oneshot::Receiver<()>>,
218        #[cfg(feature = "sync")] intersecting: &mut Vec<mpsc::Receiver<()>>,
219    ) {
220        for range_block in self.blocked.iter() {
221            if range_block.range.overlaps(check_range) {
222                #[cfg(feature = "async")]
223                let (s, r) = oneshot::channel::<()>();
224                #[cfg(feature = "sync")]
225                let (s, r) = mpsc::channel();
226
227                range_block.waitlist.lock().unwrap().push(s);
228                intersecting.push(r);
229            }
230        }
231    }
232
233    /// Enter a new blocked range into the list.
234    ///
235    /// This only blocks new requests, old requests must separately be waited for by blocking on
236    /// all receivers returned by `collect_intersecting()`.
237    fn block(&mut self, range: Range<u64>) -> Arc<RangeBlocked> {
238        let range_block = Arc::new(RangeBlocked {
239            range,
240            waitlist: Default::default(),
241            index: self.blocked.len().into(),
242            #[cfg(all(feature = "sync", debug_assertions))]
243            owner_thread: std::thread::current().id(),
244        });
245        self.blocked.push(Arc::clone(&range_block));
246        range_block
247    }
248}
249
250impl Drop for RangeBlockedGuard<'_> {
251    fn drop(&mut self) {
252        let block = self.block.take().unwrap();
253
254        {
255            let mut list = self.list.write().unwrap();
256            let i = block.index.load(Ordering::Relaxed);
257            let removed = list.blocked.swap_remove(i);
258            debug_assert!(Arc::ptr_eq(&removed, &block));
259            if let Some(block) = list.blocked.get(i) {
260                block.index.store(i, Ordering::Relaxed);
261            }
262        }
263
264        let block = Arc::into_inner(block).unwrap();
265        let waitlist = block.waitlist.into_inner().unwrap();
266        for waiting in waitlist {
267            // If the receiving end was dropped (e.g. because the request was dropped), then just
268            // ignore that
269            let _ = waiting.send(());
270        }
271    }
272}
273
274#[cfg(all(feature = "sync", test, debug_assertions))]
275mod tests {
276    use super::*;
277
278    #[test]
279    #[should_panic(expected = "Same-thread reentrancy: already holding write blocker")]
280    fn test_weak_then_overlapping_strong() {
281        let helper = CommonStorageHelper::default();
282        let _weak = helper.weak_write_blocker(0..100);
283        let _strong = helper.strong_write_blocker(50..150);
284    }
285
286    #[test]
287    #[should_panic(expected = "Same-thread reentrancy: already holding write blocker")]
288    fn test_strong_then_overlapping_strong() {
289        let helper = CommonStorageHelper::default();
290        let _first = helper.strong_write_blocker(0..100);
291        let _second = helper.strong_write_blocker(50..150);
292    }
293
294    #[test]
295    #[should_panic(expected = "Same-thread reentrancy: already holding write blocker")]
296    fn test_strong_then_overlapping_weak() {
297        let helper = CommonStorageHelper::default();
298        let _strong = helper.strong_write_blocker(0..100);
299        let _weak = helper.weak_write_blocker(50..150);
300    }
301
302    #[test]
303    fn test_non_overlapping() {
304        let helper = CommonStorageHelper::default();
305        let _first = helper.weak_write_blocker(0..100);
306        let _second = helper.strong_write_blocker(100..200);
307    }
308
309    #[test]
310    fn test_weak_then_overlapping_weak() {
311        let helper = CommonStorageHelper::default();
312        let _first = helper.weak_write_blocker(0..100);
313        let _second = helper.weak_write_blocker(50..150);
314    }
315}