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