#![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};
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
}
fn text512() -> Vec<u8> {
(b"the quick brown fox jumps over the lazy dog and the entropic ".repeat(16000))[..512 * 1024]
.to_vec()
}
fn fuse_file_sequence(
store: &Store,
ino: u64,
content: &[u8],
tag: &str,
errors: &std::sync::Mutex<Vec<String>>,
) {
let req = 128 * 1024;
for (i, off) in (0..content.len()).step_by(req).enumerate() {
let end = (off + req).min(content.len());
if let Err(e) = store.epoch_write(
ino,
off as u64,
&content[off..end],
OptimizeOptions::default(),
ForegroundPolicy::full(),
&CrashHooks::none(),
) {
errors
.lock()
.unwrap()
.push(format!("{tag} write#{i}: {e:?}"));
return;
}
}
if let Err(e) = store.epoch_setattr(
ino,
&AttrUpdate {
size: Some(content.len() as u64),
..Default::default()
},
&CrashHooks::none(),
) {
errors.lock().unwrap().push(format!("{tag} setattr: {e:?}"));
return;
}
let ep = store.epoch();
match store.read_file_epoch(&ep, ino, 0, content.len() as u64) {
Ok(b) if b != content => {
let sz = store
.get_inode_epoch(&ep, ino)
.ok()
.flatten()
.map(|i| i.size)
.unwrap_or(0);
let csz = store
.get_inode(ino)
.ok()
.flatten()
.map(|i| i.size)
.unwrap_or(0);
let pend_ext: Vec<(u64, usize)> = ep
.pending_extents
.iter()
.filter(|((f, _), _)| *f == ino)
.map(|((_, off), b)| (*off, b.len()))
.collect();
errors.lock().unwrap().push(format!(
"{tag}: READ MISMATCH (len {}, expected {}, overlay size {sz}, committed size {csz}, pending extents {pend_ext:?})",
b.len(),
content.len()
));
}
Ok(_) => {}
Err(e) => errors.lock().unwrap().push(format!("{tag}: read {e:?}")),
}
}
#[test]
fn parallel_offset_split_writes_with_setattr_flushes_stay_byte_exact() {
for kind in IoBackendKind::ALL {
let dir = tempfile::TempDir::new().unwrap();
let store = Store::create(dir.path(), &cfg(kind), [0xAB; 16]).unwrap();
let root = store.current_root().root_dir_ino;
let text = text512();
let rand = noise(512 * 1024, 0xC0FFEE);
let zero = vec![0u8; 512 * 1024];
let groups: Vec<(String, Vec<u8>)> = (0..24)
.map(|i| {
let g = i / 8;
let c = match g {
0 => text.clone(),
1 => rand.clone(),
_ => zero.clone(),
};
(format!("f{i}"), c)
})
.collect();
let errors: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
let mut last_round_inos: Vec<u64> = Vec::new();
for round in 0..8 {
let mut inos: Vec<u64> = Vec::new();
for (name, _) in &groups {
let ino = store
.create_entry(
root,
format!("r{round}-{name}").as_bytes(),
NewEntry::file(0o644, 1000, 1000),
&CrashHooks::none(),
)
.unwrap();
inos.push(ino);
}
last_round_inos = inos.clone();
std::thread::scope(|s| {
let mut hs = Vec::new();
for (i, ((name, content), ino)) in groups.iter().zip(&inos).enumerate() {
let store = &store;
let content = content.clone();
let ino = *ino;
let tag = format!("round {round} {name} ({i})");
let errors = &errors;
hs.push(s.spawn(move || {
fuse_file_sequence(store, ino, &content, &tag, errors);
}));
}
for h in hs {
h.join().unwrap();
}
});
let errs = errors.lock().unwrap().clone();
if !errs.is_empty() {
panic!("{kind:?}: round {round} failures:\n{}", errs.join("\n"));
}
}
drop(store);
let store = Store::open(dir.path(), &cfg(kind)).unwrap();
for ((name, content), ino) in groups.iter().zip(&last_round_inos) {
let back = store.read_file(*ino, 0, content.len() as u64).unwrap();
if back != *content {
let first = back
.iter()
.zip(content.iter())
.position(|(a, b)| a != b)
.unwrap_or(usize::MAX);
panic!(
"{kind:?} remount: {name} (ino {ino}) mismatch: len {} expected {}, first diff at byte {first}",
back.len(),
content.len()
);
}
}
let report = crate::fsck::fsck(dir.path(), &crate::fsck::FsckOptions::default()).unwrap();
assert!(report.is_clean(), "{kind:?} fsck:\n{}", report.render());
}
}