Skip to main content

mpmc_resize/
lib.rs

1//! This crate implements a generic construction which allows dynamically resizing a wrapped datastructure, while preserving some key properties.
2//!
3//! ## Property Preservation
4//!
5//! ### Progress Guarantees:
6//!
7//! - **Lock Freedom**: if the wrapped collection is lock-free, all corresponding operations on [`Resizable`] are also lock-free.
8//! - **Obstruction Freedom**: if the wrapped collection exposes obstruction-free methods, all corresponding operations on [`Resizable`] are also obstruction-free.
9//!
10//! [`Resizable::resize`] is blocking both on allocator and stale readers and writers.
11//!
12//! ### Ordering and Consistency Guarantees:
13//!
14//! - **Relaxed FIFO**: if the wrapped collection has FIFO ordering, [`Resizable`] has **k-FIFO** ordering.
15//! - **Linearizability**: if the wrapped collection is linearizable, all operations on [`Resizable`] are also linearizable with respect to its relaxed FIFO specification.
16//!
17//! Specifically, for any item $x$, its rank displacement is bounded by:
18//!
19//! $$\text{rank\\_error}_x \le K$$
20//!
21//! And its delay by:
22//!
23//! $$\text{delay}_x \le P$$
24//!
25//! where
26//!  $K$ is the number of concurrent calls to `try_push`
27//!  $P$ is the number of concurren calls to `try_pop` overlapping with the $K$ executions
28//!
29//! For the reasoning behind this bound consult the document [`docs/Relaxation.md`](https://github.com/lmeller-git/mpmc-resize/tree/main/docs/Relaxation.md).
30//!
31//! If no call to [`Resizable::resize`] happens, or in steady-state, [`Resizable`] has strict FIFO ordering and is strictly linearizable, given the same holds for the wrapped collection.
32//!
33//! ## Limitations
34//!
35//! ### Overhead
36//!
37//! Preserving lock-free progress guarantees and linearizability across dynamic epoch shifts introduces some operational overhead:
38//!
39//! - **Steady-State:** In a single-producer single-consumer (SPSC) scenario with no active resizes, wrapping a queue (e.g., `Resizable<ArrayQueue>`) achieves roughly **40% of the throughput** of the raw underlying queue.
40//! - **Active Resizing:** Dynamically triggering resizes under load reduces throughput by an additional **~30%** during migration bursts. However, the throughput gained from higher buffer capacity can offset this cost over time.
41//! - **Reordering in practice:** Item reordering is rare in practice and depends strongly on the number of threads concurrently invoking [`BoundedCollection::try_pop`] and [`BoundedCollection::try_push`] during an active [`Resizable::resize`].
42//!
43//! Benchmarks can be found in `benches/main_benchmarks.rs`.
44//!
45//! ### Resizing Frequency
46//!
47//! The maximum frequency of successful resizes is currently bounded by the speed with which stale accesses get processed.
48//! Before a second resize can happen, all items of the old raw collection must be removed and all accesses must be concluded.
49//!
50//! If a thread gets permanently preempted while accessing a stale raw collection, no further items get removed, then no resize can happen again.
51//!
52//! ## Usage
53//!
54//! To use [`Resizable`] on your type, you must implement [`BoundedCollection`] for it.
55//!
56//! ```rust
57//! # use mpmc_resize::{BoundedCollection, Resizable};
58//! # use std::sync::Mutex;
59//! # use std::collections::VecDeque;
60//! #
61//! # struct MyQueue<T> { deque: Mutex<VecDeque<T>>, cap: usize }
62//! # impl<T> BoundedCollection for MyQueue<T> {
63//! #     type Item = T;
64//! #     fn with_capacity(cap: usize) -> Self { Self { deque: Mutex::new(VecDeque::with_capacity(cap)), cap } }
65//! #     fn try_push(&self, item: T) -> Result<(), T> {
66//! #         let mut g = self.deque.lock().unwrap();
67//! #         if g.len() >= self.cap { Err(item) } else { g.push_back(item); Ok(()) }
68//! #     }
69//! #     fn try_pop(&self) -> Option<T> { self.deque.lock().unwrap().pop_front() }
70//! #     fn len(&self) -> usize { self.deque.lock().unwrap().len() }
71//! #     fn capacity(&self) -> usize { self.cap }
72//! # }
73//! // Create a Resizable wrapping your datastructure
74//! let container = Resizable::<MyQueue<i32>>::with_capacity(2);
75//!
76//! // It implements all operations of a BoundedCollection
77//! assert!(container.try_push(42).is_ok());
78//! assert!(container.try_push(10).is_ok());
79//! assert!(container.is_full());
80//!
81//! // The container can now be dynamically resized, even if the wrapped datastructure cannot
82//! assert!(container.resize(4));
83//! assert!(!container.is_full());
84//!
85//! assert!(container.try_push(30).is_ok());
86//! assert_eq!(container.try_pop(), Some(42));
87//! ```
88//!
89//! For more info on how to implement [`BoundedCollection`], refer to its documentation.
90//!
91//! ## Platform Support
92//!
93//! All platforms supporting native atomic operations are supported.
94//!
95//! The feature `atomic-fallback` may be used, if no native atomic operations are available.
96//!
97//! ## Feature Flags
98//!
99//! - `std`: Enables `std` support.
100//! - `atomic-fallback`:  Uses the `portable-atomic` fallback feature if native atomics are missing. It is discouraged to use this feature, as fallback atomics internally rely on locks.
101//! - `default`: None
102//!
103//! ## Testing
104//!
105//! Currently testing is based on:
106//!
107//! - **Miri** - to validate pointer arithmetic and catch undefined behavior.
108//! - **Loom and Shuttle** - to test for race conditions and non-blocking invariants.
109//! - **ASan** - to check for memory corruption.
110
111#![cfg_attr(not(any(feature = "std", test)), no_std)]
112#![deny(missing_docs)]
113#![deny(clippy::missing_safety_doc, clippy::undocumented_unsafe_blocks)]
114#![warn(unsafe_op_in_unsafe_fn)]
115
116#[cfg(any(feature = "std", test))]
117extern crate std;
118
119extern crate alloc;
120
121mod resize;
122mod sync;
123mod utils;
124
125#[cfg(test)]
126mod tests;
127
128pub use resize::{IntoIter, Resizable};
129
130/// This trait is used to describe a collection that is wrapped by a [`Resizable`].
131///
132/// Fallible operations on this type may spuriously fail.
133///
134/// `len`, `is_empty` and `is_full` will never be used for synchronization.
135pub trait BoundedCollection {
136    /// The item stored in the collection.
137    type Item;
138
139    /// Constructs a new instance of this type with capacity `capacity`.
140    ///
141    /// This method is explicitly allowed to block.
142    fn with_capacity(capacity: usize) -> Self;
143
144    /// Attempts to push an item into the collection.
145    /// Returns the item as an error if the collection is full.
146    fn try_push(&self, item: Self::Item) -> Result<(), Self::Item>;
147    /// Attempts to pop an item from the collection.
148    /// Returns `None` if the collection is empty.
149    fn try_pop(&self) -> Option<Self::Item>;
150    /// Returns the current length of the collection.
151    /// The returned value may be stale under concurrent access and should not be used for synchronization.
152    fn len(&self) -> usize;
153    /// Returns the total capacity of the collection.
154    fn capacity(&self) -> usize;
155
156    /// Indicates whether the collection is empty.
157    /// The returned value may be stale under concurrent access and should not be used for synchronization.
158    fn is_empty(&self) -> bool {
159        self.len() == 0
160    }
161
162    /// Indicates whether the collection is full.
163    /// The returned value may be stale under concurrent access and should not be used for synchronization.
164    fn is_full(&self) -> bool {
165        self.len() == self.capacity()
166    }
167}