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`] may block on the allocator.
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//! - **Empty-Linearizability**: if the wrapped collection is linearizale, all operations on [`Resizable`] are empty-linearizable with respect to the wrapped collections specification.
17//!
18//! Under FIFO ordering the relaxation is bounded as folows:
19//!
20//! For any item `x`, its rank displacement is bounded by:
21//!
22//!  **rank_error**<sub>x</sub> ≤ *K*
23//!
24//! And its delay by:
25//!
26//!  **delay**<sub>x</sub> ≤ *P*
27//!
28//! where
29//!  - *K* is the number of concurrent calls to `try_push`
30//!  - *P* is the number of concurren calls to `try_pop` overlapping with the $K$ executions
31//!
32//! > [!NOTE]
33//! > All ordering claims and relaxation bounds are made in the context of collections with FIFO ordering or no ordering.
34//! > Wrapping collections with different specifications may result in unbounded relaxation of those specifications.
35//!
36//! For the reasoning behind this bound consult the document [`docs/Relaxation.md`](https://github.com/lmeller-git/mpmc-resize/tree/main/docs/Relaxation.md).
37//!
38//! If no call to [`Resizable::resize`] happens, or in steady-state, [`Resizable`] preserves the ordering specification of the wrapped data structure strictly and is strictly linearizable, given the same holds for the wrapped collection.
39//!
40//! ## Limitations
41//!
42//! ### Overhead
43//!
44//! Preserving lock-free progress guarantees and linearizability across dynamic epoch shifts introduces some operational overhead:
45//!
46//! - **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.
47//! - **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.
48//! - **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`].
49//!
50//! Benchmarks can be found in `benches/main_benchmarks.rs`.
51//!
52//! ### Resizing Frequency
53//!
54//! The maximum frequency of successful resizes is currently bounded by the speed with which stale accesses get processed.
55//! Before a second resize can happen, all items of the old raw collection must be removed and all accesses must be concluded.
56//!
57//! If a thread gets permanently preempted while accessing a stale raw collection, no further items get removed, then no resize can happen again.
58//!
59//! ## Usage
60//!
61//! To use [`Resizable`] on your type, you must implement [`BoundedCollection`] for it.
62//!
63//! ```rust
64//! # use mpmc_resize::{BoundedCollection, Resizable};
65//! # use std::sync::Mutex;
66//! # use std::collections::VecDeque;
67//! #
68//! # struct MyQueue<T> { deque: Mutex<VecDeque<T>>, cap: usize }
69//! # impl<T> BoundedCollection for MyQueue<T> {
70//! #     type Item = T;
71//! #     fn with_capacity(cap: usize) -> Self { Self { deque: Mutex::new(VecDeque::with_capacity(cap)), cap } }
72//! #     fn try_push(&self, item: T) -> Result<(), T> {
73//! #         let mut g = self.deque.lock().unwrap();
74//! #         if g.len() >= self.cap { Err(item) } else { g.push_back(item); Ok(()) }
75//! #     }
76//! #     fn try_pop(&self) -> Option<T> { self.deque.lock().unwrap().pop_front() }
77//! #     fn len(&self) -> usize { self.deque.lock().unwrap().len() }
78//! #     fn capacity(&self) -> usize { self.cap }
79//! # }
80//! // Create a Resizable wrapping your datastructure
81//! let container = Resizable::<MyQueue<i32>>::with_capacity(2);
82//!
83//! // It implements all operations of a BoundedCollection
84//! assert!(container.try_push(42).is_ok());
85//! assert!(container.try_push(10).is_ok());
86//! assert!(container.is_full());
87//!
88//! // The container can now be dynamically resized, even if the wrapped datastructure cannot
89//! assert!(container.resize(4));
90//! assert!(!container.is_full());
91//!
92//! assert!(container.try_push(30).is_ok());
93//! assert_eq!(container.try_pop(), Some(42));
94//! ```
95//!
96//! For more info on how to implement [`BoundedCollection`], refer to its documentation.
97//!
98//! ## Platform Support
99//!
100//! All platforms supporting native atomic operations are supported.
101//!
102//! The feature `atomic-fallback` may be used, if no native atomic operations are available.
103//!
104//! ## Feature Flags
105//!
106//! - `std`: Enables `std` support.
107//! - `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.
108//! - `default`: None
109//!
110//! ## Testing
111//!
112//! Currently testing is based on:
113//!
114//! - **Miri** - to validate pointer arithmetic and catch undefined behavior.
115//! - **Loom and Shuttle** - to test for race conditions and non-blocking invariants.
116//! - **ASan** - to check for memory corruption.
117
118#![cfg_attr(not(any(feature = "std", test)), no_std)]
119#![deny(missing_docs)]
120#![deny(clippy::missing_safety_doc, clippy::undocumented_unsafe_blocks)]
121#![warn(unsafe_op_in_unsafe_fn)]
122
123#[cfg(any(feature = "std", test))]
124extern crate std;
125
126extern crate alloc;
127
128mod resize;
129mod sync;
130mod utils;
131
132#[cfg(test)]
133mod tests;
134
135pub use resize::{IntoIter, Resizable};
136
137/// This trait is used to describe a collection that is wrapped by a [`Resizable`].
138///
139/// Fallible operations on this type may spuriously fail.
140///
141/// `len`, `is_empty` and `is_full` will never be used for synchronization.
142pub trait BoundedCollection {
143    /// The item stored in the collection.
144    type Item;
145
146    /// Constructs a new instance of this type with capacity `capacity`.
147    ///
148    /// This method is explicitly allowed to block.
149    fn with_capacity(capacity: usize) -> Self;
150
151    /// Attempts to push an item into the collection.
152    /// Returns the item as an error if the collection is full.
153    fn try_push(&self, item: Self::Item) -> Result<(), Self::Item>;
154    /// Attempts to pop an item from the collection.
155    /// Returns `None` if the collection is empty.
156    fn try_pop(&self) -> Option<Self::Item>;
157    /// Returns the current length of the collection.
158    /// The returned value may be stale under concurrent access and should not be used for synchronization.
159    fn len(&self) -> usize;
160    /// Returns the total capacity of the collection.
161    fn capacity(&self) -> usize;
162
163    /// Indicates whether the collection is empty.
164    /// The returned value may be stale under concurrent access and should not be used for synchronization.
165    fn is_empty(&self) -> bool {
166        self.len() == 0
167    }
168
169    /// Indicates whether the collection is full.
170    /// The returned value may be stale under concurrent access and should not be used for synchronization.
171    fn is_full(&self) -> bool {
172        self.len() == self.capacity()
173    }
174}