Skip to main content

rx_rust/utils/
mutable.rs

1//! The [`Mutable`] abstraction over the single-threaded and the multi-threaded backend, the
2//! handful of traits that are the only sanctioned way to reach through it, and the rules a
3//! callback has to follow.
4//!
5//! # Rule 1: the guard must not outlive the operation
6//!
7//! Refer to this case: <https://stackoverflow.com/q/79621758/9315497>
8//! And this case:
9//!
10//! ```ignore
11//! let lock = Mutex::new("My String".to_owned());
12//! // let equals = { lock.lock().unwrap().clone() } == { lock.lock().unwrap().clone() }; // No deadlock
13//! let equals = lock.lock().unwrap().clone() == lock.lock().unwrap().clone(); // Deadlock
14//! ```
15//!
16//! Both guards are temporaries of the same statement, so the first one is still alive when the
17//! second is taken. [`MutableHelper::with_mut`] and [`with_ref`](MutableHelper::with_ref) rule
18//! this out structurally: they hand the callback a `&mut T` / `&T`, keep the guard as a temporary
19//! of their own body, and release it before returning. A guard can no longer be named, stored or
20//! compared.
21//!
22//! # Rule 2: the callback must not run anything that can take the same lock again
23//!
24//! This is the rule the type system cannot enforce, and it is the one that bites. Dropping a value
25//! counts as running code: the obvious `clear` on a collection of subscriptions
26//!
27//! ```ignore
28//! self.subscriptions.with_mut(Vec::clear) // Deadlock: dropping a Subscription disposes it,
29//!                                         // and disposing it takes this very lock again.
30//! ```
31//!
32//! is a deadlock in the `Disposable` of `merge_all`. So is calling an `Observer` or a `Disposable`
33//! from inside the callback, because both are user code that can re-enter the operator.
34//!
35//! The fix is always the same shape — **take the value out under the lock, act on it afterwards**:
36//!
37//! - [`MutableExt::take_value`] instead of `clear`, and [`MutableExt::replace_value`] instead of
38//!   an assignment: both return the value they displaced, so the caller acts on it — and drops it
39//!   — outside the lock.
40//! - When the operator has to decide *what* to do while holding the lock, let the callback compute
41//!   an action and return it, and run that action after `with_mut` has returned. This is what
42//!   `ref_count`, `unicast_subject`, `amb` and `serialized_delivery` do.
43//!
44//! In debug builds this rule is checked at runtime: taking a lock the current thread already holds
45//! panics at the offending call site instead of deadlocking.
46
47mod reentrancy;
48
49/// The single entry point to a [`Mutable`], for both the single-threaded and the multi-threaded
50/// backend.
51///
52/// The callback is handed a plain reference rather than the backend's guard, which is what makes
53/// the lock impossible to hold longer than the callback: the guard is a temporary inside
54/// `with_mut` / `with_ref` and is released before either returns. Everything the callback produces
55/// therefore lives — and is dropped — outside the lock.
56///
57/// See the [module documentation](self) for the rules a callback has to follow.
58pub trait MutableHelper {
59    type Value;
60
61    fn with_mut<R>(&self, callback: impl FnOnce(&mut Self::Value) -> R) -> R;
62    fn with_ref<R>(&self, callback: impl FnOnce(&Self::Value) -> R) -> R;
63}
64
65/// The handful of one-shot operations that cover most uses of a [`Mutable`].
66///
67/// Each of them takes the lock exactly once and hands every value it produces back to the caller,
68/// so the value is used — and dropped — after the lock has been released.
69///
70/// The `_value` suffixes keep these names clear of the inherent methods of the two backends:
71/// `RefCell::take` already exists, and `Mutex::{get_cloned, set}` exist behind the unstable
72/// `lock_value_accessors` feature. An inherent method wins method resolution over a trait one, so
73/// a colliding name would silently bypass the re-entrancy check on one of the two backends.
74pub trait MutableExt: MutableHelper {
75    /// Clones the contained value.
76    fn clone_value(&self) -> Self::Value
77    where
78        Self::Value: Clone,
79    {
80        self.with_ref(Clone::clone)
81    }
82
83    /// Stores `value` and returns the replaced one, leaving it to the caller to drop it outside
84    /// the lock.
85    fn replace_value(&self, value: Self::Value) -> Self::Value {
86        self.with_mut(|current| std::mem::replace(current, value))
87    }
88
89    /// Takes the contained value out, leaving the default in its place.
90    ///
91    /// For a `Mutable<Option<T>>` this is `Option::take`, and for a `Mutable<Vec<T>>` it is the
92    /// deadlock-free replacement for `clear`: the elements are dropped by the caller instead of
93    /// under the lock.
94    ///
95    /// This is the shape to reach for whenever what follows is anything the crate does not own —
96    /// `Observer::on_next`, `Observer::on_termination`, `Disposable::dispose` — because all of
97    /// them can re-enter the very lock they were reached through. `slot.take_value().map(f)` runs
98    /// `f` with the lock already released.
99    fn take_value(&self) -> Self::Value
100    where
101        Self::Value: Default,
102    {
103        self.with_mut(std::mem::take)
104    }
105}
106
107impl<M: MutableHelper + ?Sized> MutableExt for M {}
108
109pub trait MutableBoolHelper {
110    fn read(&self) -> bool;
111    fn write(&self, value: bool);
112    // Change the contained value to `value`, returns true if it was changed. otherwise false.
113    fn change_if_not_equal(&self, value: bool) -> bool;
114}
115
116cfg_if::cfg_if! {
117    if #[cfg(feature = "single-threaded")] {
118        use std::cell::{Cell, RefCell};
119
120        pub type Mutable<T> = RefCell<T>;
121
122        impl<T> MutableHelper for RefCell<T> {
123            type Value = T;
124
125            fn with_mut<R>(&self, callback: impl FnOnce(&mut T) -> R) -> R {
126                let _held = reentrancy::held_lock(self);
127                callback(&mut self.borrow_mut())
128            }
129            fn with_ref<R>(&self, callback: impl FnOnce(&T) -> R) -> R {
130                let _held = reentrancy::held_lock(self);
131                callback(&self.borrow())
132            }
133        }
134
135        pub type MutableBool = Cell<bool>;
136        impl MutableBoolHelper for Cell<bool> {
137            fn read(&self) -> bool {
138                self.get()
139            }
140            fn write(&self, value: bool) {
141                self.set(value)
142            }
143            fn change_if_not_equal(&self, value: bool) -> bool {
144                let old = self.replace(value);
145                old != value
146            }
147        }
148    } else {
149        use std::sync::Mutex;
150        use std::sync::atomic::{AtomicBool, Ordering};
151
152        pub type Mutable<T> = Mutex<T>;
153
154        impl<T> MutableHelper for Mutex<T> {
155            type Value = T;
156
157            fn with_mut<R>(&self, callback: impl FnOnce(&mut T) -> R) -> R {
158                let _held = reentrancy::held_lock(self);
159                callback(&mut self.lock().unwrap())
160            }
161            fn with_ref<R>(&self, callback: impl FnOnce(&T) -> R) -> R {
162                let _held = reentrancy::held_lock(self);
163                callback(&self.lock().unwrap())
164            }
165        }
166
167        pub type MutableBool = AtomicBool;
168        impl MutableBoolHelper for MutableBool {
169            fn read(&self) -> bool {
170                self.load(Ordering::SeqCst)
171            }
172            fn write(&self, value: bool) {
173                self.store(value, Ordering::SeqCst)
174            }
175            fn change_if_not_equal(&self, value: bool) -> bool {
176                self.compare_exchange(!value, value, Ordering::SeqCst, Ordering::SeqCst).is_ok()
177            }
178        }
179    }
180}