use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Instant, SystemTime};
use crate::serve::kv_persist::format::{
self, EnvelopeHeader, ModelFingerprint, CURRENT_FORMAT_VERSION,
};
use crate::serve::kv_persist::index::{BlockIndex, BlockMeta};
use crate::serve::kv_persist::metrics::{KvCacheMetricsSink, KvQuarantineReason};
const ORPHAN_TTL_SECS: u64 = 60;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RecoveryReport {
pub blocks_indexed: usize,
pub blocks_quarantined: usize,
pub bytes_indexed: u64,
pub bytes_quarantined: u64,
pub partial_tmp_files_ignored: usize,
pub orphan_tmp_files_removed: usize,
pub elapsed_ms: u128,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QuarantineReason {
TruncatedHeader,
VersionMismatch,
BodyHashMismatch,
ParityFail,
}
impl QuarantineReason {
fn prefix(self) -> &'static str {
match self {
QuarantineReason::TruncatedHeader => "trunc",
QuarantineReason::VersionMismatch => "verbump",
QuarantineReason::BodyHashMismatch => "bodyhash",
QuarantineReason::ParityFail => "parity",
}
}
}
impl From<QuarantineReason> for KvQuarantineReason {
fn from(r: QuarantineReason) -> KvQuarantineReason {
match r {
QuarantineReason::TruncatedHeader => KvQuarantineReason::TruncatedHeader,
QuarantineReason::VersionMismatch => KvQuarantineReason::VersionMismatch,
QuarantineReason::BodyHashMismatch => KvQuarantineReason::BodyHashMismatch,
QuarantineReason::ParityFail => KvQuarantineReason::ParityFail,
}
}
}
pub fn recover_from_disk(cache_root: &Path) -> io::Result<(BlockIndex, RecoveryReport)> {
recover_from_disk_with_counters(cache_root, None)
}
pub fn recover_from_disk_with_counters(
cache_root: &Path,
counters: Option<&Arc<dyn KvCacheMetricsSink>>,
) -> io::Result<(BlockIndex, RecoveryReport)> {
let start = Instant::now();
let index = BlockIndex::new();
let mut report = RecoveryReport::default();
let models_dir = cache_root.join("models");
if !models_dir.exists() {
report.elapsed_ms = start.elapsed().as_millis();
return Ok((index, report));
}
for slug_ent in fs::read_dir(&models_dir)? {
let slug_ent = slug_ent?;
let slug_path = slug_ent.path();
if !slug_path.is_dir() {
continue;
}
let kv_dir = slug_path.join("kv");
if !kv_dir.exists() {
continue;
}
for fanout_ent in fs::read_dir(&kv_dir)? {
let fanout_ent = fanout_ent?;
let fanout_path = fanout_ent.path();
if !fanout_path.is_dir() {
continue;
}
for blk_ent in fs::read_dir(&fanout_path)? {
let blk_ent = blk_ent?;
let blk_path = blk_ent.path();
if !blk_path.is_file() {
continue;
}
scan_one(&slug_path, &blk_path, &index, &mut report, counters)?;
}
}
}
report.elapsed_ms = start.elapsed().as_millis();
Ok((index, report))
}
fn scan_one(
slug_path: &Path,
blk_path: &Path,
index: &BlockIndex,
report: &mut RecoveryReport,
counters: Option<&Arc<dyn KvCacheMetricsSink>>,
) -> io::Result<()> {
let name = blk_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
if name.contains(".tmp.") {
report.partial_tmp_files_ignored += 1;
if let Ok(meta) = fs::metadata(blk_path) {
if let Ok(mtime) = meta.modified() {
if let Ok(age) = SystemTime::now().duration_since(mtime) {
if age.as_secs() >= ORPHAN_TTL_SECS && fs::remove_file(blk_path).is_ok() {
report.orphan_tmp_files_removed += 1;
}
}
}
}
return Ok(());
}
let file_bytes = match fs::metadata(blk_path) {
Ok(m) => m.len(),
Err(_) => 0,
};
let header = match format::read_envelope_header(blk_path) {
Ok(h) => h,
Err(_) => {
quarantine_with_prefix(slug_path, blk_path, QuarantineReason::TruncatedHeader)?;
if let Some(c) = counters {
c.record_quarantine(QuarantineReason::TruncatedHeader.into());
}
report.blocks_quarantined += 1;
report.bytes_quarantined = report.bytes_quarantined.saturating_add(file_bytes);
return Ok(());
}
};
if header.format_version != CURRENT_FORMAT_VERSION.0 {
quarantine_with_prefix(slug_path, blk_path, QuarantineReason::VersionMismatch)?;
if let Some(c) = counters {
c.record_quarantine(QuarantineReason::VersionMismatch.into());
}
report.blocks_quarantined += 1;
report.bytes_quarantined = report.bytes_quarantined.saturating_add(file_bytes);
return Ok(());
}
let metadata = fs::metadata(blk_path)?;
let mtime = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let bytes_on_disk = metadata.len();
let meta = blockmeta_from_header(&header, blk_path.to_path_buf(), mtime, bytes_on_disk);
index.insert(meta);
report.blocks_indexed += 1;
report.bytes_indexed = report.bytes_indexed.saturating_add(bytes_on_disk);
Ok(())
}
fn blockmeta_from_header(
header: &EnvelopeHeader,
file_path: PathBuf,
mtime: SystemTime,
bytes_on_disk: u64,
) -> BlockMeta {
BlockMeta {
hash: header.block_hash,
parent: header.parent_block_hash,
model_fp: header.model_fingerprint,
payload_kind: header.payload_kind.clone(),
codec_version: header.codec_version,
n_tokens: header.n_tokens,
file_path,
mtime,
bytes_on_disk,
}
}
pub fn quarantine_corrupted_block(
cache_root: &Path,
model_fp: &ModelFingerprint,
original_path: &Path,
reason: QuarantineReason,
) -> io::Result<PathBuf> {
quarantine_corrupted_block_with_counters(cache_root, model_fp, original_path, reason, None)
}
pub fn quarantine_corrupted_block_with_counters(
cache_root: &Path,
model_fp: &ModelFingerprint,
original_path: &Path,
reason: QuarantineReason,
counters: Option<&Arc<dyn KvCacheMetricsSink>>,
) -> io::Result<PathBuf> {
let slug_path = cache_root.join("models").join(model_fp.short_hex());
let dest = quarantine_with_prefix(&slug_path, original_path, reason)?;
if let Some(c) = counters {
c.record_quarantine(reason.into());
}
Ok(dest)
}
fn quarantine_with_prefix(
slug_path: &Path,
blk_path: &Path,
reason: QuarantineReason,
) -> io::Result<PathBuf> {
let q_dir = slug_path.join("kv-quarantine");
if !q_dir.exists() {
fs::create_dir_all(&q_dir)?;
}
let name = blk_path
.file_name()
.ok_or_else(|| io::Error::other(format!("blk path has no name: {}", blk_path.display())))?
.to_string_lossy()
.to_string();
let dest_name = format!("{}__{}", reason.prefix(), name);
let dest = q_dir.join(dest_name);
match fs::rename(blk_path, &dest) {
Ok(()) => Ok(dest),
Err(_) => {
fs::copy(blk_path, &dest)?;
fs::remove_file(blk_path)?;
Ok(dest)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serve::kv_persist::format::{
compute_model_fingerprint, write_envelope, BlockHash, EnvelopeHeader, ParentBlockHash,
BLOCK_TOKENS,
};
use sha2::{Digest, Sha256};
use std::process;
use std::sync::atomic::{AtomicU32, Ordering};
fn temp_dir(label: &str) -> PathBuf {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = process::id();
let nanos = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!("hf2q-kv-rec-{label}-{pid}-{nanos}-{n}"));
fs::create_dir_all(&dir).expect("temp_dir mkdir");
dir
}
fn fixture_fp(seed: &str) -> ModelFingerprint {
compute_model_fingerprint(
seed,
"Q4_0",
"hf2q-test-1.0.0",
"deadbeefcafebabe1122334455667788",
"<|im_start|>...<|im_end|>",
)
}
#[derive(Debug, Default)]
struct TestMetricsSink {
quarantines: [std::sync::atomic::AtomicU64; 4],
evictions: [std::sync::atomic::AtomicU64; 1],
}
impl TestMetricsSink {
fn new() -> Self {
Self::default()
}
fn snapshot_quarantines(&self) -> [u64; 4] {
use std::sync::atomic::Ordering;
[
self.quarantines[0].load(Ordering::Relaxed),
self.quarantines[1].load(Ordering::Relaxed),
self.quarantines[2].load(Ordering::Relaxed),
self.quarantines[3].load(Ordering::Relaxed),
]
}
}
impl crate::serve::kv_persist::metrics::KvCacheMetricsSink for TestMetricsSink {
fn record_quarantine(&self, reason: KvQuarantineReason) {
self.quarantines[reason.index()].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
fn record_eviction_budget_overflow(&self) {
self.evictions[0].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
fn make_block(
fp: ModelFingerprint,
parent: ParentBlockHash,
seed: u32,
) -> (Vec<u8>, EnvelopeHeader) {
let body: Vec<u8> = (0..512u32)
.flat_map(|i| (i.wrapping_add(seed)).to_le_bytes())
.collect();
let mut h = Sha256::new();
h.update(&body);
let bh: [u8; 32] = h.finalize().into();
let header = EnvelopeHeader {
format_version: CURRENT_FORMAT_VERSION.0,
model_fingerprint: fp,
block_hash: BlockHash(bh),
parent_block_hash: parent,
payload_kind: "kv-dense-bf16".into(),
codec_version: 1,
n_tokens: BLOCK_TOKENS,
};
(body, header)
}
fn block_path(root: &Path, fp: &ModelFingerprint, hash: &BlockHash) -> PathBuf {
let hex = hash.to_string();
let fanout = &hex[..1];
root.join("models")
.join(fp.short_hex())
.join("kv")
.join(fanout)
.join(format!("{hex}.safetensors"))
}
#[test]
fn recover_from_disk_clean_state_returns_empty_report() {
let dir = temp_dir("clean-empty");
let (idx, report) = recover_from_disk(&dir).expect("recover");
assert_eq!(idx.block_count(), 0);
assert_eq!(report.blocks_indexed, 0);
assert_eq!(report.blocks_quarantined, 0);
assert_eq!(report.bytes_indexed, 0);
assert_eq!(report.bytes_quarantined, 0);
assert_eq!(report.partial_tmp_files_ignored, 0);
fs::create_dir_all(dir.join("models")).expect("mkdir");
let (idx2, report2) = recover_from_disk(&dir).expect("recover2");
assert_eq!(idx2.block_count(), 0);
assert_eq!(report2.blocks_indexed, 0);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn recover_from_disk_50_blocks_yields_50_indexed() {
let dir = temp_dir("rec50");
let fp = fixture_fp("rec50");
let mut hashes: Vec<BlockHash> = Vec::new();
let mut total_bytes: u64 = 0;
let mut parent = ParentBlockHash(None);
for s in 0u32..50 {
let (body, header) = make_block(fp, parent, s);
let path = block_path(&dir, &fp, &header.block_hash);
write_envelope(&path, &header, &body).expect("write");
hashes.push(header.block_hash);
total_bytes += fs::metadata(&path).unwrap().len();
parent = ParentBlockHash(Some(header.block_hash));
}
let (idx, report) = recover_from_disk(&dir).expect("recover");
assert_eq!(report.blocks_indexed, 50);
assert_eq!(report.blocks_quarantined, 0);
assert_eq!(
report.bytes_indexed, total_bytes,
"bytes match real fs::metadata sum"
);
assert_eq!(report.partial_tmp_files_ignored, 0);
assert_eq!(idx.block_count(), 50);
for h in &hashes {
let m = idx.lookup(h).expect("indexed");
assert!(m.file_path.exists(), "indexed file exists on disk");
assert!(m.bytes_on_disk > 0);
}
let q = dir
.join("models")
.join(fp.short_hex())
.join("kv-quarantine");
assert!(!q.exists(), "no quarantine on clean recovery");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn recover_from_disk_with_corrupted_blocks_reports_quarantined_count() {
let dir = temp_dir("rec-quar");
let fp = fixture_fp("rec-quar");
let mut paths: Vec<PathBuf> = Vec::new();
let mut hashes: Vec<BlockHash> = Vec::new();
let mut parent = ParentBlockHash(None);
for s in 0u32..5 {
let (body, header) = make_block(fp, parent, s);
let path = block_path(&dir, &fp, &header.block_hash);
write_envelope(&path, &header, &body).expect("write");
paths.push(path);
hashes.push(header.block_hash);
parent = ParentBlockHash(Some(header.block_hash));
}
fs::write(&paths[1], b"abc").expect("truncate");
let bad_header = EnvelopeHeader {
format_version: 999,
model_fingerprint: fp,
block_hash: hashes[3],
parent_block_hash: ParentBlockHash(Some(hashes[2])),
payload_kind: "kv-dense-bf16".into(),
codec_version: 1,
n_tokens: BLOCK_TOKENS,
};
let header_json = serde_json::to_vec(&bad_header).expect("ser");
let pad = (8 - (header_json.len() % 8)) % 8;
let mut header_bytes = header_json;
header_bytes.extend(std::iter::repeat(b' ').take(pad));
let mut blob = Vec::new();
blob.extend_from_slice(&(header_bytes.len() as u64).to_le_bytes());
blob.extend_from_slice(&header_bytes);
blob.extend_from_slice(&[0u8; 64]);
fs::write(&paths[3], &blob).expect("write bad");
let (idx, report) = recover_from_disk(&dir).expect("recover");
assert_eq!(report.blocks_indexed, 3);
assert_eq!(report.blocks_quarantined, 2);
assert!(report.bytes_indexed > 0);
assert!(report.bytes_quarantined > 0, "quarantined bytes accounted");
assert_eq!(idx.block_count(), 3);
let q = dir
.join("models")
.join(fp.short_hex())
.join("kv-quarantine");
let q_files: Vec<String> = fs::read_dir(&q)
.expect("read q")
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(q_files.len(), 2, "two quarantined files; saw {q_files:?}");
assert!(
q_files.iter().any(|n| n.starts_with("trunc__")),
"trunc__ prefix"
);
assert!(
q_files.iter().any(|n| n.starts_with("verbump__")),
"verbump__ prefix"
);
assert!(!paths[1].exists());
assert!(!paths[3].exists());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn quarantine_truncated_header_moves_file_to_kv_quarantine_dir() {
let dir = temp_dir("q-trunc");
let fp = fixture_fp("q-trunc");
let (body, header) = make_block(fp, ParentBlockHash(None), 0);
let original = block_path(&dir, &fp, &header.block_hash);
write_envelope(&original, &header, &body).expect("write");
let dest =
quarantine_corrupted_block(&dir, &fp, &original, QuarantineReason::TruncatedHeader)
.expect("quarantine");
assert!(!original.exists());
assert!(dest.exists());
let dest_str = dest.to_string_lossy();
assert!(dest_str.contains("/kv-quarantine/"));
let dest_name = dest.file_name().unwrap().to_string_lossy().into_owned();
assert!(
dest_name.starts_with("trunc__"),
"trunc prefix: {dest_name}"
);
assert!(dest_name.ends_with(".safetensors"));
let moved_bytes = fs::read(&dest).expect("read moved");
assert!(moved_bytes.len() > 8, "moved file has content");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn quarantine_body_hash_mismatch_uses_distinct_reason_prefix() {
let dir = temp_dir("q-prefix");
let fp = fixture_fp("q-prefix");
let reasons = [
QuarantineReason::TruncatedHeader,
QuarantineReason::VersionMismatch,
QuarantineReason::BodyHashMismatch,
QuarantineReason::ParityFail,
];
let mut prefixes: Vec<&'static str> = Vec::new();
for (i, reason) in reasons.iter().enumerate() {
let (body, header) = make_block(fp, ParentBlockHash(None), i as u32);
let original = block_path(&dir, &fp, &header.block_hash);
write_envelope(&original, &header, &body).expect("write");
let dest =
quarantine_corrupted_block(&dir, &fp, &original, *reason).expect("quarantine");
let name = dest.file_name().unwrap().to_string_lossy().into_owned();
let prefix = name
.split("__")
.next()
.expect("prefix split")
.to_string()
.leak() as &'static str;
prefixes.push(prefix);
assert!(dest.exists());
assert!(!original.exists());
}
let mut sorted = prefixes.clone();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), 4, "four distinct prefixes; got {prefixes:?}");
assert!(prefixes.contains(&"trunc"));
assert!(prefixes.contains(&"verbump"));
assert!(prefixes.contains(&"bodyhash"));
assert!(prefixes.contains(&"parity"));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn recover_ignores_tmp_files_and_counts_them() {
let dir = temp_dir("rec-tmp");
let fp = fixture_fp("rec-tmp");
let mut hashes: Vec<BlockHash> = Vec::new();
let mut parent = ParentBlockHash(None);
for s in 0u32..3 {
let (body, header) = make_block(fp, parent, s);
let path = block_path(&dir, &fp, &header.block_hash);
write_envelope(&path, &header, &body).expect("write");
hashes.push(header.block_hash);
parent = ParentBlockHash(Some(header.block_hash));
}
let fanout_dir_a = dir
.join("models")
.join(fp.short_hex())
.join("kv")
.join(&hashes[0].to_string()[..1]);
fs::write(
fanout_dir_a.join(format!("orphan.safetensors.tmp.{}", process::id())),
b"partial-bytes-1",
)
.expect("write orphan a");
fs::write(
fanout_dir_a.join(format!("other.safetensors.tmp.{}", process::id() + 1)),
b"partial-bytes-2",
)
.expect("write orphan b");
let (idx, report) = recover_from_disk(&dir).expect("recover");
assert_eq!(report.blocks_indexed, 3);
assert_eq!(report.blocks_quarantined, 0);
assert_eq!(report.partial_tmp_files_ignored, 2);
assert_eq!(idx.block_count(), 3);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn recovery_report_elapsed_ms_is_nonzero_for_real_walk() {
let dir = temp_dir("rec-elapsed");
let fp = fixture_fp("rec-elapsed");
let mut parent = ParentBlockHash(None);
for s in 0u32..3 {
let (body, header) = make_block(fp, parent, s);
let path = block_path(&dir, &fp, &header.block_hash);
write_envelope(&path, &header, &body).expect("write");
parent = ParentBlockHash(Some(header.block_hash));
}
let (_, report) = recover_from_disk(&dir).expect("recover");
assert!(
report.elapsed_ms < 60_000,
"scan completed in well under 60s: {} ms",
report.elapsed_ms
);
let _ = fs::remove_dir_all(&dir);
}
fn backdate_mtime(path: &Path, secs: u64) -> bool {
let now = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time")
.as_secs() as i64;
let target = now - secs as i64;
let tv = [
libc::timeval {
tv_sec: target as libc::time_t,
tv_usec: 0,
},
libc::timeval {
tv_sec: target as libc::time_t,
tv_usec: 0,
},
];
let cstr =
std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).expect("path -> cstring");
let rc = unsafe { libc::utimes(cstr.as_ptr(), tv.as_ptr()) };
rc == 0
}
#[test]
fn p0_3_orphan_tmp_files_older_than_ttl_are_gc_at_recovery() {
let dir = temp_dir("rec-p0-3-gc");
let fp = fixture_fp("rec-p0-3-gc");
let (body, header) = make_block(fp, ParentBlockHash(None), 0);
let valid_path = block_path(&dir, &fp, &header.block_hash);
write_envelope(&valid_path, &header, &body).expect("write valid");
let fanout_dir = valid_path.parent().unwrap().to_path_buf();
let recent = fanout_dir.join(format!("recent.safetensors.tmp.{}", process::id()));
fs::write(&recent, b"recent orphan").expect("write recent");
let aged = fanout_dir.join(format!("aged.safetensors.tmp.{}", process::id() + 1));
fs::write(&aged, b"aged orphan").expect("write aged");
if !backdate_mtime(&aged, 120) {
let _ = fs::remove_dir_all(&dir);
return;
}
let (_idx, report) = recover_from_disk(&dir).expect("recover");
assert_eq!(
report.partial_tmp_files_ignored, 2,
"both orphans counted as ignored"
);
assert_eq!(
report.orphan_tmp_files_removed, 1,
"exactly the aged orphan was removed"
);
assert!(recent.exists(), "recent orphan kept");
assert!(!aged.exists(), "aged orphan removed");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn quarantined_total_bumps_on_truncated_header_quarantine() {
let dir = temp_dir("rf7-q-trunc");
let fp = fixture_fp("rf7-q-trunc");
let concrete = std::sync::Arc::new(TestMetricsSink::new());
let as_trait: std::sync::Arc<dyn KvCacheMetricsSink> =
std::sync::Arc::clone(&concrete) as std::sync::Arc<dyn KvCacheMetricsSink>;
assert_eq!(concrete.snapshot_quarantines(), [0u64, 0, 0, 0]);
let reasons = [
QuarantineReason::TruncatedHeader,
QuarantineReason::VersionMismatch,
QuarantineReason::BodyHashMismatch,
QuarantineReason::ParityFail,
];
for (i, reason) in reasons.iter().enumerate() {
let (body, header) = make_block(fp, ParentBlockHash(None), i as u32);
let original = block_path(&dir, &fp, &header.block_hash);
write_envelope(&original, &header, &body).expect("write");
let _dest = quarantine_corrupted_block_with_counters(
&dir,
&fp,
&original,
*reason,
Some(&as_trait),
)
.expect("quarantine");
}
assert_eq!(
concrete.snapshot_quarantines(),
[1u64, 1, 1, 1],
"all four QuarantineReason variants bumped their row by 1"
);
let (body, header) = make_block(fp, ParentBlockHash(None), 99);
let original = block_path(&dir, &fp, &header.block_hash);
write_envelope(&original, &header, &body).expect("write");
let _ = quarantine_corrupted_block_with_counters(
&dir,
&fp,
&original,
QuarantineReason::TruncatedHeader,
Some(&as_trait),
)
.expect("quarantine");
assert_eq!(
concrete.snapshot_quarantines(),
[2u64, 1, 1, 1],
"trunc-row +1 only; other three rows unchanged"
);
let (body2, header2) = make_block(fp, ParentBlockHash(None), 100);
let original2 = block_path(&dir, &fp, &header2.block_hash);
write_envelope(&original2, &header2, &body2).expect("write");
let _ =
quarantine_corrupted_block(&dir, &fp, &original2, QuarantineReason::BodyHashMismatch)
.expect("quarantine");
assert_eq!(
concrete.snapshot_quarantines(),
[2u64, 1, 1, 1],
"legacy entry point with no counters MUST NOT bump"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn kv_persist_recovery_scan_bumps_quarantined_total_on_truncated_block() {
let dir = temp_dir("rf7-rec-scan");
let fp = fixture_fp("rf7-rec-scan");
let concrete = std::sync::Arc::new(TestMetricsSink::new());
let as_trait: std::sync::Arc<dyn KvCacheMetricsSink> =
std::sync::Arc::clone(&concrete) as std::sync::Arc<dyn KvCacheMetricsSink>;
let (body, header) = make_block(fp, ParentBlockHash(None), 0);
let path = block_path(&dir, &fp, &header.block_hash);
write_envelope(&path, &header, &body).expect("write");
std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&path)
.expect("open trunc")
.set_len(4)
.expect("set_len");
std::fs::write(&path, [0u8, 0, 0, 0]).expect("write 4 bytes");
let (_idx, report) =
recover_from_disk_with_counters(&dir, Some(&as_trait)).expect("recover");
assert_eq!(report.blocks_quarantined, 1, "1 file quarantined");
assert_eq!(
concrete.snapshot_quarantines(),
[1u64, 0, 0, 0],
"trunc row bumped; other three rows untouched"
);
let _ = fs::remove_dir_all(&dir);
}
}