diff options
| author | tv <tv@krebsco.de> | 2026-11-01 13:38:10 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-01 13:38:10 +0100 |
| commit | 43b0d18fcf62cc2ee5e10767670729c9fa80bb50 (patch) | |
| tree | e33d6cb727129a7a74c9bda01abd06e85706e4e1 | |
| parent | 885da82bb0d169f995598e0ba7c75523b1f98080 (diff) | |
add rejoin writable tests
| -rw-r--r-- | src/simtest/remakes.rs | 89 |
1 files changed, 88 insertions, 1 deletions
diff --git a/src/simtest/remakes.rs b/src/simtest/remakes.rs index e4f115b..ab04efe 100644 --- a/src/simtest/remakes.rs +++ b/src/simtest/remakes.rs @@ -1,6 +1,6 @@ //! Remakes on whole nodes (`docs/remakes.md`): a compaction everywhere, competing //! proposals across a partition, a node that misses the switch, a coordinator that is -//! gone, and a skewed clock. +//! gone, a skewed clock, and writes while a node starts to rejoin. //! //! The simulated nodes' timings make a remake quick: settle 2 s (two `dormant_after`), //! switch delay a keepalive, votes within half a keepalive. @@ -11,7 +11,9 @@ use axum::http::StatusCode; use serde_json::{Value as JsonValue, json}; use super::{SOON, Sim, read, write}; +use crate::document::ChangeHash; use crate::document::replica::Epoch; +use crate::document::store; use crate::remake::testing; use crate::state::AppState; use crate::swarm::bootstrap::Start; @@ -225,3 +227,88 @@ async fn compaction_with_a_skewed_clock() { same_epoch(&[&a, &b, &c]); assert_eq!(read(&b, "data").await, Some(json!(1))); } + +/// Writes on a node right as it starts to rejoin (`Replica::writable`): each is either +/// answered and kept, or refused; none lands in the fresh document before the epoch's +/// snapshot, which every change of the new epoch descends from. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn writes_while_a_rejoin_starts_are_kept_or_refused() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::{Arc, Mutex}; + + let sim = Sim::new(36); + let (a, b, c) = three(&sim).await; + sim.net.take_down("c"); + propose(&a, json!({ "kind": "compact" })).await; + on_generation(&sim, &[&a, &b], 1).await; + + // c writes on and on, through its return and rejoin. + let answered = Arc::new(Mutex::new(Vec::<u64>::new())); + let stop = Arc::new(AtomicBool::new(false)); + let writer = { + let (c, answered, stop) = (c.clone(), Arc::clone(&answered), Arc::clone(&stop)); + tokio::spawn(async move { + let mut n = 0_u64; + while !stop.load(Ordering::Relaxed) { + let at = vec!["w".to_owned(), n.to_string()]; + let written = c + .mutate_doc(move |doc| store::write_path(doc, &at, &json!(n))) + .await; + if written.is_ok() { + answered.lock().unwrap().push(n); + } + n += 1; + tokio::time::sleep(Duration::from_millis(2)).await; + } + }) + }; + tokio::time::sleep(Duration::from_millis(200)).await; + sim.net.bring_up("c"); + on_generation(&sim, &[&a, &b, &c], 1).await; + tokio::time::sleep(Duration::from_millis(200)).await; + stop.store(true, Ordering::Relaxed); + writer.await.unwrap(); + + // Every change of c's document descends from the epoch's snapshot. + let epoch = same_epoch(&[&a, &b, &c]); + { + let replica = c.doc.read().await; + let snapshot = ChangeHash(epoch.id); + let deps: std::collections::HashMap<ChangeHash, Vec<ChangeHash>> = replica + .doc + .get_changes(&[]) + .iter() + .map(|change| (change.hash(), change.deps().to_vec())) + .collect(); + for start in deps.keys() { + let (mut todo, mut seen) = (vec![*start], std::collections::HashSet::new()); + let mut reached = false; + while let Some(hash) = todo.pop() { + if hash == snapshot { + reached = true; + break; + } + if seen.insert(hash) { + todo.extend(deps.get(&hash).into_iter().flatten().copied()); + } + } + assert!(reached, "change {start} doesn't descend from the snapshot"); + } + } + // Every write answered is there (those before the rejoin replayed into it). + let answered = answered.lock().unwrap().clone(); + assert!(!answered.is_empty()); + let c = &c; + sim.wait_until("c has every write it answered", SOON, move || { + let answered = answered.clone(); + async move { + for n in &answered { + if read(c, &format!("w/{n}")).await != Some(json!(n)) { + return false; + } + } + true + } + }) + .await; +} |
