reft_light/
lib.rs

1//! A concurrency primitive for high concurrency reads over a single-writer data structure.
2//!
3//! The primitive keeps two copies of the backing data structure, one that is accessed by readers,
4//! and one that is accessed by the (single) writer. This enables all reads to proceed in parallel
5//! with minimal coordination, and shifts the coordination overhead to the writer. In the absence
6//! of writes, reads scale linearly with the number of cores.
7//!
8//! When the writer wishes to expose new changes to the datastructure (see
9//! [`WriteHandle::publish`]), it "flips" the two copies so that subsequent reads go to the old
10//! "write side", and future writers go to the old "read side". This process does cause two cache
11//! line invalidations for the readers, but does not stop them from making progress (i.e., reads
12//! are wait-free).
13//!
14//! In order to keep both copies up to date, left-right keeps an operational log ("oplog") of all
15//! the modifications to the data structure, which it uses to bring the old read data up to date
16//! with the latest writes on a flip. Since there are two copies of the data, each oplog entry is
17//! applied twice: once to the write copy and again to the (stale) read copy.
18//!
19//! # Trade-offs
20//!
21//! Few concurrency wins come for free, and this one is no exception. The drawbacks of this
22//! primitive are:
23//!
24//!  - **Increased memory use**: since we keep two copies of the backing data structure, we are
25//!  effectively doubling the memory use of the underlying data. With some clever de-duplication,
26//!  this cost can be ameliorated to some degree, but it's something to be aware of. Furthermore,
27//!  if writers only call `publish` infrequently despite adding many writes to the operational log,
28//!  the operational log itself may grow quite large, which adds additional overhead.
29//!  - **Deterministic operations**: as the entries in the operational log are applied twice, once
30//!  to each copy of the data, it is essential that the operations are deterministic. If they are
31//!  not, the two copies will no longer mirror one another, and will continue to diverge over time.
32//!  - **Single writer**: left-right only supports a single writer. To have multiple writers, you
33//!  need to ensure exclusive access to the [`WriteHandle`] through something like a
34//!  [`Mutex`](std::sync::Mutex).
35//!  - **Slow writes**: Writes through left-right are slower than they would be directly against
36//!  the backing datastructure. This is both because they have to go through the operational log,
37//!  and because they must each be applied twice.
38//!
39//! # How does it work?
40//!
41//! Take a look at [this YouTube video](https://www.youtube.com/watch?v=eLNAMEoKAAc) which goes
42//! through the basic concurrency algorithm, as well as the initial development of this library.
43//! Alternatively, there's a shorter (but also less complete) description in [this
44//! talk](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1994).
45//!
46//! At a glance, left-right is implemented using two regular `T`s,
47//! an auxiliary type `A` which is only used during writes, an operational log, epoch
48//! counting, and some pointer magic. There is a single pointer through which all readers go. It
49//! points to a `T` that the readers access in order to read data. Every time a read has accessed
50//! the pointer, they increment a local epoch counter, and they update it again when they have
51//! finished the read. When a write occurs, the writer updates the other `T` (for which there are
52//! no readers), and also stores a copy of the change in a log. When [`WriteHandle::publish`] is
53//! called, the writer, atomically swaps the reader pointer to point to the other `T`. It then
54//! waits for the epochs of all current readers to change, and then replays the operational log to
55//! bring the stale copy up to date.
56//!
57//! The design resembles this [left-right concurrency
58//! scheme](https://hal.archives-ouvertes.fr/hal-01207881/document) from 2015, though I am not
59//! aware of any follow-up to that work.
60//!
61//! # How do I use it?
62//!
63//! If you just want a data structure for fast reads, you likely want to use a crate that _uses_
64//! this crate, like [`evmap`](https://docs.rs/evmap/). If you want to develop such a crate
65//! yourself, here's what you do:
66//!
67//! ```rust
68//! use reft_light::{Apply, ReadHandle, WriteHandle};
69//!
70//! // First, define an operational log type.
71//! // For most real-world use-cases, this will be an `enum`, but we'll keep it simple:
72//! struct CounterAddOp(i32);
73//!
74//! // Then, implement the `Apply` trait for that type,
75//! // and provide the datastructure types it operates over as generic arguments.
76//! // You can read this as "`CounterAddOp` can apply changes of types `i32` and `()`".
77//! impl Apply<i32, ()> for CounterAddOp {
78//!     // See the documentation of `Apply::apply_first`.
79//!     //
80//!     // Essentially, this is where you define what applying
81//!     // the oplog type to the datastructure does.
82//!     fn apply_first(&mut self, first: &mut i32, _: &i32, _: &mut ()) {
83//!         *first += self.0;
84//!     }
85//!
86//!     // See the documentation of `Apply::apply_second`.
87//!     //
88//!     // This may or may not be the same as `apply_first`,
89//!     // depending on whether or not you de-duplicate values
90//!     // across the two copies of your data structure.
91//!     fn apply_second(self, _: &i32, second: &mut i32, _: &mut ()) {
92//!         *second += self.0;
93//!     }
94//! }
95//!
96//! // Now, you can construct a new left-right over an instance of your data structure.
97//! // This will give you a `WriteHandle` that accepts writes in the form of oplog entries,
98//! // which can be converted into a (cloneable) `ReadHandle` that gives you `&` access to the data structure.
99//! let write = reft_light::new::<CounterAddOp, i32, ()>(0, ());
100//! let read = write.clone();
101//!
102//! // You will likely want to embed these handles in your own types so that you can
103//! // provide more ergonomic methods for performing operations on your type.
104//! struct Counter(WriteHandle<CounterAddOp, i32, ()>);
105//! impl Counter {
106//!     // The methods on you write handle type will likely all just add to the operational log.
107//!     pub fn add(&mut self, i: i32) {
108//!         self.0.append(CounterAddOp(i));
109//!     }
110//!
111//!     // You should also provide a method for exposing the results of any pending operations.
112//!     //
113//!     // Until this is called, any writes made since the last call to `publish` will not be
114//!     // visible to readers. See `WriteHandle::publish` for more details. Make sure to call
115//!     // this out in _your_ documentation as well, so that your users will be aware of this
116//!     // "weird" behavior.
117//!     pub fn publish(&mut self) {
118//!         self.0.publish();
119//!     }
120//! }
121//!
122//! // Similarly, for reads:
123//! #[derive(Clone)]
124//! struct CountReader(ReadHandle<i32>);
125//! impl CountReader {
126//!     pub fn get(&self) -> i32 {
127//!         // The `ReadHandle` itself does not allow you to access the underlying data.
128//!         // Instead, you must first "enter" the data structure. This is similar to
129//!         // taking a `Mutex`, except that no lock is actually taken. When you enter,
130//!         // you are given back a guard, which gives you shared access (through the
131//!         // `Deref` trait) to the "read copy" of the data structure.
132//!         //
133//!         // Note that `enter` may yield `None`, which implies that the `WriteHandle`
134//!         // was dropped, and took the backing data down with it.
135//!         //
136//!         // Note also that for as long as the guard lives, a writer that tries to
137//!         // call `WriteHandle::publish` will be blocked from making progress.
138//!         self.0.enter().map(|guard| *guard).unwrap_or(0)
139//!     }
140//! }
141//!
142//! // These wrapper types are likely what you'll give out to your consumers.
143//! let (mut w, r) = (Counter(write), CountReader(read));
144//!
145//! // They can then use the type fairly ergonomically:
146//! assert_eq!(r.get(), 0);
147//! w.add(1);
148//! // no call to publish, so read side remains the same:
149//! assert_eq!(r.get(), 0);
150//! w.publish();
151//! assert_eq!(r.get(), 1);
152//! drop(w);
153//! // writer dropped data, so reads yield fallback value:
154//! assert_eq!(r.get(), 0);
155//! ```
156//!
157//! One additional noteworthy detail: much like with `Mutex`, `RwLock`, and `RefCell` from the
158//! standard library, the values you dereference out of a `ReadGuard` are tied to the lifetime of
159//! that `ReadGuard`. This can make it awkward to write ergonomic methods on the read handle that
160//! return references into the underlying data, and may tempt you to clone the data out or take a
161//! closure instead. Instead, consider using [`ReadGuard::map`] and [`ReadGuard::try_map`], which
162//! (like `RefCell`'s [`Ref::map`](std::cell::Ref::map)) allow you to provide a guarded reference
163//! deeper into your data structure.
164#![warn(
165    missing_docs,
166    rust_2018_idioms,
167    missing_debug_implementations,
168    broken_intra_doc_links
169)]
170#![allow(clippy::type_complexity)]
171
172mod sync;
173
174use crate::sync::{Arc, AtomicUsize, Mutex};
175
176type Epochs = Arc<Mutex<slab::Slab<Arc<AtomicUsize>>>>;
177
178mod write;
179pub use crate::write::WriteHandle;
180
181mod read;
182pub use crate::read::{ReadGuard, ReadHandle, ReadHandleFactory};
183
184/// Types that can incorporate operations of type `O`.
185///
186/// This trait allows `left-right` to keep the two copies of the underlying data structure (see the
187/// [crate-level documentation](crate)) the same over time. Each write operation to the data
188/// structure is logged as an operation of type `O` in an _operational log_ (oplog), and is applied
189/// once to each copy of the data.
190///
191/// Implementations should ensure that the application of each operation is deterministic. That is, if
192/// two instances of the type `T` are initially equal, and the same operation is applied to both of them,
193/// they should remain equal afterwards. If this is not the case, the two copies will drift apart
194/// over time, and hold different values.
195///
196/// The trait provides separate methods for the first and second application of each operation. For many
197/// implementations, these will be the same (which is why `apply_second` defaults to calling
198/// `apply_first`), but not all. In particular, some implementations may need to modify the operation to
199/// ensure deterministic results when it is applied to the second copy.
200pub trait Apply<T, A>: Sized {
201    /// Apply `O` to the first of the two copies.
202    ///
203    /// `other` is a reference to the other copy of the data, which has seen all operations up
204    /// until the previous call to [`WriteHandle::publish`]. That is, `other` is one "publish
205    /// cycle" behind.
206    fn apply_first(&mut self, first: &mut T, second: &T, auxiliary: &mut A);
207
208    /// Apply `O` to the second of the two copies.
209    ///
210    /// `other` is a reference to the other copy of the data, which has seen all operations up to
211    /// the call to [`WriteHandle::publish`] that initially exposed this `O`. That is, `other` is
212    /// one "publish cycle" ahead.
213    ///
214    /// Note that this method should modify the underlying data in _exactly_ the same way as
215    /// `O` modified `other`, otherwise the two copies will drift apart. Be particularly mindful of
216    /// non-deterministic implementations of traits that are often assumed to be deterministic
217    /// (like `Eq` and `Hash`), and of "hidden states" that subtly affect results like the
218    /// `RandomState` of a `HashMap` which can change iteration order.
219    ///
220    /// Defaults to calling `apply_first`.
221    fn apply_second(mut self, first: &T, second: &mut T, auxiliary: &mut A) {
222        Self::apply_first(&mut self, second, first, auxiliary);
223    }
224}
225
226/// Construct a new write handle from an initial swapping value and an auxiliary value.
227///
228/// The swapping type must implement `Clone` so we can construct the second copy from the first.
229pub fn new<O, T, A>(init: T, auxiliary: A) -> WriteHandle<O, T, A>
230where
231    O: Apply<T, A>,
232    T: Clone,
233{
234    let epochs = Default::default();
235
236    let r = ReadHandle::new(init.clone(), Arc::clone(&epochs));
237    let w = WriteHandle::new(init, epochs, r, auxiliary);
238    w
239}