#![forbid(unsafe_code)]
use crate::optimizer::foreground::ForegroundPolicy;
use crate::optimizer::policy::OptimizeOptions;
use crate::store::io::IoBackendKind;
use crate::store::transaction::CrashHooks;
use crate::store::{AttrUpdate, NewEntry, Store, StoreConfig};
use tempfile::TempDir;
fn cfg(kind: IoBackendKind) -> StoreConfig {
StoreConfig {
io_backend: kind,
..Default::default()
}
}
fn noise(n: usize, seed: u64) -> Vec<u8> {
let mut state = seed;
let mut out = Vec::with_capacity(n);
while out.len() < n {
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
let b = z.to_le_bytes();
let take = (n - out.len()).min(8);
out.extend_from_slice(&b[..take]);
}
out
}
struct StopCheckpoints(std::sync::Arc<std::sync::atomic::AtomicBool>);
impl Drop for StopCheckpoints {
fn drop(&mut self) {
self.0.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
#[test]
fn epoch_sequence_is_globally_monotonic_across_checkpoints() {
for kind in IoBackendKind::ALL {
let dir = TempDir::new().unwrap();
let store = Store::create(dir.path(), &cfg(kind), [0x51; 16]).unwrap();
let root = store.current_root().root_dir_ino;
const WORKERS: usize = 8;
const FILES_PER_WORKER: usize = 6;
const CHUNKS: usize = 2;
let text_len = CHUNKS * 65536;
if std::env::var("EFS_TRACE").is_ok() {
eprintln!("[trace] {kind:?}: starting (workers {WORKERS}, files {FILES_PER_WORKER})");
}
let mut expected: Vec<(u64, Vec<u8>)> = Vec::new();
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let cp_store = &store;
let _no_cp_thread = std::env::var("EFS_NO_CP_THREAD").is_ok();
std::thread::scope(|s| {
let _stop_guard = StopCheckpoints(std::sync::Arc::clone(&stop));
let stop_cp = std::sync::Arc::clone(&stop);
if !_no_cp_thread {
s.spawn(move || {
while !stop_cp.load(std::sync::atomic::Ordering::Relaxed) {
cp_store
.epoch_checkpoint(&CrashHooks::none())
.expect("checkpoint");
std::thread::yield_now();
}
});
}
let handles: Vec<_> = (0..WORKERS)
.map(|w| {
let store = &store;
s.spawn(move || {
let mut results = Vec::new();
for f in 0..FILES_PER_WORKER {
let name = format!("w{w}f{f}");
if std::env::var("EFS_TRACE").is_ok() {
eprintln!("[trace] w{w}: create {name}");
}
let ino = store
.epoch_create(
root,
name.as_bytes(),
NewEntry::file(0o644, 1000, 1000),
&CrashHooks::none(),
)
.expect("epoch_create");
let text = noise(text_len, 0xC0FFEE + (w as u64) * 256 + f as u64);
if std::env::var("EFS_TRACE").is_ok() {
eprintln!("[trace] w{w}: write {ino}");
}
store
.epoch_write(
ino,
0,
&text,
OptimizeOptions::default(),
ForegroundPolicy::full(),
&CrashHooks::none(),
)
.expect("epoch_write");
if std::env::var("EFS_TRACE").is_ok() {
eprintln!("[trace] w{w}: setattr {ino}");
}
store
.epoch_setattr(
ino,
&AttrUpdate {
size: Some(text.len() as u64),
..Default::default()
},
&CrashHooks::none(),
)
.expect("epoch_setattr");
if std::env::var("EFS_TRACE").is_ok() {
eprintln!("[trace] w{w}: readback {ino}");
}
let ep = store.epoch();
let back = store
.read_file_epoch(&ep, ino, 0, text.len() as u64)
.expect("overlay read");
drop(ep);
assert_eq!(back, text, "{kind:?} overlay read mismatch");
if std::env::var("EFS_TRACE").is_ok() {
eprintln!("[trace] w{w}: done {ino}");
}
results.push((ino, text));
}
if std::env::var("EFS_TRACE").is_ok() {
eprintln!("[trace] w{w}: ALL DONE");
}
results
})
})
.collect();
for h in handles {
expected.extend(h.join().unwrap());
}
});
drop(store);
let store = Store::open(dir.path(), &cfg(kind)).unwrap();
for (ino, text) in &expected {
let back = store
.read_file(*ino, 0, text.len() as u64)
.unwrap_or_else(|e| panic!("{kind:?}: remount read of ino {ino} failed: {e:?}"));
assert_eq!(
&back, text,
"{kind:?}: acknowledged write lost across the crash (ino {ino})"
);
}
let report = crate::fsck::fsck(dir.path(), &crate::fsck::FsckOptions::default()).unwrap();
assert!(
report.is_clean(),
"{kind:?} fsck after the crash:\n{}",
report.render()
);
}
}