use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use super::world::{Grown, IdentityFacts, MemberFacts, ResumeFacts, SurveyFacts};
use super::*;
#[derive(Default)]
pub(crate) struct Script {
pub(crate) in_flight: VecDeque<u64>,
pub(crate) starting: VecDeque<Option<String>>,
pub(crate) surveys: VecDeque<SurveyFacts>,
pub(crate) unmount_fails: Option<String>,
pub(crate) stop_fails: Option<String>,
pub(crate) suspend_fails: Option<String>,
pub(crate) grown: VecDeque<Result<Grown, String>>,
pub(crate) resume_drift: Vec<String>,
pub(crate) reopen_fails: Option<String>,
pub(crate) note_fails: Option<Step>,
}
pub(crate) struct Fake {
pub(crate) script: Mutex<Script>,
calls: Mutex<Vec<String>>,
journal: Mutex<Vec<(u64, u64, Step)>>,
nonces: Mutex<Vec<String>>,
paused: AtomicBool,
serving: AtomicBool,
suspended: AtomicBool,
stores_closed: AtomicBool,
surveys_taken: AtomicU64,
hotplug: AtomicBool,
flush: Mutex<Option<crate::grow::world::FlushVerdict>>,
hold_unmount: AtomicBool,
in_unmount: AtomicBool,
hold_grow: AtomicBool,
dir: tempfile::TempDir,
}
impl Fake {
pub(crate) fn new(script: Script) -> Arc<Fake> {
let fake = Fake::without_a_journal(script);
std::fs::write(fake.journal_path(), "").expect("an empty journal, as korp-start leaves one");
fake
}
fn without_a_journal(script: Script) -> Arc<Fake> {
Arc::new(Fake {
script: Mutex::new(script),
calls: Mutex::new(Vec::new()),
journal: Mutex::new(Vec::new()),
nonces: Mutex::new(Vec::new()),
paused: AtomicBool::new(false),
serving: AtomicBool::new(true),
suspended: AtomicBool::new(false),
stores_closed: AtomicBool::new(false),
surveys_taken: AtomicU64::new(0),
hotplug: AtomicBool::new(true),
flush: Mutex::new(None),
hold_unmount: AtomicBool::new(false),
in_unmount: AtomicBool::new(false),
hold_grow: AtomicBool::new(false),
dir: tempfile::tempdir().expect("tempdir"),
})
}
fn flush_answers(&self, v: crate::grow::world::FlushVerdict) {
*self.flush.lock().unwrap() = Some(v);
}
fn call(&self, what: &str) {
self.calls.lock().unwrap().push(what.to_owned());
}
pub(crate) fn calls(&self) -> Vec<String> {
self.calls.lock().unwrap().clone()
}
fn steps(&self) -> Vec<Step> {
self.journal.lock().unwrap().iter().map(|r| r.2).collect()
}
fn write_journal_file(&self) {
let text: String = self
.journal
.lock()
.unwrap()
.iter()
.map(|(e, t, s)| format!("epoch={e} target_sectors={t} member=- step={} at=1\n", s.as_str()))
.collect();
std::fs::write(self.journal_path(), text).unwrap();
}
}
impl World for Fake {
fn engine_present(&self) -> bool {
true
}
fn hotplug(&self) -> bool {
self.hotplug.load(Ordering::SeqCst)
}
fn journal_path(&self) -> PathBuf {
self.dir.path().join("grow-journal")
}
fn serving(&self) -> bool {
self.serving.load(Ordering::SeqCst)
}
fn mounted_bytes(&self) -> u64 {
if self.serving() { 4096 } else { 0 }
}
fn flush_replication(&self, _ceiling: std::time::Duration) -> crate::grow::world::FlushVerdict {
self.call("flush");
self.flush
.lock()
.unwrap()
.clone()
.unwrap_or_else(|| crate::grow::world::FlushVerdict::NoStandby("the fake was given no standby".to_owned()))
}
fn pause_admission(&self) {
self.call("pause");
self.paused.store(true, Ordering::SeqCst);
}
fn unpause_admission(&self) {
self.call("unpause");
self.paused.store(false, Ordering::SeqCst);
}
fn in_flight(&self) -> u64 {
let mut s = self.script.lock().unwrap();
if s.in_flight.len() > 1 { s.in_flight.pop_front().unwrap() } else { s.in_flight.front().copied().unwrap_or(0) }
}
fn starting(&self) -> Option<String> {
let mut s = self.script.lock().unwrap();
if s.starting.len() > 1 { s.starting.pop_front().unwrap() } else { s.starting.front().cloned().unwrap_or(None) }
}
fn suspend_identity(&self, _why: &str) -> Result<IdentityFacts, String> {
self.call("suspend_identity");
if let Some(e) = self.script.lock().unwrap().suspend_fails.clone() {
return Err(e);
}
self.suspended.store(true, Ordering::SeqCst);
Ok(IdentityFacts { documents: 3, bytes: 999 })
}
fn resume_identity(&self) -> Result<ResumeFacts, String> {
self.call("resume_identity");
self.suspended.store(false, Ordering::SeqCst);
Ok(ResumeFacts { documents_matched: 3, drift: self.script.lock().unwrap().resume_drift.clone(), off_stripe_ms: 5 })
}
fn close_stores(&self) -> Result<(), String> {
self.call("close_stores");
self.stores_closed.store(true, Ordering::SeqCst);
Ok(())
}
fn reopen_stores(&self) -> Result<(), String> {
self.call("reopen_stores");
if let Some(e) = self.script.lock().unwrap().reopen_fails.clone() {
return Err(e);
}
self.stores_closed.store(false, Ordering::SeqCst);
Ok(())
}
fn sync_and_unmount(&self) -> Result<(), String> {
self.call("unmount");
while self.hold_unmount.load(Ordering::SeqCst) {
self.in_unmount.store(true, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(5));
}
self.in_unmount.store(false, Ordering::SeqCst);
if let Some(e) = self.script.lock().unwrap().unmount_fails.clone() {
return Err(e);
}
self.serving.store(false, Ordering::SeqCst);
Ok(())
}
fn stop_array(&self) -> Result<(), String> {
self.call("stop_array");
if let Some(e) = self.script.lock().unwrap().stop_fails.clone() {
return Err(e);
}
Ok(())
}
fn rescan(&self) -> Result<(), String> {
self.call("rescan");
Ok(())
}
fn survey(&self) -> SurveyFacts {
self.surveys_taken.fetch_add(1, Ordering::SeqCst);
let mut s = self.script.lock().unwrap();
if s.surveys.len() > 1 { s.surveys.pop_front().unwrap() } else { s.surveys.front().cloned().unwrap_or_default() }
}
fn grow_and_mount(&self) -> Result<Grown, String> {
self.call("grow_and_mount");
while self.hold_grow.load(Ordering::SeqCst) {
std::thread::sleep(Duration::from_millis(5));
}
let mut s = self.script.lock().unwrap();
let answer = if s.grown.len() > 1 { s.grown.pop_front().unwrap() } else { s.grown.front().cloned().unwrap_or_else(|| Err("no script".into())) };
if answer.is_ok() {
self.serving.store(true, Ordering::SeqCst);
}
answer
}
fn epoch_for(&self, _target_sectors: u64) -> u64 {
7
}
fn note(&self, epoch: u64, target_sectors: u64, step: Step) -> Result<(), String> {
if self.script.lock().unwrap().note_fails == Some(step) {
return Err(format!("the grow journal refused {}: ENOSPC", step.as_str()));
}
self.journal.lock().unwrap().push((epoch, target_sectors, step));
self.write_journal_file();
Ok(())
}
fn remember_nonce(&self, nonce: &str) -> Result<bool, String> {
let mut n = self.nonces.lock().unwrap();
if n.iter().any(|x| x == nonce) {
return Ok(false);
}
n.push(nonce.to_owned());
Ok(true)
}
}
pub(crate) fn complete(larger: bool) -> SurveyFacts {
let member = |i: u32| MemberFacts {
index: i,
device: format!("vd{}", (b'b' + i as u8) as char),
partition: format!("vd{}1", (b'b' + i as u8) as char),
disk_bytes: if larger { 200 << 20 } else { 100 << 20 },
set_bytes: 100 << 20,
offered_bytes: if larger { 200 << 20 } else { 100 << 20 },
larger,
};
SurveyFacts {
verdict: "complete".into(),
raid_disks: 4,
members: (0..4).map(member).collect(),
eligible: larger,
target_sectors: larger.then_some(400_000),
set_uuid: "5d".repeat(16),
description: if larger { "complete (4 of 4): all larger".into() } else { "complete (4 of 4): as before".into() },
}
}
fn go(target: u64, nonce: &str) -> Vec<u8> {
GoAhead::operator(target, nonce, gunnar_grpc::bus::now_unix_ms())
}
fn request(target: u64, nonce: &str) -> gunnar_proto::GrowStartRequest {
gunnar_proto::GrowStartRequest {
target_sectors_per_member: target,
go_ahead: go(target, nonce),
drain_timeout_secs: 2,
wait_members_timeout_secs: 1,
}
}
fn wait_for(m: &GrowMachine, phase: Phase) -> gunnar_proto::GrowStatusResponse {
let deadline = std::time::Instant::now() + Duration::from_secs(20);
loop {
if m.phase() == phase {
return m.status();
}
assert!(std::time::Instant::now() < deadline, "the machine never reached {phase:?}; it is {:?} ({})", m.phase(), m.status().detail);
std::thread::sleep(Duration::from_millis(20));
}
}
fn wait_settled(m: &GrowMachine) -> gunnar_proto::GrowStatusResponse {
let deadline = std::time::Instant::now() + Duration::from_secs(20);
loop {
if !m.phase().is_active() {
return m.status();
}
assert!(std::time::Instant::now() < deadline, "the machine never settled; it is {:?} ({})", m.phase(), m.status().detail);
std::thread::sleep(Duration::from_millis(20));
}
}
#[test]
fn a_growth_runs_drain_unmount_wait_grow_mount_reopen_in_that_order() {
let script = Script {
in_flight: VecDeque::from([2, 1, 0]),
surveys: VecDeque::from([complete(false), complete(false), complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
let announced = Arc::new(Mutex::new(Vec::new()));
let a = Arc::clone(&announced);
m.with_announcer(move |x| a.lock().unwrap().push(x));
let idle = m.status();
assert_eq!((idle.phase.as_str(), idle.engine_present, idle.serving), ("idle", true, true));
assert_eq!(idle.members.len(), 4);
let first = m.start(&request(0, "n1")).expect("started");
assert_eq!(first.phase, "draining");
assert_eq!(first.go_ahead_signer, "operator");
let s = wait_for(&m, Phase::WaitingMembers);
assert!(!s.serving && s.identity_in_ram, "{s:?}");
assert_eq!(s.epoch, 7);
assert!(s.journal.as_ref().is_some_and(|j| j.phase == "in_progress"), "{:?}", s.journal);
assert!(world.paused.load(Ordering::SeqCst), "the gate is paused through the window");
let done = wait_settled(&m);
assert_eq!((done.phase.as_str(), done.why.as_str()), ("done", ""));
assert!(done.serving && !done.identity_in_ram);
assert!(!world.paused.load(Ordering::SeqCst), "the gate is open again");
assert!(!world.suspended.load(Ordering::SeqCst) && !world.stores_closed.load(Ordering::SeqCst));
assert_eq!(
world.calls(),
vec![
"pause", "flush", "suspend_identity", "close_stores", "unmount", "stop_array", "rescan", "rescan", "grow_and_mount", "resume_identity", "reopen_stores", "unpause",
]
);
assert_eq!(world.steps(), vec![Step::Drain, Step::Unmount, Step::Stop, Step::Wait, Step::Rescan, Step::Reopen]);
assert!(done.journal.as_ref().is_some_and(|j| j.phase == "in_progress"), "the fake writes no G6; the engine would");
let announced = announced.lock().unwrap();
assert!(matches!(announced.first(), Some(Announcement::Draining { kind: "growing", .. })));
assert!(matches!(announced.last(), Some(Announcement::Ready)));
assert!(m.start(&request(0, "n1")).unwrap_err().contains("already spent"));
}
#[test]
fn a_drain_that_times_out_aborts_with_nothing_changed() {
let script = Script {
in_flight: VecDeque::from([3]),
surveys: VecDeque::from([complete(false)]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
let mut req = request(0, "n1");
req.drain_timeout_secs = 1;
m.start(&req).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(s.why.contains("aborted") && s.why.contains("3 transfer(s)") && s.why.contains("nothing was changed"), "{}", s.why);
assert!(s.serving && !s.identity_in_ram);
assert_eq!(world.calls(), vec!["pause", "unpause"]);
assert!(world.steps().is_empty(), "nothing on the journal: no epoch was opened");
script_reset(&world, VecDeque::from([0]));
assert_eq!(m.start(&request(0, "n2")).unwrap().phase, "draining");
}
#[test]
fn the_drain_waits_for_the_start_up_disk_test_and_names_it_when_it_never_ends() {
let starting = "starting: the start-up disk test is still running".to_owned();
let script = Script {
in_flight: VecDeque::from([0]),
starting: VecDeque::from([Some(starting.clone()), Some(starting.clone()), Some(starting.clone()), None]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "done", "{}", s.why);
assert!(world.steps().contains(&Step::Drain), "the drain closed only after the test finished: {:?}", world.steps());
let script = Script {
in_flight: VecDeque::from([0]),
starting: VecDeque::from([Some(starting.clone())]),
surveys: VecDeque::from([complete(false)]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
let mut req = request(0, "n2");
req.drain_timeout_secs = 1;
m.start(&req).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(s.why.contains("aborted") && s.why.contains("still running") && s.why.contains("nothing was changed"), "{}", s.why);
assert!(s.serving && !s.identity_in_ram);
assert!(world.steps().is_empty(), "nothing on the journal");
}
#[test]
fn a_member_a_hair_under_the_asked_volume_size_still_reaches_the_target() {
let asked: u64 = 64 << 21; assert!(super::target_reached(asked, asked));
assert!(super::target_reached(asked + 1, asked));
assert!(super::target_reached(asked - 4096, asked), "a partition start under the volume size");
assert!(super::target_reached(asked - asked / 100, asked), "1 % under");
assert!(!super::target_reached(asked / 2, asked), "half the ask is not the ask");
assert!(!super::target_reached(asked - asked / 50, asked), "2 % under is a smaller volume");
let mut back = complete(true);
back.target_sectors = Some(asked - 4096);
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), back]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(asked, "n-hair")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "done", "{}", s.why);
}
fn script_reset(world: &Fake, in_flight: VecDeque<u64>) {
world.script.lock().unwrap().in_flight = in_flight;
}
#[test]
fn a_refused_unmount_puts_everything_back_and_aborts() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false)]),
unmount_fails: Some("umount /data: EBUSY — 2 descriptor(s) of this process still hold it: [fd 9 → /data/x]".into()),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(s.why.contains("aborted") && s.why.contains("EBUSY"), "{}", s.why);
assert!(s.serving && !s.identity_in_ram);
assert_eq!(world.calls(), vec!["pause", "flush", "suspend_identity", "close_stores", "unmount", "reopen_stores", "resume_identity", "unpause"]);
assert_eq!(world.steps(), vec![Step::Drain, Step::Abort]);
assert!(s.journal.as_ref().is_some_and(|j| j.phase == "aborted"), "{:?}", s.journal);
}
#[test]
fn waiting_for_members_never_fails_on_its_timeout_and_resume_looks_again() {
let mut gone = SurveyFacts { verdict: "incomplete: member 1 of 4 is missing".into(), raid_disks: 4, ..Default::default() };
gone.description = gone.verdict.clone();
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), gone]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
let mut req = request(0, "n1");
req.wait_members_timeout_secs = 1;
m.start(&req).expect("started");
let s = wait_for(&m, Phase::WaitingMembers);
assert!(s.members_verdict.starts_with("incomplete"), "{s:?}");
std::thread::sleep(Duration::from_millis(1500));
let s = m.status();
assert_eq!(s.phase, "waiting-members", "a timeout is not a failure");
assert!(s.detail.contains("timeout passed"), "{}", s.detail);
assert!(!s.serving && s.identity_in_ram);
let r = m.resume(false).expect("resume applies while waiting");
assert_eq!(r.phase, "waiting-members");
world.script.lock().unwrap().surveys = VecDeque::from([complete(true)]);
m.resume(false).expect("resume");
let done = wait_settled(&m);
assert_eq!(done.phase, "done", "{}", done.detail);
assert!(world.steps().contains(&Step::Rescan) && world.steps().contains(&Step::Reopen));
let late = m.resume(false).expect("a resume after done answers the status");
assert_eq!(late.phase, "done", "{}", late.detail);
}
#[test]
fn giving_up_while_waiting_serves_what_is_there() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false)]),
grown: VecDeque::from([Ok(Grown { grew: false, array_bytes: 400 << 20, refusal: Some("nothing to do".into()), xfs: String::new() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
wait_for(&m, Phase::WaitingMembers);
m.resume(true).expect("give up");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(s.serving && !s.identity_in_ram);
assert!(s.why.contains("gave up"), "{}", s.why);
assert!(!world.paused.load(Ordering::SeqCst));
assert_eq!(world.steps().last(), Some(&Step::Reopen));
assert!(world.steps().contains(&Step::Abort) && !world.steps().contains(&Step::Fail), "{:?}", world.steps());
world.write_journal_file();
let j = m.status().journal.expect("the journal");
assert_eq!(j.phase, "aborted", "{}", j.detail);
}
#[test]
fn giving_up_after_a_member_came_back_larger_keeps_the_epoch_failed() {
let mut partial = complete(false);
partial.members[0].larger = true;
partial.members[0].disk_bytes = 200 << 20;
partial.description = "complete (4 of 4): 1 larger".into();
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([partial]),
grown: VecDeque::from([Ok(Grown { grew: false, array_bytes: 400 << 20, refusal: Some("the members are unequal".into()), xfs: String::new() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
wait_for(&m, Phase::WaitingMembers);
m.resume(true).expect("give up");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(world.steps().contains(&Step::Fail) && !world.steps().contains(&Step::Abort), "{:?}", world.steps());
}
#[test]
fn an_engine_refusal_serves_at_the_old_size_and_keeps_the_epoch_open() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: false, array_bytes: 400 << 20, refusal: None, xfs: "FAILED: XFS_IOC_FSGROWFSDATA: EIO".into() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(s.serving, "serving at the old size");
assert!(s.why.contains("XFS_IOC_FSGROWFSDATA"), "{}", s.why);
assert!(!world.paused.load(Ordering::SeqCst));
assert!(s.journal.as_ref().is_some_and(|j| j.phase == "failed"), "{:?}", s.journal);
assert_eq!(world.steps(), vec![Step::Drain, Step::Unmount, Step::Stop, Step::Wait, Step::Rescan, Step::Fail, Step::Reopen], "FAIL, then /data is back");
world.script.lock().unwrap().surveys = VecDeque::from([complete(true)]);
world.script.lock().unwrap().grown = VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]);
m.start(&request(0, "n2")).expect("a failed-but-serving box may try again");
assert_eq!(wait_settled(&m).phase, "done");
}
#[test]
fn a_set_that_cannot_be_mounted_is_fatal_but_the_status_still_answers() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Err("korp data set: 3 of 4 members present — member 2 of 4 is missing".into())]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(!s.serving && s.identity_in_ram, "{s:?}");
assert!(s.why.contains("FATAL") && s.why.contains("member 2 of 4"), "{}", s.why);
assert!(world.paused.load(Ordering::SeqCst), "nothing to serve: the gate stays paused");
assert_eq!(s.total_bytes, 0);
assert!(m.start(&request(0, "n2")).unwrap_err().contains("not being served"));
assert!(world.steps().contains(&Step::Fail));
}
#[test]
fn an_array_that_will_not_stop_is_mounted_back_and_served() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false)]),
stop_fails: Some("md: /sys/block/md_korp_data still exists".into()),
grown: VecDeque::from([Ok(Grown { grew: false, array_bytes: 400 << 20, refusal: None, xfs: String::new() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(s.serving && s.why.contains("would not stop"), "{}", s.why);
assert_eq!(world.calls(), vec!["pause", "flush", "suspend_identity", "close_stores", "unmount", "stop_array", "grow_and_mount", "resume_identity", "reopen_stores", "unpause"]);
}
#[test]
fn the_go_ahead_and_the_box_are_checked_before_anything_moves() {
let script = Script {
in_flight: VecDeque::from([5]),
surveys: VecDeque::from([complete(false)]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
const T: u64 = 1_000_000;
let mut r = request(T, "n1");
r.go_ahead = Vec::new();
assert!(m.start(&r).unwrap_err().contains("empty"));
r.go_ahead = go(T - 1, "n1");
assert!(m.start(&r).unwrap_err().contains("must agree"));
r.go_ahead = GoAhead::operator(T, "n-old", 1);
assert!(m.start(&r).unwrap_err().contains("past the"));
assert!(world.calls().is_empty(), "nothing moved");
r.go_ahead = go(T, "n1");
r.drain_timeout_secs = 3;
m.start(&r).expect("started");
assert!(m.start(&request(T, "n2")).unwrap_err().contains("already draining"));
script_reset(&world, VecDeque::from([0]));
world.script.lock().unwrap().unmount_fails = Some("x".into());
wait_settled(&m);
world.script.lock().unwrap().surveys = VecDeque::from([SurveyFacts { verdict: "incomplete: member 3 of 4 is missing".into(), ..Default::default() }]);
assert!(m.start(&request(T, "n3")).unwrap_err().contains("not a complete md set"));
}
#[test]
fn without_the_engine_status_answers_idle_and_start_is_refused() {
let m = GrowMachine::new(Arc::new(world::NoEngine));
let s = m.status();
assert_eq!((s.phase.as_str(), s.engine_present, s.serving), ("idle", false, true));
assert!(s.detail.contains("no runtime grow engine"), "{}", s.detail);
assert!(m.start(&request(0, "n1")).unwrap_err().contains("grow-runtime"));
assert!(m.resume(false).unwrap_err().contains("applies while waiting-members"));
}
#[test]
fn identity_drift_on_resume_is_named_in_the_status() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
resume_drift: vec!["refs/gunnar/auth: ram aa vs disk bb".into()],
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "done");
assert!(s.why.contains("identity drift") && s.why.contains("refs/gunnar/auth"), "{}", s.why);
}
#[test]
fn the_nonce_ledger_spends_each_go_ahead_once() {
let dir = tempfile::tempdir().unwrap();
let ledger = dir.path().join(world::NONCE_LEDGER_FILE);
assert!(world::remember_nonce_in(&ledger, "a").unwrap());
assert!(!world::remember_nonce_in(&ledger, "a").unwrap());
assert!(world::remember_nonce_in(&ledger, "b").unwrap());
assert_eq!(std::fs::read_to_string(&ledger).unwrap(), "a\nb\n");
}
#[test]
fn a_kernel_without_pci_hotplug_refuses_a_runtime_growth_by_name() {
let world = Fake::new(Script::default());
world.hotplug.store(false, Ordering::SeqCst);
let m = GrowMachine::new(world.clone());
assert!(!m.status().hotplug);
let err = m.start(&request(1000, "hp1")).unwrap_err();
assert!(err.contains("no PCI hot-plug") && err.contains("refused"), "{err}");
assert_eq!(m.status().phase, "idle", "nothing moved");
assert!(world.calls().is_empty(), "nothing drained: {:?}", world.calls());
world.hotplug.store(true, Ordering::SeqCst);
assert!(m.status().hotplug);
}
#[test]
fn a_box_with_no_grow_journal_refuses_growstart_by_name_and_one_with_a_journal_starts() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false)]),
..Script::default()
};
let world = Fake::without_a_journal(script);
assert!(!world.journal_path().exists(), "the fixture is a box with no journal file");
let m = GrowMachine::new(world.clone());
let err = m.start(&request(0, "nj1")).unwrap_err();
assert!(
err.contains("keeps no grow journal") && err.contains("GROWTH_SEALED"),
"the refusal names the journal and the capacity verdict it agrees with: {err}"
);
assert_eq!(m.status().phase, "idle", "nothing moved");
assert!(world.calls().is_empty(), "the gate was never paused: {:?}", world.calls());
assert!(world.nonces.lock().unwrap().is_empty(), "no nonce was spent on a refused start");
std::fs::write(world.journal_path(), "").unwrap();
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
*world.script.lock().unwrap() = script;
assert_eq!(m.start(&request(0, "nj2")).unwrap().phase, "draining");
assert_eq!(wait_settled(&m).phase, "done");
}
#[cfg(feature = "metered")]
#[test]
fn a_monetize_signed_go_ahead_starts_a_growth_and_a_replay_of_it_is_refused_by_name() {
use ed25519_dalek::SigningKey;
let monetize = SigningKey::from_bytes(&[3u8; 32]);
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::with_policy(
world.clone(),
Arc::new(go_ahead::MonetizePolicy::new(&monetize.verifying_key().to_bytes()).expect("a real key")),
);
let bytes = go_ahead::metered_tests::signed(&monetize, 0, "mz-1", gunnar_grpc::bus::now_unix_ms());
let req = gunnar_proto::GrowStartRequest {
target_sectors_per_member: 0,
go_ahead: bytes.clone(),
drain_timeout_secs: 2,
wait_members_timeout_secs: 1,
};
let first = m.start(&req).expect("monetize's go-ahead starts the growth");
assert_eq!((first.phase.as_str(), first.go_ahead_signer.as_str()), ("draining", "monetize"));
assert_eq!(wait_settled(&m).phase, "done");
let err = m.start(&req).unwrap_err();
assert!(err.contains("already spent") && err.contains("mz-1"), "the replay is refused by name: {err}");
assert_eq!(m.status().phase, "done", "the replay moved nothing");
assert_eq!(m.start(&request(0, "op-after")).unwrap().phase, "draining");
assert_eq!(wait_settled(&m).phase, "done");
}
#[test]
fn a_standby_still_behind_when_the_gate_shuts_aborts_the_growth_untouched() {
let script = Script {
surveys: VecDeque::from([complete(false), complete(true)]),
..Script::default()
};
let world = Fake::new(script);
world.flush_answers(crate::grow::world::FlushVerdict::Behind {
pending: 19_264,
needs_full_resync: false,
why: "the twin read `applied 0 of 19264` and went quiet".to_owned(),
});
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let done = wait_settled(&m);
assert_eq!(done.phase, "failed", "{done:?}");
assert!(done.why.contains("a standby is still behind"), "{}", done.why);
assert!(done.why.contains("19264"), "the refusal must carry the number: {}", done.why);
assert!(done.serving, "the box must still be serving: {done:?}");
assert!(!world.paused.load(Ordering::SeqCst), "the gate was left shut");
assert_eq!(world.calls(), vec!["pause", "flush", "unpause"], "something happened after the flush refused");
assert!(world.steps().is_empty(), "a journal record was written for a growth that never began: {:?}", world.steps());
}
#[test]
fn a_box_with_no_standby_grows_and_the_detail_says_there_was_none() {
let script = Script {
surveys: VecDeque::from([complete(false), complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
world.flush_answers(crate::grow::world::FlushVerdict::NoStandby("no replicator is running on this box".to_owned()));
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let done = wait_settled(&m);
assert_eq!((done.phase.as_str(), done.why.as_str()), ("done", ""), "{done:?}");
assert!(world.calls().contains(&"grow_and_mount".to_owned()), "{:?}", world.calls());
}
#[test]
fn a_drain_that_could_not_look_aborts_the_growth_untouched() {
let script = Script {
surveys: VecDeque::from([complete(false), complete(true)]),
..Script::default()
};
let world = Fake::new(script);
world.flush_answers(crate::grow::world::FlushVerdict::CouldNotLook(
"twin 'gunnar-twin' has reported no position since this server started".to_owned(),
));
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let done = wait_settled(&m);
assert_eq!(done.phase, "failed", "a growth that proved nothing must not reach done: {done:?}");
assert!(done.why.contains("COULD NOT BE ESTABLISHED"), "{}", done.why);
assert!(done.why.contains("gunnar-twin"), "the refusal must name the twin: {}", done.why);
assert!(done.serving, "the box must still be serving: {done:?}");
assert!(!world.paused.load(Ordering::SeqCst), "the gate was left shut");
assert_eq!(world.calls(), vec!["pause", "flush", "unpause"], "something happened after the drain could not look");
assert!(world.steps().is_empty(), "a journal record was written for a growth that never began: {:?}", world.steps());
}
#[test]
fn a_twin_at_zero_of_zero_is_behind_and_never_caught_up() {
let script = Script {
surveys: VecDeque::from([complete(false), complete(true)]),
..Script::default()
};
let world = Fake::new(script);
world.flush_answers(crate::grow::world::FlushVerdict::Behind {
pending: 318,
needs_full_resync: false,
why: "twin 'gunnar-twin' has applied 0 of this box's head 318".to_owned(),
});
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let done = wait_settled(&m);
assert_eq!(done.phase, "failed", "{done:?}");
assert!(done.why.contains("applied 0 of this box's head 318"), "{}", done.why);
assert!(world.steps().is_empty(), "nothing may be journalled: {:?}", world.steps());
}
#[test]
fn a_twin_behind_is_waited_for_within_the_ceiling_and_silence_is_refused_at_once() {
use crate::grow::world::{await_twins, FlushVerdict};
use std::time::Duration;
let behind = || FlushVerdict::Behind { pending: 1, needs_full_resync: false, why: "applied 718 of 719".into() };
let mut n = 0;
let got = await_twins(Duration::from_secs(5), Duration::from_millis(1), || {
n += 1;
if n < 3 { Err(behind()) } else { Ok((1, 719)) }
});
assert!(matches!(got, Ok((1, 719))) && n == 3, "{got:?} after {n}");
let got = await_twins(Duration::from_millis(20), Duration::from_millis(5), || Err(behind()));
assert!(matches!(got, Err(FlushVerdict::Behind { pending: 1, .. })), "{got:?}");
let mut looks = 0;
let got = await_twins(Duration::from_secs(5), Duration::from_millis(1), || {
looks += 1;
Err(FlushVerdict::CouldNotLook("silent".into()))
});
assert!(matches!(got, Err(FlushVerdict::CouldNotLook(_))) && looks == 1, "{got:?} after {looks}");
}
#[test]
fn unmounted_is_not_said_until_the_set_is_off_and_a_second_growth_says_it_the_same_way() {
let script = Script {
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
for (round, nonce) in [(1, "twice-1"), (2, "twice-2")] {
world.script.lock().unwrap().surveys = VecDeque::from([complete(false), complete(false), complete(true)]);
world.hold_unmount.store(true, Ordering::SeqCst);
let calls_before = world.calls().len();
let first = m.start(&request(0, nonce)).unwrap_or_else(|e| panic!("growth {round} refused: {e}"));
assert_eq!(first.phase, "draining", "growth {round}");
let deadline = std::time::Instant::now() + Duration::from_secs(20);
while !world.in_unmount.load(Ordering::SeqCst) {
assert!(std::time::Instant::now() < deadline, "growth {round} never reached the unmount; {:?}", m.phase());
std::thread::sleep(Duration::from_millis(5));
}
assert!(world.serving(), "the fake is still mounted inside the unmount");
let s = m.status();
assert_eq!(
s.phase, "draining",
"growth {round}: /data is still mounted and the phase already tells the outside actor the volumes are free: {s:?}"
);
assert!(!matches!(m.phase(), Phase::Unmounted | Phase::WaitingMembers), "growth {round}");
world.hold_unmount.store(false, Ordering::SeqCst);
loop {
let p = m.phase();
if matches!(p, Phase::Unmounted | Phase::WaitingMembers | Phase::Growing | Phase::Mounting | Phase::Done) {
let calls = world.calls()[calls_before..].to_vec();
assert!(calls.iter().any(|c| c == "stop_array"), "growth {round}: {p:?} said before the array stopped: {calls:?}");
break;
}
assert!(std::time::Instant::now() < deadline, "growth {round} never left draining: {p:?}");
std::thread::sleep(Duration::from_millis(1));
}
let done = wait_settled(&m);
assert_eq!(done.phase, "done", "growth {round}: {done:?}");
assert!(done.serving, "growth {round}");
}
assert_eq!(m.resume(false).expect("done answers").phase, "done");
}
pub(crate) fn carrying(sectors: u64) -> SurveyFacts {
let mut s = complete(false);
for m in &mut s.members {
m.disk_bytes = sectors * 512;
m.set_bytes = sectors * 512;
m.offered_bytes = sectors * 512;
}
s.description = format!("complete (4 of 4): nothing to grow at {sectors} sectors per member");
s
}
#[test]
fn a_growstart_whose_target_the_members_already_carry_finishes_at_once_and_never_unmounts() {
const SIXTY_GIB: u64 = 125_829_120;
for (label, carried) in [("exactly", SIXTY_GIB), ("a partition start under", SIXTY_GIB - 67)] {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([carrying(carried)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
let mut req = request(SIXTY_GIB, "twin-grow/gunnar/1790685635332");
req.wait_members_timeout_secs = 3600;
let first = m.start(&req).unwrap_or_else(|e| panic!("{label}: a growth already carried is not a refusal: {e}"));
assert_eq!(first.phase, "done", "{label}: finished at once: {first:?}");
assert!(first.serving, "{label}: /data never left");
assert!(first.detail.contains("already"), "{label}: the detail says why nothing moved: {}", first.detail);
std::thread::sleep(Duration::from_millis(300));
assert_eq!(m.phase(), Phase::Done, "{label}");
assert!(world.calls().is_empty(), "{label}: nothing drained, nothing unmounted: {:?}", world.calls());
assert!(world.steps().is_empty(), "{label}: no epoch opened: {:?}", world.steps());
assert!(world.serving(), "{label}");
assert_eq!(m.resume(false).expect("done answers").phase, "done", "{label}");
}
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([carrying(SIXTY_GIB / 3)]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
assert_eq!(m.start(&request(SIXTY_GIB, "n-grow")).expect("started").phase, "draining");
world.script.lock().unwrap().unmount_fails = Some("stop here".into());
wait_settled(&m);
}
#[test]
fn growresume_while_growing_or_mounting_answers_the_status_not_a_refusal() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
world.hold_grow.store(true, Ordering::SeqCst);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
wait_for(&m, Phase::Growing);
let r = m.resume(false).expect("a resume while growing is progress");
assert_eq!(r.phase, "growing", "{r:?}");
let r = m.resume(true).expect("give_up while growing changes nothing: the engine is already running");
assert_eq!(r.phase, "growing", "{r:?}");
world.hold_grow.store(false, Ordering::SeqCst);
let done = wait_settled(&m);
assert_eq!(done.phase, "done", "a resume during the engine's run neither stops nor repeats it: {done:?}");
assert_eq!(world.calls().iter().filter(|c| *c == "grow_and_mount").count(), 1, "{:?}", world.calls());
let idle = GrowMachine::new(Fake::new(Script::default()));
assert!(idle.resume(false).unwrap_err().contains("start one with GrowStart"));
}
#[test]
fn a_set_left_unmounted_past_its_bound_is_mounted_back_named_and_alarmed() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false)]),
grown: VecDeque::from([Ok(Grown { grew: false, array_bytes: 400 << 20, refusal: None, xfs: String::new() })]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
assert!(m.alarm().is_none(), "no alarm before anything happened");
let mut req = request(1_000_000, "n-stuck");
req.wait_members_timeout_secs = 1;
m.start(&req).expect("started");
wait_for(&m, Phase::WaitingMembers);
let s = wait_settled(&m);
assert_eq!(s.phase, "failed", "{s:?}");
assert!(s.serving, "mounted back: {s:?}");
assert!(!s.identity_in_ram, "{s:?}");
assert!(s.why.contains(UNMOUNTED_TOO_LONG), "the failure is named: {}", s.why);
assert!(!world.paused.load(Ordering::SeqCst), "the gate is open again");
assert!(world.serving(), "the world was asked to mount");
assert!(world.steps().contains(&Step::Fail), "{:?}", world.steps());
let alarm = m.alarm().expect("an alarm on Admin.Health");
assert_eq!(alarm.event, UNMOUNTED_TOO_LONG);
assert!(alarm.detail.contains("mounted back"), "{}", alarm.detail);
let mut gone = SurveyFacts { verdict: "incomplete: member 1 of 4 is missing".into(), raid_disks: 4, ..Default::default() };
gone.description = gone.verdict.clone();
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), gone]),
..Script::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
let mut req = request(1_000_000, "n-gone");
req.wait_members_timeout_secs = 1;
m.start(&req).expect("started");
wait_for(&m, Phase::WaitingMembers);
std::thread::sleep(Duration::from_millis(1600));
assert_eq!(m.phase(), Phase::WaitingMembers, "a set with a member missing cannot be mounted back");
let alarm = m.alarm().expect("alarmed while it cannot come back");
assert_eq!(alarm.event, UNMOUNTED_TOO_LONG);
assert!(alarm.detail.contains("missing"), "{}", alarm.detail);
assert!(!world.calls().iter().any(|c| c == "grow_and_mount"), "{:?}", world.calls());
}
#[test]
fn the_growth_machine_says_it_is_up_once_when_it_is_built() {
#[derive(Clone, Default)]
struct Heard(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for Heard {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut said = String::new();
for _ in 0..20 {
let heard = Heard::default();
let h = heard.clone();
let sub = tracing_subscriber::fmt().with_writer(move || h.clone()).with_ansi(false).finish();
tracing::subscriber::with_default(sub, || {
tracing::callsite::rebuild_interest_cache();
let _m = GrowMachine::new(Fake::new(Script::default()));
});
said = String::from_utf8(heard.0.lock().unwrap().clone()).unwrap();
if !said.is_empty() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
let up: Vec<&str> = said.lines().filter(|l| l.contains(TARGET) && l.contains("the growth machine is up")).collect();
assert_eq!(up.len(), 1, "one line, on {TARGET}:\n{said}");
assert!(up[0].contains("idle") && up[0].contains("engine"), "its phase and whether it can grow at runtime: {}", up[0]);
}
#[test]
fn cov13_an_alone_go_ahead_grows_past_a_standby_that_is_behind_and_says_so() {
let script = Script {
surveys: VecDeque::from([complete(false), complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
world.flush_answers(crate::grow::world::FlushVerdict::Behind {
pending: 92_775,
needs_full_resync: false,
why: "twin 'gunnar-twin' has applied 711743 of this box's head 804518".to_owned(),
});
let m = GrowMachine::new(world.clone());
let mut go: serde_json::Value = serde_json::from_slice(&go(0, "n-alone")).unwrap();
go["alone"] = serde_json::Value::String("ALERT gunnar: main grown alone: twin gunnar-twin not caught up within the flush bound".into());
let req = gunnar_proto::GrowStartRequest { go_ahead: serde_json::to_vec(&go).unwrap(), ..request(0, "n-alone") };
m.start(&req).expect("started");
let done = wait_settled(&m);
assert_eq!((done.phase.as_str(), done.why.as_str()), ("done", ""), "an emergency sent alone must grow past a lagging standby: {done:?}");
assert!(world.calls().contains(&"grow_and_mount".to_owned()), "{:?}", world.calls());
assert!(done.detail.contains("ALONE") && done.detail.contains("92775"), "the run must say it went alone and how far behind: {}", done.detail);
}
#[test]
fn an_abort_whose_stores_will_not_reopen_keeps_the_gate_shut() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false)]),
unmount_fails: Some("umount /data: EBUSY".into()),
reopen_fails: Some("the control store would not open: EIO".into()),
..Default::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(world.paused.load(Ordering::SeqCst), "the gate opened over closed stores: {:?}", world.calls());
assert!(!world.calls().contains(&"unpause".to_owned()), "{:?}", world.calls());
assert!(s.why.contains("EIO"), "the refusal is named: {}", s.why);
assert!(!s.serving, "a box whose stores are closed does not say it is serving");
}
#[test]
fn a_growth_whose_stores_will_not_reopen_after_the_mount_keeps_the_gate_shut() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
reopen_fails: Some("the control store would not open: EIO".into()),
..Default::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
let announced = Arc::new(Mutex::new(Vec::new()));
let a = Arc::clone(&announced);
m.with_announcer(move |x| a.lock().unwrap().push(x));
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed", "{}", s.why);
assert!(s.why.contains("EIO"), "{}", s.why);
assert!(world.paused.load(Ordering::SeqCst), "the gate opened over closed stores: {:?}", world.calls());
assert!(!announced.lock().unwrap().iter().any(|x| matches!(x, Announcement::Ready)), "Ready announced over closed stores");
}
#[test]
fn a_journal_that_cannot_record_the_wait_fails_the_growth() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: false, array_bytes: 400 << 20, refusal: None, xfs: "as before".into() })]),
note_fails: Some(Step::Wait),
..Default::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed", "{}", s.why);
assert!(s.why.contains("journal"), "the journal's refusal is named: {}", s.why);
assert!(!world.steps().contains(&Step::Rescan), "the growth went on past a step the journal lost: {:?}", world.steps());
}
#[test]
fn a_journal_that_cannot_record_the_reopen_fails_the_growth() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false), complete(true)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 800 << 20, refusal: None, xfs: "grown".into() })]),
note_fails: Some(Step::Reopen),
..Default::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed", "{}", s.why);
assert!(s.why.contains("journal") && s.why.contains("REOPEN"), "{}", s.why);
}
#[test]
fn a_journal_that_cannot_record_the_abort_says_so() {
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([complete(false)]),
unmount_fails: Some("umount /data: EBUSY".into()),
note_fails: Some(Step::Abort),
..Default::default()
};
let world = Fake::new(script);
let m = GrowMachine::new(world.clone());
m.start(&request(0, "n1")).expect("started");
let s = wait_settled(&m);
assert_eq!(s.phase, "failed");
assert!(s.why.contains("journal") && s.why.contains("ABORT"), "{}", s.why);
}