From 965b5901b33126a67ead2c355378678cc401d7bf Mon Sep 17 00:00:00 2001 From: Istvan Date: Fri, 24 Apr 2026 11:20:40 +0200 Subject: [PATCH 1/2] Add contributor-only Shuttle lock tests Keep the default parking_lot backend and runtime behavior unchanged. Add a Shuttle-specific parking shim for model-checking the internal lock, expose it through a dedicated feature-gated test target, and cover reader/writer contention plus downgrade wakeups. Document the supported contributor workflow and add a separate CI job for the stable Shuttle test command. --- .github/workflows/ci.yml | 10 +- Cargo.lock | 229 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 ++ README.md | 15 +++ src/lock.rs | 161 +++++++++++++++++++++++++-- tests/shuttle.rs | 145 +++++++++++++++++++++++++ 6 files changed, 559 insertions(+), 8 deletions(-) create mode 100644 tests/shuttle.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5d272a9..6a28e2a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,15 @@ jobs: - name: style run: cargo fmt -- --check - name: clippy - run: cargo clippy --all-targets --all-features -- -D warnings + run: cargo clippy --all-targets --features all -- -D warnings + + shuttle: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v2 + - uses: dtolnay/rust-toolchain@stable + - name: test + run: cargo test --features shuttle --test shuttle test: runs-on: ubuntu-22.04 diff --git a/Cargo.lock b/Cargo.lock index 96fa4c33..a9aa6017 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +[[package]] +name = "assoc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8" + [[package]] name = "autocfg" version = "1.4.0" @@ -20,12 +26,43 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "const-siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03efed02df0504d71e44bfd51d3329f401b8303a2fd14254acf05c95a0af0153" + +[[package]] +name = "corosensei" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -64,6 +101,7 @@ dependencies = [ "parking_lot_core", "rayon", "serde", + "shuttle", "typesize", ] @@ -79,12 +117,35 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "libc" version = "0.2.169" @@ -101,6 +162,18 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot_core" version = "0.9.10" @@ -114,6 +187,21 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -132,6 +220,51 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.10.0" @@ -161,6 +294,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -187,6 +326,27 @@ dependencies = [ "syn", ] +[[package]] +name = "shuttle" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba93071c1b720be2505f4c8ce2863502cb9a26a3819e268df1932458a755152c" +dependencies = [ + "assoc", + "bitvec", + "cfg-if", + "const-siphasher", + "corosensei", + "hex", + "owo-colors", + "rand", + "rand_core", + "rand_pcg", + "scoped-tls", + "smallvec", + "tracing", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -204,6 +364,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "typesize" version = "0.1.13" @@ -231,6 +416,21 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -294,3 +494,32 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 946e9e2f..8eed65bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,14 @@ categories = ["concurrency", "algorithms", "data-structures"] [features] all = ["raw-api", "typesize", "serde", "rayon", "arbitrary"] raw-api = [] +shuttle = ["dep:shuttle"] typesize = ["dep:typesize"] inline-more = ["hashbrown/inline-more"] [dependencies] lock_api = "0.4.12" parking_lot_core = "0.9.10" +shuttle = { version = "0.9.1", optional = true } equivalent = "1.0.1" hashbrown = { version = "0.15.2", default-features = false } serde = { version = "1.0.217", optional = true, features = ["derive"] } @@ -33,3 +35,8 @@ typesize = { version = "0.1.13", default-features = false, features = ["hashbrow [package.metadata.docs.rs] features = ["all"] + +[[test]] +name = "shuttle" +path = "tests/shuttle.rs" +required-features = ["shuttle"] diff --git a/README.md b/README.md index 7d203922..fc6b5b4e 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,21 @@ I will take a look as soon as I have time for it. That said I do not get paid (yet) to work on open-source. This means that my time is limited and my work here comes after my personal life. +### Concurrency Testing + +DashMap includes a contributor-only [Shuttle](https://docs.rs/shuttle)-based test mode for +model checking the internal lock implementation. This is not part of DashMap's supported +runtime feature surface and is only intended to be run through the dedicated integration +test target. + +Use a current stable toolchain and run: + +```bash +RUSTUP_TOOLCHAIN=stable cargo test --features shuttle --test shuttle +``` + +Running the full crate test suite with `--features shuttle` is intentionally unsupported. + ## Performance A comprehensive benchmark suite including DashMap can be found [here](https://github.com/xacrimon/conc-map-bench). diff --git a/src/lock.rs b/src/lock.rs index c762ac9c..9bac5a67 100644 --- a/src/lock.rs +++ b/src/lock.rs @@ -1,5 +1,12 @@ +#[cfg(not(feature = "shuttle"))] use core::sync::atomic::{AtomicUsize, Ordering}; -use parking_lot_core::{ParkToken, SpinWait, UnparkToken}; +#[cfg(not(feature = "shuttle"))] +use parking_lot_core::{park, unpark_all, unpark_one, ParkToken, SpinWait, UnparkToken}; + +#[cfg(feature = "shuttle")] +use self::shuttle_parking::{park, unpark_all, unpark_one, ParkToken, SpinWait, UnparkToken}; +#[cfg(feature = "shuttle")] +use shuttle::sync::atomic::{AtomicUsize, Ordering}; pub type RwLock = lock_api::RwLock; pub(crate) type RwLockReadGuardDetached<'a> = crate::util::RwLockReadGuardDetached<'a, RawRwLock>; @@ -80,7 +87,7 @@ unsafe impl lock_api::RawRwLockDowngrade for RawRwLock { .state .fetch_and(ONE_READER | WRITERS_PARKED, Ordering::Release); if state & READERS_PARKED != 0 { - parking_lot_core::unpark_all((self as *const _ as usize) + 1, UnparkToken(0)); + unpark_all((self as *const _ as usize) + 1, UnparkToken(0)); } } } @@ -124,7 +131,7 @@ impl RawRwLock { } let _ = unsafe { - parking_lot_core::park( + park( self as *const _ as usize, || { let state = self.state.load(Ordering::Relaxed); @@ -168,13 +175,13 @@ impl RawRwLock { if parked == READERS_PARKED { return unsafe { - parking_lot_core::unpark_all((self as *const _ as usize) + 1, UnparkToken(0)); + unpark_all((self as *const _ as usize) + 1, UnparkToken(0)); }; } assert_eq!(parked, WRITERS_PARKED); unsafe { - parking_lot_core::unpark_one(self as *const _ as usize, |_| UnparkToken(0)); + unpark_one(self as *const _ as usize, |_| UnparkToken(0)); } } @@ -267,7 +274,7 @@ impl RawRwLock { } let _ = unsafe { - parking_lot_core::park( + park( (self as *const _ as usize) + 1, || { let state = self.state.load(Ordering::Relaxed); @@ -293,8 +300,148 @@ impl RawRwLock { .is_ok() { unsafe { - parking_lot_core::unpark_one(self as *const _ as usize, |_| UnparkToken(0)); + unpark_one(self as *const _ as usize, |_| UnparkToken(0)); } } } } + +#[cfg(feature = "shuttle")] +mod shuttle_parking { + use shuttle::sync::Mutex; + use shuttle::thread::{self, Thread, ThreadId}; + use std::collections::{HashMap, VecDeque}; + use std::sync::OnceLock; + use std::time::Instant; + + #[allow(dead_code)] + pub struct ParkToken(pub usize); + #[allow(dead_code)] + pub struct UnparkToken(pub usize); + + pub struct ParkResult; + pub struct UnparkResult; + + pub struct SpinWait { + counter: u32, + } + + type WaitQueues = HashMap>; + + fn wait_queues() -> &'static Mutex { + static WAIT_QUEUES: OnceLock> = OnceLock::new(); + WAIT_QUEUES.get_or_init(|| Mutex::new(HashMap::new())) + } + + fn lock_wait_queues() -> shuttle::sync::MutexGuard<'static, WaitQueues> { + wait_queues() + .lock() + .expect("Shuttle wait queue mutex poisoned") + } + + fn remove_waiter(wait_queues: &mut WaitQueues, key: usize, thread_id: ThreadId) { + let mut remove_key = false; + + if let Some(queue) = wait_queues.get_mut(&key) { + if let Some(index) = queue.iter().position(|thread| thread.id() == thread_id) { + queue.remove(index); + } + + remove_key = queue.is_empty(); + } + + if remove_key { + wait_queues.remove(&key); + } + } + + impl SpinWait { + #[inline] + pub fn new() -> Self { + Self { counter: 0 } + } + + #[inline] + pub fn spin(&mut self) -> bool { + self.counter += 1; + if self.counter <= 10 { + shuttle::thread::yield_now(); + true + } else { + false + } + } + + #[inline] + pub fn spin_no_yield(&mut self) { + shuttle::thread::yield_now(); + } + } + + pub unsafe fn park( + key: usize, + validate: impl FnOnce() -> bool, + before_sleep: impl FnOnce(), + _timed_out: impl FnOnce(usize, bool), + _park_token: ParkToken, + _timeout: Option, + ) -> ParkResult { + let current = thread::current(); + + { + let mut wait_queues = lock_wait_queues(); + if !validate() { + return ParkResult; + } + + wait_queues + .entry(key) + .or_default() + .push_back(current.clone()); + before_sleep(); + } + + thread::park(); + + let mut wait_queues = lock_wait_queues(); + remove_waiter(&mut wait_queues, key, current.id()); + + ParkResult + } + + pub unsafe fn unpark_one(key: usize, callback: impl FnOnce(UnparkResult) -> UnparkToken) { + let waiter = { + let mut wait_queues = lock_wait_queues(); + let waiter = wait_queues.get_mut(&key).and_then(VecDeque::pop_front); + + if wait_queues.get(&key).is_some_and(VecDeque::is_empty) { + wait_queues.remove(&key); + } + + waiter + }; + + let _ = callback(UnparkResult); + + if let Some(waiter) = waiter { + waiter.unpark(); + } + } + + pub unsafe fn unpark_all(key: usize, _token: UnparkToken) { + let waiters = { + let mut wait_queues = lock_wait_queues(); + wait_queues + .remove(&key) + .unwrap_or_default() + .into_iter() + .collect::>() + }; + + // Wake waiters after dropping the queue mutex so they can immediately + // re-register themselves if the outer lock algorithm loops. + for waiter in waiters { + waiter.unpark(); + } + } +} diff --git a/tests/shuttle.rs b/tests/shuttle.rs new file mode 100644 index 00000000..5506c140 --- /dev/null +++ b/tests/shuttle.rs @@ -0,0 +1,145 @@ +use dashmap::DashMap; +use shuttle::sync::atomic::{AtomicBool, Ordering}; +use shuttle::{check_random, thread}; +use std::sync::Arc; + +fn test_map() -> DashMap { + DashMap::with_shard_amount(4) +} + +#[test] +fn concurrent_insert_and_read() { + check_random( + || { + let map = Arc::new(test_map()); + let map2 = Arc::clone(&map); + + let t1 = thread::spawn(move || { + map2.insert(1, "a"); + }); + + map.insert(2, "b"); + t1.join().expect("insert thread panicked"); + + assert_eq!(map.len(), 2); + }, + 1000, + ); +} + +#[test] +fn concurrent_insert_and_remove() { + check_random( + || { + let map = Arc::new(test_map()); + map.insert(1, "initial"); + + let map2 = Arc::clone(&map); + let t1 = thread::spawn(move || { + map2.remove(&1); + }); + + let map3 = Arc::clone(&map); + let t2 = thread::spawn(move || { + map3.insert(1, "replaced"); + }); + + t1.join().expect("remove thread panicked"); + t2.join().expect("insert thread panicked"); + + match map.get(&1) { + Some(value) => assert_eq!(*value, "replaced"), + None => {} + }; + }, + 1000, + ); +} + +#[test] +fn concurrent_entry_api() { + check_random( + || { + let map = Arc::new(test_map()); + let map2 = Arc::clone(&map); + + let t1 = thread::spawn(move || { + map2.entry(1).or_insert("first"); + }); + + map.entry(1).or_insert("second"); + t1.join().expect("entry thread panicked"); + + let value = map.get(&1).unwrap(); + assert!(*value == "first" || *value == "second"); + }, + 1000, + ); +} + +#[test] +fn writer_waits_for_active_reader() { + check_random( + || { + let map = Arc::new(test_map()); + map.insert(1, "initial"); + + let reader_map = Arc::clone(&map); + let writer_map = Arc::clone(&map); + let reader_ready = Arc::new(AtomicBool::new(false)); + + let reader_ready_for_reader = Arc::clone(&reader_ready); + let reader = thread::spawn(move || { + let value = reader_map.get(&1).expect("missing value"); + reader_ready_for_reader.store(true, Ordering::Release); + thread::yield_now(); + assert_eq!(*value, "initial"); + }); + + let reader_ready_for_writer = Arc::clone(&reader_ready); + let writer = thread::spawn(move || { + while !reader_ready_for_writer.load(Ordering::Acquire) { + thread::yield_now(); + } + writer_map.insert(1, "updated"); + }); + + reader.join().expect("reader thread panicked"); + writer.join().expect("writer thread panicked"); + + assert_eq!(*map.get(&1).expect("missing updated value"), "updated"); + }, + 1000, + ); +} + +#[test] +fn downgrade_wakes_waiting_reader() { + check_random( + || { + let map = Arc::new(test_map()); + map.insert(1, "initial"); + + let writer_map = Arc::clone(&map); + let reader_map = Arc::clone(&map); + + let writer = thread::spawn(move || { + let write_ref = writer_map.get_mut(&1).expect("missing value"); + thread::yield_now(); + + let read_ref = write_ref.downgrade(); + thread::yield_now(); + assert_eq!(*read_ref, "initial"); + }); + + let reader = thread::spawn(move || { + let value = reader_map.get(&1).expect("missing value"); + assert_eq!(*value, "initial"); + }); + + writer.join().expect("writer thread panicked"); + reader.join().expect("reader thread panicked"); + }, + 1000, + ); +} From bdb7500495cdc7650366db136273286c2f51d3b4 Mon Sep 17 00:00:00 2001 From: Istvan Date: Sat, 25 Apr 2026 01:00:52 +0200 Subject: [PATCH 2/2] Force stable toolchain in Shuttle CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a28e2a2..55109a26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v2 - uses: dtolnay/rust-toolchain@stable - name: test - run: cargo test --features shuttle --test shuttle + run: RUSTUP_TOOLCHAIN=stable cargo test --features shuttle --test shuttle test: runs-on: ubuntu-22.04