use super::*;
use crate::fs::{Fs, MemFs};
use std::path::Path;
fn write_file(fs: &MemFs, path: &Path, bytes: &[u8]) {
use std::io::Write;
let opts = crate::fs::FsOpenOptions::new().write(true).create(true);
let mut f = fs.open(path, &opts).unwrap();
f.write_all(bytes).unwrap();
}
pub(super) mod drain_barrier {
use std::sync::Mutex;
use std::sync::mpsc;
static CHANNEL: Mutex<Option<mpsc::Receiver<()>>> = Mutex::new(None);
pub fn arm() -> mpsc::Sender<()> {
let (tx, rx) = mpsc::channel();
*CHANNEL.lock().unwrap() = Some(rx);
tx
}
pub fn wait() {
let rx = CHANNEL.lock().unwrap().take();
if let Some(rx) = rx {
let _ = rx.recv();
}
}
}
#[test]
fn deletion_pause_defers_then_executes_removal() {
let fs = MemFs::new();
fs.create_dir_all(Path::new("/d")).unwrap();
let path = Path::new("/d/file.sst").to_path_buf();
write_file(&fs, &path, b"sst");
let dyn_fs: Arc<dyn Fs> = Arc::new(fs.clone());
let pause = DeletionPause::new_shared();
let guard = pause.acquire();
assert!(pause.try_enqueue(dyn_fs.clone(), path.clone()));
assert!(
fs.exists(&path).unwrap(),
"file must still exist while paused"
);
drop(guard);
assert!(
!fs.exists(&path).unwrap(),
"file must be removed after pause released"
);
}
#[test]
fn enqueue_returns_false_when_inactive() {
let fs = MemFs::new();
fs.create_dir_all(Path::new("/d")).unwrap();
let path = Path::new("/d/file.sst").to_path_buf();
write_file(&fs, &path, b"x");
let dyn_fs: Arc<dyn Fs> = Arc::new(fs.clone());
let pause = DeletionPause::new_shared();
assert!(!pause.try_enqueue(dyn_fs, path.clone()));
assert!(fs.exists(&path).unwrap());
}
#[test]
fn drain_does_not_steal_a_new_generation_queue() {
use std::sync::mpsc;
use std::thread;
let fs = MemFs::new();
fs.create_dir_all(Path::new("/d")).unwrap();
let path = Path::new("/d/race.sst").to_path_buf();
write_file(&fs, &path, b"keep-me");
let dyn_fs: Arc<dyn Fs> = Arc::new(fs.clone());
let pause = DeletionPause::new_shared();
let a = pause.acquire();
let release_a_tx = drain_barrier::arm();
let (in_window_tx, in_window_rx) = mpsc::channel::<()>();
let (b_ready_tx, b_ready_rx) = mpsc::channel::<()>();
let (release_b_tx, release_b_rx) = mpsc::channel::<()>();
let a_pause = Arc::clone(&pause);
let a_thread = thread::spawn(move || {
in_window_tx.send(()).unwrap();
drop(a);
drop(a_pause);
});
in_window_rx.recv().unwrap();
while pause.active.load(Ordering::Acquire) != 0 {
core::hint::spin_loop();
}
let b_pause = Arc::clone(&pause);
let b_fs = Arc::clone(&dyn_fs);
let b_path = path.clone();
let b_thread = thread::spawn(move || {
let _b = b_pause.acquire();
assert!(b_pause.try_enqueue(b_fs, b_path));
b_ready_tx.send(()).unwrap();
release_b_rx.recv().unwrap();
});
b_ready_rx.recv().unwrap();
release_a_tx.send(()).unwrap();
a_thread.join().unwrap();
assert!(
fs.exists(&path).unwrap(),
"file must survive while Thread B holds an active pause \
(a's drain leaked into b's generation)",
);
release_b_tx.send(()).unwrap();
b_thread.join().unwrap();
assert!(
!fs.exists(&path).unwrap(),
"file should be removed after both pauses dropped",
);
}
#[test]
fn a_deferred_punch_survives_an_unprovable_link_count_and_is_retried() {
use crate::fs::{Fault, FaultFs, FaultOp, FaultRule};
let mem = MemFs::new();
mem.create_dir_all(Path::new("/d")).unwrap();
let path = Path::new("/d/1").to_path_buf();
write_file(&mem, &path, &[b'x'; 4096]);
let faulty = Arc::new(FaultFs::new(mem.clone()));
let dyn_fs: Arc<dyn Fs> = faulty.clone();
faulty.injector().arm(FaultRule::new(
FaultOp::HardLinkCount,
Fault::Error(crate::io::ErrorKind::PermissionDenied),
));
let pause = DeletionPause::new_shared();
let guard = pause.acquire();
assert!(pause.try_enqueue_punch(Arc::clone(&dyn_fs), path, vec![(0, 2048)]));
drop(guard);
assert_eq!(
mem.punched_bytes(),
0,
"an unprovable link count must not be punched through",
);
assert!(
pause.has_pending_reclaims(),
"the reclaim is retained, not discarded: nothing else would ever free \
the consumed prefix",
);
faulty.injector().clear();
pause.retry_pending_reclaims();
assert_eq!(
mem.punched_bytes(),
2048,
"the retained reclaim runs once the file can be proven exclusive",
);
assert!(
!pause.has_pending_reclaims(),
"a completed reclaim is not retained",
);
}
#[test]
fn a_deferred_punch_waits_for_an_open_checkpoint_link_window() {
use std::sync::mpsc;
let mem = MemFs::new();
mem.create_dir_all(Path::new("/d")).unwrap();
let path = Path::new("/d/1").to_path_buf();
write_file(&mem, &path, &[b'x'; 4096]);
let dyn_fs: Arc<dyn Fs> = Arc::new(mem.clone());
let pause = DeletionPause::new_shared();
let guard = pause.acquire();
assert!(pause.try_enqueue_punch(Arc::clone(&dyn_fs), path, vec![(0, 2048)]));
let checkpoint = pause.enter_link_window();
let retrier = Arc::clone(&pause);
let (done_tx, done_rx) = mpsc::channel::<()>();
let handle = std::thread::spawn(move || {
drop(guard); retrier.retry_pending_reclaims();
done_tx.send(()).ok();
});
assert!(
done_rx
.recv_timeout(std::time::Duration::from_millis(250))
.is_err(),
"a deferred punch must not run while a checkpoint's link window is open",
);
assert_eq!(
mem.punched_bytes(),
0,
"and it must not have punched the inode the checkpoint is linking",
);
drop(checkpoint);
assert!(handle.join().is_ok(), "the retry thread finishes");
assert_eq!(
mem.punched_bytes(),
2048,
"once the link window closes the reclaim runs",
);
}
#[test]
fn a_failed_punch_retains_the_extent_and_the_untried_remainder() {
use crate::fs::{Fault, FaultFs, FaultOp, FaultRule};
let mem = MemFs::new();
mem.create_dir_all(Path::new("/d")).unwrap();
let path = Path::new("/d/1").to_path_buf();
write_file(&mem, &path, &[b'x'; 4096]);
let faulty = Arc::new(FaultFs::new(mem.clone()));
let dyn_fs: Arc<dyn Fs> = faulty.clone();
faulty.injector().arm(
FaultRule::new(
FaultOp::PunchHole,
Fault::Error(crate::io::ErrorKind::Interrupted),
)
.skip(1)
.once(),
);
let pause = DeletionPause::new_shared();
let guard = pause.acquire();
assert!(pause.try_enqueue_punch(
Arc::clone(&dyn_fs),
path,
vec![(3072, 1024), (2048, 1024), (1024, 1024)],
));
drop(guard);
assert_eq!(
mem.punched_bytes(),
1024,
"the pass stops at the first failure, so only the top extent landed",
);
assert!(
pause.has_pending_reclaims(),
"the failed extent and the untried remainder are retained, or nothing \
would ever free that space",
);
pause.retry_pending_reclaims();
assert_eq!(
mem.punched_bytes(),
3072,
"the retry reclaims the extents the failed pass left behind",
);
assert!(!pause.has_pending_reclaims(), "nothing left to retry");
}
#[test]
fn nested_pauses_only_release_on_last_drop() {
let fs = MemFs::new();
fs.create_dir_all(Path::new("/d")).unwrap();
let path = Path::new("/d/file.sst").to_path_buf();
write_file(&fs, &path, b"x");
let dyn_fs: Arc<dyn Fs> = Arc::new(fs.clone());
let pause = DeletionPause::new_shared();
let outer = pause.acquire();
let inner = pause.acquire();
assert!(pause.try_enqueue(dyn_fs, path.clone()));
drop(inner);
assert!(fs.exists(&path).unwrap(), "still paused by outer guard");
drop(outer);
assert!(
!fs.exists(&path).unwrap(),
"released after last guard dropped"
);
}