#![forbid(unsafe_code)]
use std::sync::Arc;
use tempfile::TempDir;
use crate::optimizer::foreground::ForegroundPolicy;
use crate::optimizer::policy::OptimizeOptions;
use crate::store::transaction::CrashHooks;
use crate::store::{NewEntry, Store, StoreConfig};
fn create_store(dir: &TempDir) -> Arc<Store> {
Arc::new(Store::create(dir.path(), &StoreConfig::default(), [0x81; 16]).unwrap())
}
fn stage_overdepth_chain(store: &Arc<Store>) -> u64 {
let hooks = &CrashHooks::none();
let root = store.current_root().root_dir_ino;
let dir_ino = store
.epoch_create(root, b"d", NewEntry::dir(0o755, 1000, 1000), hooks)
.unwrap();
let file_ino = store
.epoch_create(dir_ino, b"f", NewEntry::file(0o644, 1000, 1000), hooks)
.unwrap();
store.ensure_epoch_flushed(hooks).unwrap();
store.epoch_checkpoint(hooks).unwrap();
let b5: Vec<u8> = (0..64u8)
.map(|i| b"abcdefghijklmnopqrstuvwxyz012345"[i as usize % 32])
.collect();
let c5 = crate::core::extent::ChunkId::of(&b5);
let raw5 = crate::core::representation::Representation::Raw {
obj: c5,
len: b5.len() as u64,
};
store
.commit_file_extents(
file_ino,
vec![crate::store::ExtentUpdate {
offset: 0,
descriptor: raw5,
content_id: c5,
objects: vec![crate::core::candidate::ObjectRecord::data(b5.clone())],
}],
None,
hooks,
)
.unwrap();
let mut prev_id = c5;
let mut prev_bytes = b5;
let xors = [0xABu8, 0xCD, 0xEF, 0x12];
let mut ids = Vec::new();
for x in xors.iter() {
let cur_bytes: Vec<u8> = prev_bytes
.iter()
.enumerate()
.map(|(j, b)| if j == 0 { b ^ x } else { *b })
.collect();
let cur_id = crate::core::extent::ChunkId::of(&cur_bytes);
let desc = crate::core::representation::Representation::BaseResidual {
base: prev_id,
base_len: prev_bytes.len() as u64,
residual: crate::core::representation::Residual::XorSparse {
len: prev_bytes.len() as u64,
edits: vec![crate::core::representation::Edit { pos: 0, val: *x }],
},
len: cur_bytes.len() as u64,
};
store
.commit_file_extents(
file_ino,
vec![crate::store::ExtentUpdate {
offset: 0,
descriptor: desc,
content_id: cur_id,
objects: Vec::new(),
}],
None,
hooks,
)
.unwrap();
ids.push(cur_id);
prev_id = cur_id;
prev_bytes = cur_bytes;
}
let e_bytes: Vec<u8> = prev_bytes
.iter()
.enumerate()
.map(|(j, b)| if j == 0 { b ^ 0x56 } else { *b })
.collect();
let e_cid = crate::core::extent::ChunkId::of(&e_bytes);
let e_desc = crate::core::representation::Representation::BaseResidual {
base: prev_id,
base_len: prev_bytes.len() as u64,
residual: crate::core::representation::Residual::XorSparse {
len: prev_bytes.len() as u64,
edits: vec![crate::core::representation::Edit { pos: 0, val: 0x56 }],
},
len: e_bytes.len() as u64,
};
store
.commit_file_extents(
file_ino,
vec![crate::store::ExtentUpdate {
offset: 0,
descriptor: e_desc,
content_id: e_cid,
objects: Vec::new(),
}],
None,
hooks,
)
.unwrap();
store.ensure_epoch_flushed(hooks).unwrap();
store.epoch_checkpoint(hooks).unwrap();
file_ino
}
#[test]
fn overdepth_chain_is_detected_and_repaired() {
let dir = TempDir::new().unwrap();
let store = create_store(&dir);
let file_ino = stage_overdepth_chain(&store);
let limits = *store.limits();
let extent = store
.extent_descriptor(file_ino, 0)
.unwrap()
.expect("extent exists");
let desc = crate::format::descriptor::decode(&extent, &limits).unwrap();
assert!(
crate::core::materialize::materialize_to_vec(&desc, &*store, &limits).is_err(),
"the staged chain must exceed the decode cap"
);
assert_eq!(
crate::optimizer::rebase::chain_depth_uncapped(&store, &desc, &Default::default()),
5,
"the uncapped walk must see the over-depth chain"
);
let rebased = store.rebase_overdepth_extents(&CrashHooks::none()).unwrap();
assert!(rebased >= 1, "the over-depth extent must be rebased");
let after = store
.extent_descriptor(file_ino, 0)
.unwrap()
.expect("extent still exists");
let after_desc = crate::format::descriptor::decode(&after, &limits).unwrap();
assert_eq!(
crate::optimizer::rebase::depth_of(&after_desc),
0,
"the repaired extent must be terminal (depth 0)"
);
let bytes = crate::core::materialize::materialize_to_vec(&after_desc, &*store, &limits)
.expect("repaired extent materializes");
assert_eq!(bytes.len(), 64);
let again = store.rebase_overdepth_extents(&CrashHooks::none()).unwrap();
assert_eq!(again, 0, "the repair must be idempotent");
}
#[test]
fn optimize_pass_survives_and_repairs_overdepth_store() {
let dir = TempDir::new().unwrap();
let store = create_store(&dir);
stage_overdepth_chain(&store);
let stats =
crate::optimizer::background::optimize_pass(&store, OptimizeOptions::default(), None, None)
.expect("optimize_pass must not crash on an over-depth store");
assert!(stats.errors == 0, "no extent may error: {stats:?}");
let limits = *store.limits();
let inos = store.all_inodes().unwrap();
let mut live_checked = 0u64;
for ino in inos {
let Some(inode) = store.get_inode(ino).unwrap() else {
continue;
};
if !inode.is_file() {
continue;
}
let extent_root = match &inode.data {
crate::store::inode::InodeData::File { extent_root } => *extent_root,
_ => continue,
};
if extent_root.is_zero() {
continue;
}
let entries = crate::store::extent_tree::scan_all(
extent_root,
crate::store::BTREE_ORDER,
limits.max_fanout,
&*store,
)
.unwrap();
for (_start, desc_bytes) in entries {
live_checked += 1;
let desc = crate::format::descriptor::decode(&desc_bytes, &limits).unwrap();
assert!(
crate::core::materialize::materialize_to_vec(&desc, &*store, &limits).is_ok(),
"every live extent must be readable after the repair"
);
}
}
assert!(live_checked >= 1);
}
#[test]
fn write_path_base_channels_tolerate_overlay_only_inodes() {
for pool in [false, true] {
let dir = TempDir::new().unwrap();
let store = create_store(&dir);
let _pool_guard = if pool {
Some(
crate::store::workers::tests::POOL_LOCK
.lock()
.expect("poisoned"),
)
} else {
None
};
if pool {
crate::store::workers::POOL.enable(16, 8);
crate::store::workers::POOL.bind(&store);
store.enable_worker_pool();
}
let hooks = &CrashHooks::none();
let fg = ForegroundPolicy::full();
let opts = OptimizeOptions::default();
let root = store.current_root().root_dir_ino;
let dir_ino = store
.epoch_create(root, b"d", NewEntry::dir(0o755, 1000, 1000), hooks)
.unwrap();
let ino = store
.epoch_create(dir_ino, b"f", NewEntry::file(0o644, 1000, 1000), hooks)
.unwrap();
for r in 0..6u64 {
let mut b = Vec::with_capacity(1024 * 1024);
let mut x: u64 = r * 0x9E3779B97F4A7C15 + 1;
while b.len() < 1024 * 1024 {
x = x
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
b.extend_from_slice(
format!(
"f-{r} line={} key={:04x} val={:06x} name=obj-{}\n",
b.len(),
x & 0xffff,
(x >> 16) & 0xffffff,
(x >> 20) % 10000,
)
.as_bytes(),
);
}
store
.epoch_write_semantic(ino, r * 1024 * 1024, &b, opts, fg, None, hooks)
.expect("the write must tolerate the overlay-only inode");
}
store.ensure_epoch_flushed(hooks).unwrap();
store.epoch_checkpoint(hooks).unwrap();
let limits = *store.limits();
let inode = store.get_inode(ino).unwrap().unwrap();
let extent_root = match &inode.data {
crate::store::inode::InodeData::File { extent_root } => *extent_root,
_ => unreachable!(),
};
let entries = crate::store::extent_tree::scan_all(
extent_root,
crate::store::BTREE_ORDER,
limits.max_fanout,
&*store,
)
.unwrap();
assert!(!entries.is_empty());
for (_start, desc_bytes) in entries {
let desc = crate::format::descriptor::decode(&desc_bytes, &limits).unwrap();
assert!(
crate::core::materialize::materialize_to_vec(&desc, &*store, &limits).is_ok(),
"no over-depth chain from the sequential-append pattern"
);
}
if pool {
crate::store::workers::POOL.disable();
}
}
}