From c7e2ce39ffb025175feab9338b5e4e63dc6cf6bc Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Mon, 2 Nov 2026 11:06:09 +0100 Subject: [PATCH 1/3] Add concurrency scenarios for schedule explorers Small multi-threaded scenarios in nomt/tests/concurrency_scenarios.rs, meant to run one at a time under FFS or shuttle (they are #[ignore]d natively): witness assembly with concurrent workers, coexisting sessions with rollback and with warm-up, overlay commit vs a child session, commit vs readers, rollback vs readers, and nested sessions vs a commit. Three of them deadlock on master. Hooks for the explorers: - cfg(nomt_sync_io) selects the blocking pread/pwrite I/O backend on Linux, so I/O completions are ordinary thread events rather than kernel-side io_uring state. - cfg(shuttle) spawns fsyncer threads through shuttle and bypasses the page pool's thread-local freelist, which would otherwise stay borrowed across a scheduling point (all shuttle threads share one OS thread). Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 172 ++++++++++++- nomt/Cargo.toml | 5 +- nomt/src/io/fsyncer.rs | 6 +- nomt/src/io/mod.rs | 4 +- nomt/src/io/page_pool.rs | 17 ++ nomt/tests/concurrency_scenarios.rs | 376 ++++++++++++++++++++++++++++ 6 files changed, 566 insertions(+), 14 deletions(-) create mode 100644 nomt/tests/concurrency_scenarios.rs diff --git a/Cargo.lock b/Cargo.lock index 9b98a8c5bd..60d0f14196 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "assoc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8" + [[package]] name = "autocfg" version = "1.3.0" @@ -339,12 +345,31 @@ dependencies = [ "sha2", ] +[[package]] +name = "const-siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03efed02df0504d71e44bfd51d3329f401b8303a2fd14254acf05c95a0af0153" + [[package]] name = "constant_time_eq" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "corosensei" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6886a0c0f263965933c438626e7179139a62b978a33aa18281cbf0cd5a975f34" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1081,9 +1106,10 @@ dependencies = [ "nomt-test-utils", "parking_lot", "quickcheck", - "rand", - "rand_pcg", + "rand 0.10.1", + "rand_pcg 0.10.2", "serde", + "shuttle", "slab", "tempfile", "thread_local", @@ -1125,8 +1151,8 @@ version = "0.1.0" dependencies = [ "nomt-core", "quickcheck", - "rand", - "rand_pcg", + "rand 0.10.1", + "rand_pcg 0.10.2", ] [[package]] @@ -1176,6 +1202,12 @@ version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +[[package]] +name = "owo-colors" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" + [[package]] name = "page_size" version = "0.6.0" @@ -1275,6 +1307,15 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1311,7 +1352,7 @@ checksum = "95c589f335db0f6aaa168a7cd27b1fc6920f5e1470c804f814d9cd6e62a0f70b" dependencies = [ "env_logger", "log", - "rand", + "rand 0.10.1", ] [[package]] @@ -1335,6 +1376,17 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.10.1" @@ -1346,11 +1398,24 @@ dependencies = [ "rand_core 0.10.1", ] +[[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 0.6.4", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] [[package]] name = "rand_core" @@ -1365,7 +1430,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" dependencies = [ "num-traits", - "rand", + "rand 0.10.1", +] + +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core 0.6.4", ] [[package]] @@ -1571,6 +1645,75 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shuttle" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84550ec050f1512e6b8067087db7d00547f0ba0c1e2273f4920a573cd5a128d" +dependencies = [ + "bitvec", + "cfg-if", + "const-siphasher", + "corosensei", + "hex", + "owo-colors", + "rand 0.8.8", + "rand_core 0.6.4", + "rand_pcg 0.3.1", + "scoped-tls", + "shuttle-engine", + "shuttle-schedulers", + "shuttle-std", + "tracing", +] + +[[package]] +name = "shuttle-engine" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6dad68d0b9d252e56ba6374e05b542b659ee076cf7f319f7c1312d5879d65" +dependencies = [ + "assoc", + "bitvec", + "cfg-if", + "const-siphasher", + "corosensei", + "hex", + "owo-colors", + "rand 0.8.8", + "rand_core 0.6.4", + "rand_pcg 0.3.1", + "scoped-tls", + "smallvec", + "tracing", +] + +[[package]] +name = "shuttle-schedulers" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7673a579e1660404067f5aa1344d6530f15b59f53b0fe98767d7ab117d14067" +dependencies = [ + "rand 0.8.8", + "rand_pcg 0.3.1", + "shuttle-engine", + "smallvec", + "tracing", +] + +[[package]] +name = "shuttle-std" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320b7adce8256548602d4930ef030ac473815a892fd252099702bebcca0d33f8" +dependencies = [ + "assoc", + "owo-colors", + "shuttle-engine", + "smallvec", + "tracing", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -1769,9 +1912,9 @@ dependencies = [ "libc", "nix", "nomt", - "rand", + "rand 0.10.1", "rand_distr", - "rand_pcg", + "rand_pcg 0.10.2", "ruint", "serde", "tempfile", @@ -1854,9 +1997,9 @@ dependencies = [ "fuser", "libc", "log", - "rand", + "rand 0.10.1", "rand_distr", - "rand_pcg", + "rand_pcg 0.10.2", "tempfile", ] @@ -2103,6 +2246,15 @@ dependencies = [ "windows-targets", ] +[[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-sys" version = "0.61.2" diff --git a/nomt/Cargo.toml b/nomt/Cargo.toml index f86120d0b0..8fc6002cb8 100644 --- a/nomt/Cargo.toml +++ b/nomt/Cargo.toml @@ -38,6 +38,9 @@ io-uring.workspace = true [target.'cfg(loom)'.dependencies] loom.workspace = true +[target.'cfg(shuttle)'.dependencies] +shuttle = "0.9.9" + [dev-dependencies] rand_pcg.workspace = true hex-literal.workspace = true @@ -50,7 +53,7 @@ blake3.workspace = true nomt-test-utils = { path = "../nomt-test-utils" } [lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(loom)'] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(loom)', 'cfg(nomt_sync_io)', 'cfg(shuttle)'] } [[bench]] name = "beatree" diff --git a/nomt/src/io/fsyncer.rs b/nomt/src/io/fsyncer.rs index 51ffbdf48c..c66087ee48 100644 --- a/nomt/src/io/fsyncer.rs +++ b/nomt/src/io/fsyncer.rs @@ -43,7 +43,11 @@ impl Fsyncer { cv: Condvar::new(), s: Mutex::new(State::Idle), }); - let _thread = std::thread::Builder::new() + #[cfg(not(shuttle))] + use std::thread::Builder; + #[cfg(shuttle)] + use shuttle::thread::Builder; + let _thread = Builder::new() .name(name) .spawn({ let shared = shared.clone(); diff --git a/nomt/src/io/mod.rs b/nomt/src/io/mod.rs index 3448d43982..4c8abd5e5c 100644 --- a/nomt/src/io/mod.rs +++ b/nomt/src/io/mod.rs @@ -11,11 +11,11 @@ use std::{ }; use threadpool::ThreadPool; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", not(nomt_sync_io)))] #[path = "linux.rs"] mod platform; -#[cfg(not(target_os = "linux"))] +#[cfg(any(not(target_os = "linux"), nomt_sync_io))] #[path = "unix.rs"] mod platform; diff --git a/nomt/src/io/page_pool.rs b/nomt/src/io/page_pool.rs index 6fa6a91db8..c67be3b25d 100644 --- a/nomt/src/io/page_pool.rs +++ b/nomt/src/io/page_pool.rs @@ -164,6 +164,17 @@ impl PagePool { /// /// The contents of the page are undefined. pub fn alloc(&self) -> Page { + // Under shuttle every thread shares one OS thread, so the thread-local freelist would be + // borrowed across the freelist lock, which is a scheduling point. + #[cfg(shuttle)] + { + let mut freelist = self.inner.freelist.write(); + if freelist.is_empty() { + self.grow(&mut freelist); + } + return freelist.pop().unwrap(); + } + #[allow(unreachable_code)] // fast path: try to serve request from the thread-local freelist. let mut tls_freelist = self.tls_freelist(); if let Some(page) = tls_freelist.pop() { @@ -187,6 +198,12 @@ impl PagePool { /// Deallocates a [`Page`]. pub fn dealloc(&self, page: Page) { + #[cfg(shuttle)] + { + self.inner.freelist.write().push(page); + return; + } + #[allow(unreachable_code)] // fast path: try to place page in thread-local freelist. let mut tls_freelist = self.tls_freelist(); tls_freelist.push(page); diff --git a/nomt/tests/concurrency_scenarios.rs b/nomt/tests/concurrency_scenarios.rs new file mode 100644 index 0000000000..59b6095073 --- /dev/null +++ b/nomt/tests/concurrency_scenarios.rs @@ -0,0 +1,376 @@ +//! Small concurrency scenarios meant to be run one at a time under a schedule explorer: +//! - FFS: `ffs explore -- --ignored --exact --test-threads=1`. All are +//! `#[ignore]`d so that plain `cargo test` is unaffected. +//! - shuttle: built with `--cfg shuttle` and the shims from `nomt-shuttle-shims`, each scenario +//! runs under `shuttle::check_pct` (or `check_random` with `SHUTTLE_SCHED=random`) for +//! `SHUTTLE_ITERS` iterations. + +mod common; + +use bitvec::prelude::*; +use common::{account_path, Test}; +use nomt::{ + hasher::Blake3Hasher, proof, trie::KeyPath, trie::LeafData, KeyReadWrite, Nomt, Options, + Root, SessionParams, +}; +#[cfg(shuttle)] +use shuttle::thread; +use std::{path::Path, sync::Arc}; +#[cfg(not(shuttle))] +use std::{sync::mpsc, thread, time::Duration}; + +type Db = Nomt; + +#[cfg(not(shuttle))] +/// Run `f` on a fresh thread and panic if it doesn't finish within `secs`, so a deadlock +/// surfaces as a failure instead of a hung test binary. +/// `CS_NO_DEADLINE=1` runs `f` inline instead, leaving hang detection to the explorer. +fn with_deadline(secs: u64, f: impl FnOnce() + Send + 'static) { + if std::env::var_os("CS_NO_DEADLINE").is_some() { + return f(); + } + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + f(); + let _ = tx.send(()); + }); + match rx.recv_timeout(Duration::from_secs(secs)) { + Ok(()) => {} + Err(mpsc::RecvTimeoutError::Timeout) => panic!("scenario did not finish in {secs}s"), + Err(mpsc::RecvTimeoutError::Disconnected) => panic!("scenario panicked"), + } +} + +fn open(path: &Path, configure: impl FnOnce(&mut Options)) -> Db { + let mut o = Options::new(); + o.path(path); + o.bitbox_seed([0; 16]); + o.hashtable_buckets(4096); + o.io_workers(1); + o.commit_concurrency(1); + configure(&mut o); + Nomt::open(o).unwrap() +} + +fn value(round: u64) -> Vec { + round.to_le_bytes().to_vec() +} + +fn write_all(keys: &[KeyPath], round: u64) -> Vec<(KeyPath, KeyReadWrite)> { + let mut actuals: Vec<_> = keys + .iter() + .map(|k| (*k, KeyReadWrite::Write(Some(value(round))))) + .collect(); + actuals.sort_by_key(|(k, _)| *k); + actuals +} + +fn commit(db: &Db, actuals: Vec<(KeyPath, KeyReadWrite)>) -> Root { + let session = db.begin_session(SessionParams::default()); + for (k, _) in &actuals { + session.warm_up(*k); + session.preserve_prior_value(*k); + } + let finished = session.finish(actuals).unwrap(); + let root = finished.root(); + finished.commit(db).unwrap(); + root +} + +fn keys(n: u64) -> Vec { + (0..n).map(account_path).collect() +} + +/// Positive control: witness operations must refer to the path proof covering their key when +/// several commit workers finish in arbitrary order (fixed by b6ff34d, not on master). +fn scenario_witness_concurrent_workers() { + let mut t = Test::new_with_params(common::fresh_test_name("cs_witness"), 4, 4096, None, true); + for i in 0..200 { + common::set_balance(&mut t, i, 1000); + } + let (mut prev_root, _) = t.commit(); + + for round in 0..3 { + for i in 0..100 { + t.read_id(i); + } + for i in 100..200 { + common::set_balance(&mut t, i, round); + } + let (new_root, witness) = t.commit(); + + let verified = witness + .path_proofs + .iter() + .map(|p| { + p.inner + .verify::(&p.path.path(), prev_root.into_inner()) + .unwrap() + }) + .collect::>(); + + for read in &witness.operations.reads { + let leaf = LeafData { + key_path: read.key, + value_hash: read.value.unwrap(), + }; + assert!(verified[read.path_index].confirm_value(&leaf).unwrap()); + } + + let mut ops_by_path = std::collections::BTreeMap::>::new(); + for write in &witness.operations.writes { + ops_by_path + .entry(write.path_index) + .or_default() + .push((write.key, write.value)); + } + let mut updates = ops_by_path + .into_iter() + .map(|(i, ops)| proof::PathUpdate { + inner: verified[i].clone(), + ops, + }) + .collect::>(); + updates.sort_by(|a, b| a.inner.path().partial_cmp(b.inner.path()).unwrap()); + assert_eq!( + proof::verify_update::(prev_root.into_inner(), &updates).unwrap(), + new_root.into_inner(), + ); + prev_root = new_root; + } +} + +/// Three coexisting sessions with rollback enabled; the rollback worker pool has 2 threads and +/// each live session's reverse-delta worker occupies one until that session ends. +fn scenario_rollback_three_sessions() { + let dir = tempfile::tempdir().unwrap(); + let db = open(dir.path(), |o| o.rollback(true)); + let ks = keys(8); + commit(&db, write_all(&ks, 1)); + + let s1 = db.begin_session(SessionParams::default()); + let s2 = db.begin_session(SessionParams::default()); + let s3 = db.begin_session(SessionParams::default()); + for k in &ks { + s3.preserve_prior_value(*k); + } + let _ = s3.finish(write_all(&ks, 2)).unwrap(); + drop((s1, s2)); +} + +/// Two coexisting sessions with warm-up and a single commit worker: the first session's warm-up +/// task holds the only `nomt-commit` thread until that session finishes. +fn scenario_warm_up_two_sessions() { + let dir = tempfile::tempdir().unwrap(); + let db = open(dir.path(), |o| o.warm_up(true)); + let ks = keys(8); + commit(&db, write_all(&ks, 1)); + + let s1 = db.begin_session(SessionParams::default()); + let s2 = db.begin_session(SessionParams::default()); + let _ = s2.finish(write_all(&ks, 2)).unwrap(); + drop(s1); +} + +/// Commit an overlay on one thread while another builds a session on top of its child. +/// Either the child is rejected as incomplete, or the session must see both overlays' writes. +fn scenario_overlay_commit_vs_child_session() { + let dir = tempfile::tempdir().unwrap(); + let db = Arc::new(open(dir.path(), |o| o.commit_concurrency(2))); + let k1 = account_path(1); + let k2 = account_path(2); + commit(&db, write_all(&[k1, k2], 0)); + + let o1 = db + .begin_session(SessionParams::default()) + .finish(write_all(&[k1], 1)) + .unwrap() + .into_overlay(); + let o2 = Arc::new( + db.begin_session(SessionParams::default().overlay([&o1]).unwrap()) + .finish(write_all(&[k2], 2)) + .unwrap() + .into_overlay(), + ); + let o2_root = o2.root(); + + let committer = thread::spawn({ + let db = db.clone(); + move || o1.commit(&db).unwrap() + }); + let reader = thread::spawn({ + let db = db.clone(); + let o2 = o2.clone(); + move || { + if let Ok(params) = SessionParams::default().overlay([&*o2]) { + let s = db.begin_session(params); + assert_eq!(s.read(k1).unwrap(), Some(value(1))); + assert_eq!(s.read(k2).unwrap(), Some(value(2))); + assert_eq!(s.prev_root(), o2_root); + } + } + }); + committer.join().unwrap(); + reader.join().unwrap(); + + Arc::into_inner(o2).unwrap().commit(&db).unwrap(); + assert_eq!(db.root(), o2_root); +} + +/// One thread commits rounds that rewrite every key; another opens sessions and checks it sees +/// a single round across all keys, and that proofs verify against the session's root. +fn scenario_commit_vs_readers() { + let dir = tempfile::tempdir().unwrap(); + let db = Arc::new(open(dir.path(), |o| o.commit_concurrency(2))); + let ks = keys(16); + commit(&db, write_all(&ks, 0)); + const ROUNDS: u64 = 3; + + let writer = thread::spawn({ + let db = db.clone(); + let ks = ks.clone(); + move || { + for round in 1..=ROUNDS { + commit(&db, write_all(&ks, round)); + } + } + }); + let reader = thread::spawn({ + let db = db.clone(); + let ks = ks.clone(); + move || { + for _ in 0..ROUNDS { + let s = db.begin_session(SessionParams::default()); + let seen = s.read(ks[0]).unwrap(); + for k in &ks { + assert_eq!(s.read(*k).unwrap(), seen, "torn snapshot"); + } + let proof = s.prove(ks[3]).unwrap(); + let verified = proof + .verify::(ks[3].view_bits::(), s.prev_root().into_inner()) + .unwrap(); + let leaf = LeafData { + key_path: ks[3], + value_hash: *blake3::hash(seen.as_ref().unwrap()).as_bytes(), + }; + assert!(verified.confirm_value(&leaf).unwrap()); + assert_eq!(db.read(ks[5]).unwrap().is_some(), true); + } + } + }); + writer.join().unwrap(); + reader.join().unwrap(); + assert_eq!(db.read(ks[0]).unwrap(), Some(value(ROUNDS))); +} + +/// A thread holding a session opens a second one while another thread commits. Both sessions take +/// `access_lock` for reading; parking_lot's fair `RwLock` makes the second read wait behind a +/// queued writer, which itself waits for the first session. +fn scenario_nested_session_vs_commit() { + let dir = tempfile::tempdir().unwrap(); + let db = Arc::new(open(dir.path(), |_| {})); + let ks = keys(4); + commit(&db, write_all(&ks, 0)); + + let writer = thread::spawn({ + let db = db.clone(); + let ks = ks.clone(); + move || { + commit(&db, write_all(&ks, 1)); + } + }); + let reader = thread::spawn({ + let db = db.clone(); + move || { + let s1 = db.begin_session(SessionParams::default()); + let s2 = db.begin_session(SessionParams::default()); + drop((s1, s2)); + } + }); + writer.join().unwrap(); + reader.join().unwrap(); +} + +/// Rollback racing with readers: readers must always see a whole round, and the final root must +/// be the one recorded before the rolled-back commit. +fn scenario_rollback_vs_readers() { + let dir = tempfile::tempdir().unwrap(); + let db = Arc::new(open(dir.path(), |o| { + o.rollback(true); + o.commit_concurrency(2); + })); + let ks = keys(16); + let root0 = commit(&db, write_all(&ks, 0)); + commit(&db, write_all(&ks, 1)); + + let roller = thread::spawn({ + let db = db.clone(); + move || db.rollback(1).unwrap() + }); + let reader = thread::spawn({ + let db = db.clone(); + let ks = ks.clone(); + move || { + for _ in 0..2 { + let s = db.begin_session(SessionParams::default()); + let seen = s.read(ks[0]).unwrap(); + assert!(seen == Some(value(0)) || seen == Some(value(1))); + for k in &ks { + assert_eq!(s.read(*k).unwrap(), seen, "torn snapshot"); + } + } + } + }); + roller.join().unwrap(); + reader.join().unwrap(); + assert_eq!(db.root(), root0); + assert_eq!(db.read(ks[7]).unwrap(), Some(value(0))); +} + +#[cfg(not(shuttle))] +macro_rules! scenario_tests { + ($($name:ident => $scenario:ident),* $(,)?) => { + $( + #[test] + #[ignore] + fn $name() { + with_deadline(60, $scenario); + } + )* + }; +} + +#[cfg(shuttle)] +fn run_shuttle(f: fn()) { + use shuttle::scheduler::{PctScheduler, RandomScheduler}; + let iters = std::env::var("SHUTTLE_ITERS").map_or(100, |v| v.parse().unwrap()); + let mut config = shuttle::Config::new(); + // NOMT builds large arrays on the stack (e.g. the page pool's region table). + config.stack_size = 8 << 20; + match std::env::var("SHUTTLE_SCHED").as_deref() { + Ok("random") => shuttle::Runner::new(RandomScheduler::new(iters), config).run(f), + _ => shuttle::Runner::new(PctScheduler::new(3, iters), config).run(f), + }; +} + +#[cfg(shuttle)] +macro_rules! scenario_tests { + ($($name:ident => $scenario:ident),* $(,)?) => { + $( + #[test] + fn $name() { + run_shuttle($scenario); + } + )* + }; +} + +scenario_tests! { + witness_concurrent_workers => scenario_witness_concurrent_workers, + rollback_three_sessions => scenario_rollback_three_sessions, + warm_up_two_sessions => scenario_warm_up_two_sessions, + overlay_commit_vs_child_session => scenario_overlay_commit_vs_child_session, + commit_vs_readers => scenario_commit_vs_readers, + rollback_vs_readers => scenario_rollback_vs_readers, + nested_session_vs_commit => scenario_nested_session_vs_commit, +} From 33e2c4c1058867ef9540cd409d5c60c57decdec7 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Mon, 2 Nov 2026 11:06:10 +0100 Subject: [PATCH 2/3] Add shuttle shims for crossbeam-channel, parking_lot and threadpool shuttle-shims/ holds shuttle-backed stand-ins for the three crates, covering only the API NOMT uses (channels incl. Select/select!, parking_lot locks plus a Condvar, which shuttle-parking_lot lacks, and a thread pool whose workers exit when the pool is dropped). run.sh patches them in with `cargo --config patch.crates-io...` and runs the concurrency scenarios under shuttle, restoring Cargo.lock afterwards. The directory is excluded from the workspace. Co-Authored-By: Claude Opus 5.5 --- Cargo.toml | 2 +- shuttle-shims/.gitignore | 3 + shuttle-shims/README.md | 23 ++ shuttle-shims/crossbeam-channel/Cargo.toml | 12 + shuttle-shims/crossbeam-channel/src/lib.rs | 360 +++++++++++++++++++++ shuttle-shims/parking_lot/Cargo.toml | 14 + shuttle-shims/parking_lot/src/lib.rs | 75 +++++ shuttle-shims/run.sh | 18 ++ shuttle-shims/threadpool/Cargo.toml | 8 + shuttle-shims/threadpool/src/lib.rs | 144 +++++++++ 10 files changed, 658 insertions(+), 1 deletion(-) create mode 100644 shuttle-shims/.gitignore create mode 100644 shuttle-shims/README.md create mode 100644 shuttle-shims/crossbeam-channel/Cargo.toml create mode 100644 shuttle-shims/crossbeam-channel/src/lib.rs create mode 100644 shuttle-shims/parking_lot/Cargo.toml create mode 100644 shuttle-shims/parking_lot/src/lib.rs create mode 100755 shuttle-shims/run.sh create mode 100644 shuttle-shims/threadpool/Cargo.toml create mode 100644 shuttle-shims/threadpool/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index f617240e21..d44640a9ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ "trickfs", "trickfs/trickmnt", ] -exclude = ["benchtop"] +exclude = ["benchtop", "shuttle-shims"] [workspace.package] authors = ["thrum"] diff --git a/shuttle-shims/.gitignore b/shuttle-shims/.gitignore new file mode 100644 index 0000000000..40c0451d7f --- /dev/null +++ b/shuttle-shims/.gitignore @@ -0,0 +1,3 @@ +target/ +Cargo.lock +.Cargo.lock.bak diff --git a/shuttle-shims/README.md b/shuttle-shims/README.md new file mode 100644 index 0000000000..df6931560a --- /dev/null +++ b/shuttle-shims/README.md @@ -0,0 +1,23 @@ +# shuttle-shims + +Shuttle-backed stand-ins for `crossbeam-channel`, `parking_lot` and `threadpool`. `run.sh` injects them into a NOMT build with `cargo --config 'patch.crates-io..path=...'`, so NOMT runs under the [shuttle](https://github.com/awslabs/shuttle) scheduler without changing its dependency list. Only the API surface NOMT uses is implemented. The directory is excluded from the workspace. + +Run the scenarios in `nomt/tests/concurrency_scenarios.rs` under shuttle: + +```sh +shuttle-shims/run.sh # all scenarios, PCT, 100 iterations each +SHUTTLE_ITERS=5000 shuttle-shims/run.sh nested_session_vs_commit +SHUTTLE_SCHED=random shuttle-shims/run.sh +SHUTTLE_CAPTURE_BACKTRACE=1 shuttle-shims/run.sh # per-task backtraces on deadlock +``` + +The build uses `--cfg shuttle --cfg nomt_sync_io`: `nomt_sync_io` swaps io_uring for the blocking pread/pwrite backend, whose completions shuttle can schedule. `Cargo.lock` is restored after the run, since the patches rewrite it. + +The same scenarios run natively (`cargo test -p nomt --release --test concurrency_scenarios -- --ignored`) and under [FFS](https://github.com/aoli-al/ffs): + +```sh +CS_NO_DEADLINE=1 ffs explore --iterations 600 --policy pct -- \ + --ignored --exact --test-threads=1 +``` + +FFS failures on NOMT have not re-executed reliably from its saved schedules; confirm them under shuttle. diff --git a/shuttle-shims/crossbeam-channel/Cargo.toml b/shuttle-shims/crossbeam-channel/Cargo.toml new file mode 100644 index 0000000000..484ffd7c9e --- /dev/null +++ b/shuttle-shims/crossbeam-channel/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "crossbeam-channel" +version = "0.5.15" +edition = "2021" +publish = false + +[dependencies] +shuttle = "0.9.9" + +[features] +default = ["std"] +std = [] diff --git a/shuttle-shims/crossbeam-channel/src/lib.rs b/shuttle-shims/crossbeam-channel/src/lib.rs new file mode 100644 index 0000000000..e4543ebfd4 --- /dev/null +++ b/shuttle-shims/crossbeam-channel/src/lib.rs @@ -0,0 +1,360 @@ +//! Shuttle-backed subset of `crossbeam-channel` 0.5: `bounded`/`unbounded` MPMC channels, +//! `Select::{recv, ready}` and a `select!` limited to `recv(..) -> .. => ..` arms. + +use shuttle::sync::{Condvar, Mutex}; +use std::{collections::VecDeque, fmt, sync::Arc}; + +struct Waker { + fired: Mutex, + cv: Condvar, +} + +impl Waker { + fn fire(&self) { + *self.fired.lock().unwrap() = true; + self.cv.notify_all(); + } +} + +struct State { + queue: VecDeque, + senders: usize, + receivers: usize, + pushed: u64, + popped: u64, + selectors: Vec>, +} + +struct Chan { + cap: Option, + state: Mutex>, + cv: Condvar, +} + +impl Chan { + fn notify(&self, state: &State) { + self.cv.notify_all(); + for w in &state.selectors { + w.fire(); + } + } +} + +trait Selectable { + fn is_ready(&self) -> bool; + fn register(&self, waker: &Arc); + fn unregister(&self, waker: &Arc); +} + +impl Selectable for Chan { + fn is_ready(&self) -> bool { + let s = self.state.lock().unwrap(); + !s.queue.is_empty() || s.senders == 0 + } + fn register(&self, waker: &Arc) { + self.state.lock().unwrap().selectors.push(waker.clone()); + } + fn unregister(&self, waker: &Arc) { + self.state + .lock() + .unwrap() + .selectors + .retain(|w| !Arc::ptr_eq(w, waker)); + } +} + +pub struct Sender(Arc>); +pub struct Receiver(Arc>); + +fn channel(cap: Option) -> (Sender, Receiver) { + let chan = Arc::new(Chan { + cap, + state: Mutex::new(State { + queue: VecDeque::new(), + senders: 1, + receivers: 1, + pushed: 0, + popped: 0, + selectors: Vec::new(), + }), + cv: Condvar::new(), + }); + (Sender(chan.clone()), Receiver(chan)) +} + +pub fn bounded(cap: usize) -> (Sender, Receiver) { + channel(Some(cap)) +} + +pub fn unbounded() -> (Sender, Receiver) { + channel(None) +} + +impl Sender { + pub fn send(&self, msg: T) -> Result<(), SendError> { + let chan = &*self.0; + let mut s = chan.state.lock().unwrap(); + let limit = chan.cap.map(|c| c.max(1)); + loop { + if s.receivers == 0 { + return Err(SendError(msg)); + } + match limit { + Some(l) if s.queue.len() >= l => s = chan.cv.wait(s).unwrap(), + _ => break, + } + } + s.queue.push_back(msg); + s.pushed += 1; + let seq = s.pushed; + chan.notify(&s); + if chan.cap == Some(0) { + // Rendezvous: wait until a receiver has taken this message. + while s.popped < seq && s.receivers > 0 { + s = chan.cv.wait(s).unwrap(); + } + } + Ok(()) + } +} + +impl Clone for Sender { + fn clone(&self) -> Self { + self.0.state.lock().unwrap().senders += 1; + Sender(self.0.clone()) + } +} + +impl Drop for Sender { + fn drop(&mut self) { + let mut s = self.0.state.lock().unwrap(); + s.senders -= 1; + if s.senders == 0 { + self.0.notify(&s); + } + } +} + +impl Receiver { + fn pop(&self, s: &mut State) -> Option { + let msg = s.queue.pop_front()?; + s.popped += 1; + self.0.notify(s); + Some(msg) + } + + pub fn recv(&self) -> Result { + let mut s = self.0.state.lock().unwrap(); + loop { + if let Some(msg) = self.pop(&mut s) { + return Ok(msg); + } + if s.senders == 0 { + return Err(RecvError); + } + s = self.0.cv.wait(s).unwrap(); + } + } + + pub fn try_recv(&self) -> Result { + let mut s = self.0.state.lock().unwrap(); + match self.pop(&mut s) { + Some(msg) => Ok(msg), + None if s.senders == 0 => Err(TryRecvError::Disconnected), + None => Err(TryRecvError::Empty), + } + } + + pub fn len(&self) -> usize { + self.0.state.lock().unwrap().queue.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&self) -> Iter<'_, T> { + Iter(self) + } +} + +impl Clone for Receiver { + fn clone(&self) -> Self { + self.0.state.lock().unwrap().receivers += 1; + Receiver(self.0.clone()) + } +} + +impl Drop for Receiver { + fn drop(&mut self) { + let mut s = self.0.state.lock().unwrap(); + s.receivers -= 1; + if s.receivers == 0 { + self.0.notify(&s); + } + } +} + +pub struct Iter<'a, T>(&'a Receiver); + +impl Iterator for Iter<'_, T> { + type Item = T; + fn next(&mut self) -> Option { + self.0.recv().ok() + } +} + +pub struct IntoIter(Receiver); + +impl Iterator for IntoIter { + type Item = T; + fn next(&mut self) -> Option { + self.0.recv().ok() + } +} + +impl IntoIterator for Receiver { + type Item = T; + type IntoIter = IntoIter; + fn into_iter(self) -> IntoIter { + IntoIter(self) + } +} + +impl<'a, T> IntoIterator for &'a Receiver { + type Item = T; + type IntoIter = Iter<'a, T>; + fn into_iter(self) -> Iter<'a, T> { + self.iter() + } +} + +impl fmt::Debug for Sender { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("Sender { .. }") + } +} + +impl fmt::Debug for Receiver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("Receiver { .. }") + } +} + +/// Waits on several receivers. `ready` returns the index of one that has a message or is +/// disconnected; when several are, shuttle's random source picks, so the choice is explored. +pub struct Select<'a> { + chans: Vec<&'a dyn Selectable>, +} + +impl<'a> Select<'a> { + pub fn new() -> Self { + Select { chans: Vec::new() } + } + + pub fn recv(&mut self, r: &'a Receiver) -> usize { + self.chans.push(&*r.0); + self.chans.len() - 1 + } + + pub fn ready(&mut self) -> usize { + let waker = Arc::new(Waker { + fired: Mutex::new(false), + cv: Condvar::new(), + }); + for c in &self.chans { + c.register(&waker); + } + let index = loop { + let ready: Vec = (0..self.chans.len()) + .filter(|&i| self.chans[i].is_ready()) + .collect(); + if !ready.is_empty() { + use shuttle::rand::Rng; + break ready[shuttle::rand::thread_rng().gen_range(0..ready.len())]; + } + let mut fired = waker.fired.lock().unwrap(); + while !*fired { + fired = waker.cv.wait(fired).unwrap(); + } + *fired = false; + }; + for c in &self.chans { + c.unregister(&waker); + } + index + } +} + +impl Default for Select<'_> { + fn default() -> Self { + Self::new() + } +} + +/// Only `recv(rx) -> pat => body` arms. Assumes the selected receiver isn't drained by another +/// consumer between `ready` and `recv`, which holds for NOMT's single-consumer uses. +#[macro_export] +macro_rules! select { + (@arms $index:ident, $k:expr;) => { unreachable!() }; + (@arms $index:ident, $k:expr; recv($rx:expr) -> $res:pat => $body:expr $(, recv($rxs:expr) -> $ress:pat => $bodies:expr)*) => { + if $index == $k { + let $res = $rx.recv(); + $body + } else { + $crate::select!(@arms $index, $k + 1; $(recv($rxs) -> $ress => $bodies),*) + } + }; + ($(recv($rx:expr) -> $res:pat => $body:expr),+ $(,)?) => {{ + let mut __sel = $crate::Select::new(); + $( __sel.recv(&$rx); )+ + let __index = __sel.ready(); + drop(__sel); + $crate::select!(@arms __index, 0usize; $(recv($rx) -> $res => $body),+) + }}; +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub struct SendError(pub T); + +impl fmt::Debug for SendError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("SendError { .. }") + } +} + +impl fmt::Display for SendError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("sending on a disconnected channel") + } +} + +impl std::error::Error for SendError {} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub struct RecvError; + +impl fmt::Display for RecvError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("receiving on an empty and disconnected channel") + } +} + +impl std::error::Error for RecvError {} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum TryRecvError { + Empty, + Disconnected, +} + +impl fmt::Display for TryRecvError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TryRecvError::Empty => f.pad("receiving on an empty channel"), + TryRecvError::Disconnected => f.pad("receiving on an empty and disconnected channel"), + } + } +} + +impl std::error::Error for TryRecvError {} diff --git a/shuttle-shims/parking_lot/Cargo.toml b/shuttle-shims/parking_lot/Cargo.toml new file mode 100644 index 0000000000..9d2bd699d9 --- /dev/null +++ b/shuttle-shims/parking_lot/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "parking_lot" +version = "0.12.3" +edition = "2021" +publish = false + +[features] +arc_lock = ["shuttle-parking_lot-impl/arc_lock"] +send_guard = ["shuttle-parking_lot-impl/send_guard"] + +[dependencies] +shuttle = "0.9.9" +shuttle-parking_lot-impl = "0.1.3" +lock_api = "0.4" diff --git a/shuttle-shims/parking_lot/src/lib.rs b/shuttle-shims/parking_lot/src/lib.rs new file mode 100644 index 0000000000..a842b8b75e --- /dev/null +++ b/shuttle-shims/parking_lot/src/lib.rs @@ -0,0 +1,75 @@ +//! `parking_lot` 0.12 under shuttle: the locks come from `shuttle-parking_lot-impl`; `Condvar`, +//! which that crate lacks, is added here for `lock_api` mutex guards. + +pub use shuttle_parking_lot_impl::*; + +use std::time::Instant; + +type Guard<'a, T> = lock_api::MutexGuard<'a, RawMutex, T>; + +pub struct WaitTimeoutResult(bool); + +impl WaitTimeoutResult { + pub fn timed_out(&self) -> bool { + self.0 + } +} + +/// Generation-counting condvar: a waiter takes the generation lock before releasing the user's +/// mutex, so a notify between the two can't be lost. +pub struct Condvar { + generation: shuttle::sync::Mutex, + cv: shuttle::sync::Condvar, +} + +impl Default for Condvar { + fn default() -> Self { + Self::new() + } +} + +impl Condvar { + pub fn new() -> Self { + Condvar { + generation: shuttle::sync::Mutex::new(0), + cv: shuttle::sync::Condvar::new(), + } + } + + pub fn wait(&self, guard: &mut Guard<'_, T>) { + let mut generation = self.generation.lock().unwrap(); + let start = *generation; + lock_api::MutexGuard::unlocked(guard, move || { + while *generation == start { + generation = self.cv.wait(generation).unwrap(); + } + }); + } + + pub fn wait_while bool>( + &self, + guard: &mut Guard<'_, T>, + mut condition: F, + ) { + while condition(&mut *guard) { + self.wait(guard); + } + } + + /// Shuttle has no clock: never times out, behaves like `wait`. + pub fn wait_until(&self, guard: &mut Guard<'_, T>, _: Instant) -> WaitTimeoutResult { + self.wait(guard); + WaitTimeoutResult(false) + } + + pub fn notify_one(&self) -> bool { + self.notify_all(); + true + } + + pub fn notify_all(&self) -> usize { + *self.generation.lock().unwrap() += 1; + self.cv.notify_all(); + 0 + } +} diff --git a/shuttle-shims/run.sh b/shuttle-shims/run.sh new file mode 100755 index 0000000000..552e7a2ff8 --- /dev/null +++ b/shuttle-shims/run.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Build and run NOMT's concurrency scenarios under shuttle. +# shuttle-shims/run.sh [cargo test args...] (NOMT=/path/to/nomt to override the checkout) +# e.g. SHUTTLE_ITERS=500 ./run.sh commit_vs_readers -- --nocapture +# Cargo.lock is restored afterwards: the patches rewrite it. +set -euo pipefail +SHIMS=$(cd "$(dirname "$0")" && pwd) +NOMT=${NOMT:-$SHIMS/..} +cp "$NOMT/Cargo.lock" "$SHIMS/.Cargo.lock.bak" +trap 'cp "$SHIMS/.Cargo.lock.bak" "$NOMT/Cargo.lock"' EXIT +cd "$NOMT" +RUSTFLAGS="--cfg shuttle --cfg nomt_sync_io" \ +CARGO_TARGET_DIR="$NOMT/target/shuttle" \ +cargo \ + --config "patch.crates-io.crossbeam-channel.path=\"$SHIMS/crossbeam-channel\"" \ + --config "patch.crates-io.parking_lot.path=\"$SHIMS/parking_lot\"" \ + --config "patch.crates-io.threadpool.path=\"$SHIMS/threadpool\"" \ + test -p nomt --release --test concurrency_scenarios "$@" diff --git a/shuttle-shims/threadpool/Cargo.toml b/shuttle-shims/threadpool/Cargo.toml new file mode 100644 index 0000000000..ddddf9b5a1 --- /dev/null +++ b/shuttle-shims/threadpool/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "threadpool" +version = "1.8.1" +edition = "2021" +publish = false + +[dependencies] +shuttle = "0.9.9" diff --git a/shuttle-shims/threadpool/src/lib.rs b/shuttle-shims/threadpool/src/lib.rs new file mode 100644 index 0000000000..43f4859fdf --- /dev/null +++ b/shuttle-shims/threadpool/src/lib.rs @@ -0,0 +1,144 @@ +//! `threadpool` 1.8 under shuttle. Workers are shuttle threads that exit once every `ThreadPool` +//! handle is dropped and the queue is drained, so an execution can end cleanly. + +use shuttle::sync::{Condvar, Mutex}; +use std::{collections::VecDeque, sync::Arc}; + +type Job = Box; + +struct State { + queue: VecDeque, + active: usize, + handles: usize, +} + +struct Inner { + state: Mutex, + work: Condvar, + idle: Condvar, + max: usize, +} + +pub struct ThreadPool(Arc); + +impl ThreadPool { + pub fn new(num_threads: usize) -> Self { + Builder::new().num_threads(num_threads).build() + } + + pub fn with_name(name: String, num_threads: usize) -> Self { + Builder::new() + .num_threads(num_threads) + .thread_name(name) + .build() + } + + pub fn execute(&self, job: F) { + self.0.state.lock().unwrap().queue.push_back(Box::new(job)); + self.0.work.notify_one(); + } + + pub fn join(&self) { + let mut s = self.0.state.lock().unwrap(); + while !s.queue.is_empty() || s.active > 0 { + s = self.0.idle.wait(s).unwrap(); + } + } + + pub fn max_count(&self) -> usize { + self.0.max + } + + pub fn queued_count(&self) -> usize { + self.0.state.lock().unwrap().queue.len() + } + + pub fn active_count(&self) -> usize { + self.0.state.lock().unwrap().active + } +} + +impl Clone for ThreadPool { + fn clone(&self) -> Self { + self.0.state.lock().unwrap().handles += 1; + ThreadPool(self.0.clone()) + } +} + +impl Drop for ThreadPool { + fn drop(&mut self) { + self.0.state.lock().unwrap().handles -= 1; + self.0.work.notify_all(); + } +} + +fn worker(inner: Arc) { + loop { + let job = { + let mut s = inner.state.lock().unwrap(); + loop { + if let Some(job) = s.queue.pop_front() { + s.active += 1; + break job; + } + if s.handles == 0 { + return; + } + s = inner.work.wait(s).unwrap(); + } + }; + job(); + inner.state.lock().unwrap().active -= 1; + inner.idle.notify_all(); + } +} + +#[derive(Default)] +pub struct Builder { + num_threads: Option, + name: Option, +} + +impl Builder { + pub fn new() -> Self { + Self::default() + } + + pub fn num_threads(mut self, n: usize) -> Self { + assert!(n > 0); + self.num_threads = Some(n); + self + } + + pub fn thread_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + pub fn thread_stack_size(self, _: usize) -> Self { + self + } + + pub fn build(self) -> ThreadPool { + let max = self.num_threads.unwrap_or(1); + let inner = Arc::new(Inner { + state: Mutex::new(State { + queue: VecDeque::new(), + active: 0, + handles: 1, + }), + work: Condvar::new(), + idle: Condvar::new(), + max, + }); + for _ in 0..max { + let mut b = shuttle::thread::Builder::new(); + if let Some(name) = &self.name { + b = b.name(name.clone()); + } + let inner = inner.clone(); + b.spawn(move || worker(inner)).unwrap(); + } + ThreadPool(inner) + } +} From c78a3c6f4e6584b4e9a59f1f8a93e284a83a7090 Mon Sep 17 00:00:00 2001 From: Nikolai Golub Date: Mon, 2 Nov 2026 12:19:54 +0100 Subject: [PATCH 3/3] Fix formatting and update benchtop's Cargo.lock benchtop locks nomt's dependencies separately, so it needs the shuttle target dependency too. shuttle requires rand >= 0.8.6 and, through corosensei, libc >= 0.2.159; libc is pinned to 0.2.186 to match the workspace lock. Co-Authored-By: Claude Opus 5.5 --- benchtop/Cargo.lock | 219 +++++++++++++++++++++------- nomt/src/io/fsyncer.rs | 4 +- nomt/tests/concurrency_scenarios.rs | 4 +- 3 files changed, 173 insertions(+), 54 deletions(-) diff --git a/benchtop/Cargo.lock b/benchtop/Cargo.lock index 587de333ed..949de4bac3 100644 --- a/benchtop/Cargo.lock +++ b/benchtop/Cargo.lock @@ -289,7 +289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.8", ] [[package]] @@ -310,6 +310,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "assoc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8" + [[package]] name = "async-trait" version = "0.1.80" @@ -450,7 +456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" dependencies = [ "bitcoin_hashes 0.11.0", - "rand 0.8.5", + "rand 0.8.8", "rand_core 0.6.4", "serde", "unicode-normalization", @@ -762,7 +768,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -828,6 +834,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03efed02df0504d71e44bfd51d3329f401b8303a2fd14254acf05c95a0af0153" + [[package]] name = "constant_time_eq" version = "0.3.0" @@ -852,6 +864,19 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "corosensei" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6886a0c0f263965933c438626e7179139a62b978a33aa18281cbf0cd5a975f34" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", +] + [[package]] name = "cpp_demangle" version = "0.3.5" @@ -1254,7 +1279,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand 0.8.5", + "rand 0.8.8", "rustc-hex", "static_assertions", ] @@ -1478,7 +1503,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" dependencies = [ - "rand 0.8.5", + "rand 0.8.8", "rand_core 0.6.4", ] @@ -1933,9 +1958,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -1944,7 +1969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] @@ -1982,7 +2007,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand 0.8.5", + "rand 0.8.8", "serde", "sha2 0.9.9", ] @@ -2228,6 +2253,7 @@ dependencies = [ "nomt-core", "parking_lot", "rand 0.10.1", + "shuttle", "slab", "thread_local", "threadpool", @@ -2358,6 +2384,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" + [[package]] name = "parity-bip39" version = "2.0.1" @@ -2365,7 +2397,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e69bf016dc406eff7d53a7d3f7cf1c2e72c82b9088aac1118591e36dd2cd3e9" dependencies = [ "bitcoin_hashes 0.13.0", - "rand 0.8.5", + "rand 0.8.8", "rand_core 0.6.4", "serde", "unicode-normalization", @@ -2725,9 +2757,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2815,6 +2847,15 @@ dependencies = [ "rand 0.10.1", ] +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -3308,6 +3349,75 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shuttle" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84550ec050f1512e6b8067087db7d00547f0ba0c1e2273f4920a573cd5a128d" +dependencies = [ + "bitvec", + "cfg-if", + "const-siphasher", + "corosensei", + "hex", + "owo-colors", + "rand 0.8.8", + "rand_core 0.6.4", + "rand_pcg", + "scoped-tls", + "shuttle-engine", + "shuttle-schedulers", + "shuttle-std", + "tracing", +] + +[[package]] +name = "shuttle-engine" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6dad68d0b9d252e56ba6374e05b542b659ee076cf7f319f7c1312d5879d65" +dependencies = [ + "assoc", + "bitvec", + "cfg-if", + "const-siphasher", + "corosensei", + "hex", + "owo-colors", + "rand 0.8.8", + "rand_core 0.6.4", + "rand_pcg", + "scoped-tls", + "smallvec", + "tracing", +] + +[[package]] +name = "shuttle-schedulers" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7673a579e1660404067f5aa1344d6530f15b59f53b0fe98767d7ab117d14067" +dependencies = [ + "rand 0.8.8", + "rand_pcg", + "shuttle-engine", + "smallvec", + "tracing", +] + +[[package]] +name = "shuttle-std" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320b7adce8256548602d4930ef030ac473815a892fd252099702bebcca0d33f8" +dependencies = [ + "assoc", + "owo-colors", + "shuttle-engine", + "smallvec", + "tracing", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3478,7 +3588,7 @@ dependencies = [ "parking_lot", "paste", "primitive-types", - "rand 0.8.5", + "rand 0.8.8", "scale-info", "schnorrkel", "secp256k1", @@ -3525,7 +3635,7 @@ dependencies = [ "parking_lot", "paste", "primitive-types", - "rand 0.8.5", + "rand 0.8.8", "scale-info", "schnorrkel", "secp256k1", @@ -3696,7 +3806,7 @@ dependencies = [ "log", "parity-scale-codec", "parking_lot", - "rand 0.8.5", + "rand 0.8.8", "smallvec", "sp-core 28.0.0", "sp-externalities 0.25.0", @@ -3768,7 +3878,7 @@ dependencies = [ "nohash-hasher", "parity-scale-codec", "parking_lot", - "rand 0.8.5", + "rand 0.8.8", "scale-info", "schnellru", "sp-core 28.0.0", @@ -3793,7 +3903,7 @@ dependencies = [ "nohash-hasher", "parity-scale-codec", "parking_lot", - "rand 0.8.5", + "rand 0.8.8", "scale-info", "schnellru", "sp-core 31.0.0", @@ -4216,7 +4326,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", "digest 0.10.7", - "rand 0.8.5", + "rand 0.8.8", "static_assertions", ] @@ -4327,7 +4437,7 @@ dependencies = [ "arrayref", "constcat", "digest 0.10.7", - "rand 0.8.5", + "rand 0.8.8", "rand_chacha 0.3.1", "rand_core 0.6.4", "sha2 0.10.8", @@ -4579,7 +4689,7 @@ dependencies = [ "memfd", "memoffset", "paste", - "rand 0.8.5", + "rand 0.8.8", "rustix 0.36.17", "wasmtime-asm-macros", "wasmtime-environ", @@ -4628,7 +4738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ "windows-core 0.54.0", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -4637,7 +4747,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -4647,7 +4757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ "windows-result", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -4656,7 +4766,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -4683,7 +4793,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -4718,18 +4837,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -4746,9 +4865,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -4764,9 +4883,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -4782,15 +4901,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -4806,9 +4925,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -4824,9 +4943,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -4842,9 +4961,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -4860,9 +4979,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" diff --git a/nomt/src/io/fsyncer.rs b/nomt/src/io/fsyncer.rs index c66087ee48..1023a471ad 100644 --- a/nomt/src/io/fsyncer.rs +++ b/nomt/src/io/fsyncer.rs @@ -43,10 +43,10 @@ impl Fsyncer { cv: Condvar::new(), s: Mutex::new(State::Idle), }); - #[cfg(not(shuttle))] - use std::thread::Builder; #[cfg(shuttle)] use shuttle::thread::Builder; + #[cfg(not(shuttle))] + use std::thread::Builder; let _thread = Builder::new() .name(name) .spawn({ diff --git a/nomt/tests/concurrency_scenarios.rs b/nomt/tests/concurrency_scenarios.rs index 59b6095073..32e0497384 100644 --- a/nomt/tests/concurrency_scenarios.rs +++ b/nomt/tests/concurrency_scenarios.rs @@ -10,8 +10,8 @@ mod common; use bitvec::prelude::*; use common::{account_path, Test}; use nomt::{ - hasher::Blake3Hasher, proof, trie::KeyPath, trie::LeafData, KeyReadWrite, Nomt, Options, - Root, SessionParams, + hasher::Blake3Hasher, proof, trie::KeyPath, trie::LeafData, KeyReadWrite, Nomt, Options, Root, + SessionParams, }; #[cfg(shuttle)] use shuttle::thread;