use super::*;
use crate::service_fault::hooks;
use crate::service_index_marker::LOCK_MARKER;
use crate::test_support::{fixture, remove};
fn stale_lock_text(seq: u64) -> String {
let mut child = std::process::Command::new("true").spawn().expect("a short process");
let pid: i64 = i64::from(child.id());
child.wait().expect("reaped");
return format!("{LOCK_MARKER}{seq} {pid} gone\n");
}
fn index_in(name: &str) -> (PathBuf, PathBuf) {
let root: PathBuf = fixture(name);
let index: PathBuf = root.join("index");
std::fs::write(&index, "old index").expect("index");
return (root, index);
}
#[test]
fn a_stale_lock_is_removed_and_leaves_no_file() {
let (root, index) = index_in("reclaim-plain");
std::fs::write(lock_path_of(&index), stale_lock_text(5)).expect("stale lock");
remove_stale(&index, 5);
assert!(!lock_path_of(&index).exists());
let names: Vec<String> = std::fs::read_dir(&root).expect("listing").flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).collect();
assert_eq!(names, vec![String::from("index")], "no taken-aside file is left");
remove(root.as_path());
}
#[test]
fn a_lock_placed_after_the_stale_one_was_found_is_not_removed() {
let (root, index) = index_in("reclaim-replaced");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, stale_lock_text(5)).expect("stale lock");
let (hook_lock, hook_root) = (lock.clone(), root.clone());
hooks::set("reclaim-judged", std::rc::Rc::new(move || {
std::fs::remove_file(&hook_lock).expect("the stale lock is removed by someone else");
std::fs::write(hook_root.join("staged-foreign"), "a foreign program's lock").expect("staged");
std::fs::rename(hook_root.join("staged-foreign"), &hook_lock).expect("placed");
}));
remove_stale(&index, 5);
assert_eq!(std::fs::read_to_string(&lock).expect("the foreign lock stands"), "a foreign program's lock");
let names: Vec<String> = std::fs::read_dir(&root).expect("listing").flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).collect();
assert_eq!(names.len(), 2, "only the index and the lock: {names:?}");
remove(root.as_path());
}
#[test]
fn a_foreign_lock_and_a_running_holders_lock_stay() {
let (root, index) = index_in("reclaim-foreign");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, "foreign").expect("foreign");
remove_stale(&index, 5);
assert_eq!(std::fs::read_to_string(&lock).expect("stands"), "foreign");
remove(root.as_path());
}
#[test]
fn a_live_lock_found_by_a_late_reclaimer_is_never_open_to_a_third_program() {
let (root, index) = index_in("reclaim-late-reclaimer");
let lock: PathBuf = lock_path_of(&index);
let live = try_acquire(&index, 7).expect("the live holder places its lock");
let live_marker: String = std::fs::read_to_string(&lock).expect("the live marker");
let third_placed = std::rc::Rc::new(std::cell::Cell::new(false));
let (hook_lock, hook_placed) = (lock.clone(), std::rc::Rc::clone(&third_placed));
hooks::set("reclaim-opened", std::rc::Rc::new(move || {
let created = std::fs::OpenOptions::new().write(true).create_new(true).open(&hook_lock);
hook_placed.set(created.is_ok());
}));
let outcome: Reclaimed = reclaim(&SYSTEM, &lock);
assert_eq!(outcome, Reclaimed::Kept, "a lock whose holder runs is not removed");
assert!(!third_placed.get(), "a third program placed its lock while the reclaimer worked on the live holder's lock");
assert_eq!(std::fs::read_to_string(&lock).expect("a lock stands"), live_marker, "the live holder's lock stands under its name");
let names: Vec<String> = std::fs::read_dir(&root).expect("listing").flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).collect();
assert_eq!(names.len(), 2, "only the index and the lock: {names:?}");
drop(live);
assert!(!lock.exists(), "the live holder still owns its lock and releases it");
remove(root.as_path());
}
#[test]
fn a_reclaimer_whose_judged_file_was_replaced_removes_nothing() {
let (root, index) = index_in("reclaim-replaced-inode");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, stale_lock_text(5)).expect("stale lock");
let live_cell: std::rc::Rc<std::cell::RefCell<Option<IndexLock>>> = std::rc::Rc::new(std::cell::RefCell::new(None));
let (hook_lock, hook_index, hook_live) = (lock.clone(), index.clone(), std::rc::Rc::clone(&live_cell));
hooks::set("reclaim-judged", std::rc::Rc::new(move || {
std::fs::remove_file(&hook_lock).expect("the other reclaimer removes the stale lock");
*hook_live.borrow_mut() = Some(try_acquire(&hook_index, 8).expect("the live landing places its lock"));
}));
let outcome: Reclaimed = reclaim(&SYSTEM, &lock);
assert_eq!(outcome, Reclaimed::Changed);
assert_eq!(std::fs::read_to_string(&lock).expect("the live lock stands"), marker_text(8, &crate::service_index_marker::own_identity).expect("this process has an identity"));
let live = live_cell.borrow_mut().take().expect("the live lock");
drop(live);
assert!(!lock.exists(), "the live holder still owns its lock and releases it");
remove(root.as_path());
}
#[test]
fn a_second_reclaimer_of_one_stale_lock_backs_off_while_the_first_holds_it() {
let (root, index) = index_in("reclaim-two-reclaimers");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, stale_lock_text(5)).expect("stale lock");
let second: std::rc::Rc<std::cell::RefCell<Option<Reclaimed>>> = std::rc::Rc::new(std::cell::RefCell::new(None));
let (hook_lock, hook_second) = (lock.clone(), std::rc::Rc::clone(&second));
hooks::set("reclaim-held", std::rc::Rc::new(move || {
*hook_second.borrow_mut() = Some(reclaim(&SYSTEM, &hook_lock));
}));
let first: Reclaimed = reclaim(&SYSTEM, &lock);
assert_eq!(*second.borrow(), Some(Reclaimed::Busy), "the second reclaimer backs off");
assert_eq!(first, Reclaimed::Removed, "the first reclaimer removes the stale lock");
assert!(!lock.exists());
remove(root.as_path());
}
#[test]
fn a_reclaimer_that_backed_off_can_reclaim_once_the_hold_is_dropped() {
let (root, index) = index_in("reclaim-after-busy");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, stale_lock_text(5)).expect("stale lock");
let holding = std::fs::File::open(&lock).expect("the first reclaimer's descriptor");
let taken: i32 = unsafe { libc::flock(std::os::fd::AsRawFd::as_raw_fd(&holding), libc::LOCK_EX | libc::LOCK_NB) };
assert_eq!(taken, 0, "the control plays the first reclaimer");
assert_eq!(reclaim(&SYSTEM, &lock), Reclaimed::Busy);
assert!(lock.exists(), "a reclaimer that backs off removes nothing");
drop(holding);
assert_eq!(reclaim(&SYSTEM, &lock), Reclaimed::Removed);
assert!(!lock.exists());
remove(root.as_path());
}
fn counting_hold<'a>(refused: &'a std::cell::Cell<usize>, first_refusal: &'a std::sync::mpsc::Sender<()>) -> impl Fn(&std::fs::File) -> std::io::Result<()> + 'a {
return move |file: &std::fs::File| -> std::io::Result<()> {
let answer = crate::service_index_reclaim::flock_exclusive(file);
if answer.as_ref().is_err_and(|error| { return error.kind() == std::io::ErrorKind::WouldBlock; }) {
refused.set(refused.get() + 1);
if refused.get() == 1 {
let _told = first_refusal.send(());
}
}
return answer;
};
}
fn spawn_other_reclaimer(lock: PathBuf, taken: std::sync::mpsc::Sender<()>, let_go: std::sync::mpsc::Receiver<()>, give_up: std::time::Duration) -> std::thread::JoinHandle<()> {
return std::thread::spawn(move || {
let holding = std::fs::File::open(&lock).expect("the other reclaimer's descriptor");
let result: i32 = unsafe { libc::flock(std::os::fd::AsRawFd::as_raw_fd(&holding), libc::LOCK_EX | libc::LOCK_NB) };
assert_eq!(result, 0, "the control plays the other reclaimer");
taken.send(()).expect("the control waits for the hold");
let _told = let_go.recv_timeout(give_up);
drop(holding);
});
}
#[test]
fn a_landing_waits_for_the_reclaimer_that_holds_a_stale_lock() {
let (root, index) = index_in("reclaim-landing-busy");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, stale_lock_text(5)).expect("stale lock");
let ceiling: std::time::Duration = crate::service_index_watch::POLL_PAUSE * 20;
let (taken_tx, taken_rx) = std::sync::mpsc::channel::<()>();
let (let_go_tx, let_go_rx) = std::sync::mpsc::channel::<()>();
let reclaimer = spawn_other_reclaimer(lock.clone(), taken_tx, let_go_rx, ceiling * 4);
taken_rx.recv().expect("the hold is taken");
let refused = std::cell::Cell::new(0_usize);
let (first_tx, _first_rx) = std::sync::mpsc::channel::<()>();
let hold = counting_hold(&refused, &first_tx);
let platform: Platform = Platform { flock: &hold, ..SYSTEM };
let began: std::time::Instant = std::time::Instant::now();
let failure = acquire_within(&platform, &index, 9, ceiling, &|_| {}).expect_err("the lock is not taken while the other reclaimer holds it");
assert_eq!(failure.code, FailureCode::IndexInUse, "the wait ends at its ceiling, as for any held lock");
assert!(began.elapsed() >= ceiling, "the wait ended before its ceiling");
assert!(refused.get() > 1, "the landing looked again while the hold lasted: {} refusals", refused.get());
assert!(lock.exists(), "the stale lock stays for the reclaimer that holds it");
let_go_tx.send(()).expect("the other reclaimer is told to let go");
reclaimer.join().expect("the other reclaimer ends");
remove(root.as_path());
}
#[test]
fn a_landing_takes_the_stale_lock_when_the_other_reclaimers_hold_is_dropped() {
let (root, index) = index_in("reclaim-landing-wakes");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, stale_lock_text(5)).expect("stale lock");
let ceiling: std::time::Duration = crate::service_index_watch::POLL_PAUSE * 20;
let (taken_tx, taken_rx) = std::sync::mpsc::channel::<()>();
let (first_tx, first_rx) = std::sync::mpsc::channel::<()>();
let reclaimer = spawn_other_reclaimer(lock.clone(), taken_tx, first_rx, ceiling * 4);
taken_rx.recv().expect("the hold is taken");
let refused = std::cell::Cell::new(0_usize);
let hold = counting_hold(&refused, &first_tx);
let platform: Platform = Platform { flock: &hold, ..SYSTEM };
let holders = std::cell::RefCell::new(Vec::<String>::new());
let began: std::time::Instant = std::time::Instant::now();
let taken = acquire_within(&platform, &index, 9, ceiling, &|announce| {
if let Announce::Holder(who) = announce {
holders.borrow_mut().push(who);
}
})
.expect("the stale lock is reclaimed in the same acquisition once the hold is dropped");
assert!(refused.get() >= 1, "the acquisition met the other reclaimer's hold");
assert!(began.elapsed() < ceiling, "the acquisition took the whole ceiling");
assert!(holders.borrow().is_empty(), "a held stale lock is no dead holder to give up on: {:?}", holders.borrow());
drop(taken);
reclaimer.join().expect("the other reclaimer ends");
remove(root.as_path());
}
#[test]
fn a_hold_reported_as_taken_by_another_reclaimer_is_a_back_off() {
let (root, index) = index_in("reclaim-platform-busy");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, stale_lock_text(5)).expect("stale lock");
let held = |_: &std::fs::File| -> std::io::Result<()> { return Err(std::io::Error::from(std::io::ErrorKind::WouldBlock)); };
let platform: Platform = Platform { flock: &held, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Busy);
assert!(lock.exists());
remove(root.as_path());
}
#[test]
fn a_failed_flock_never_has_the_stale_lock_removed() {
let (root, index) = index_in("reclaim-no-flock");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, stale_lock_text(5)).expect("stale lock");
let tried = std::cell::Cell::new(0_usize);
let refusing = |_: &std::fs::File| -> std::io::Result<()> {
tried.set(tried.get() + 1);
return Err(std::io::Error::from_raw_os_error(libc::ENOLCK));
};
let platform: Platform = Platform { flock: &refusing, ..SYSTEM };
let outcome: Reclaimed = reclaim(&platform, &lock);
assert!(matches!(outcome, Reclaimed::Unguarded(_)), "the stale lock is left, and the caller is told why: {outcome:?}");
assert_eq!(tried.get(), 1, "the hold was asked for once");
assert!(lock.exists(), "the stale lock stays");
remove(root.as_path());
}
#[test]
fn a_landing_with_no_flock_waits_to_its_ceiling_and_names_the_stale_lock() {
let (root, index) = index_in("reclaim-landing-no-flock");
let lock: PathBuf = lock_path_of(&index);
let text: String = stale_lock_text(5);
std::fs::write(&lock, &text).expect("stale lock");
let refusing = |_: &std::fs::File| -> std::io::Result<()> { return Err(std::io::Error::from_raw_os_error(libc::ENOLCK)); };
let platform: Platform = Platform { flock: &refusing, ..SYSTEM };
let ceiling: std::time::Duration = crate::service_index_watch::POLL_PAUSE * 4;
let said = std::cell::RefCell::new(Vec::<String>::new());
let began: std::time::Instant = std::time::Instant::now();
let failure = acquire_within(&platform, &index, 9, ceiling, &|announce| {
if let Announce::Waiting(line) = announce {
said.borrow_mut().push(line);
}
})
.expect_err("the lock is not taken");
assert_eq!(failure.code, FailureCode::IndexInUse);
assert!(began.elapsed() >= ceiling, "the wait ended before its ceiling");
assert_eq!(std::fs::read_to_string(&lock).expect("the stale lock stays"), text);
let lines: String = said.borrow().join("\n");
assert!(lines.contains(lock.to_string_lossy().as_ref()), "the notice names the lock: {lines}");
assert!(lines.contains("remove it"), "the notice says how to remove it: {lines}");
assert!(lines.contains("batch 5"), "the notice names the batch that left it: {lines}");
remove(root.as_path());
}
#[test]
fn a_reclaimer_that_finds_a_live_lock_leaves_no_event_on_the_lock_name() {
let (root, index) = index_in("reclaim-live-no-event");
let lock: PathBuf = lock_path_of(&index);
let live = try_acquire(&index, 7).expect("the live holder places its lock");
let watch: i32 = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
assert!(watch >= 0, "an inotify descriptor");
let directory = std::ffi::CString::new(std::os::unix::ffi::OsStrExt::as_bytes(root.as_os_str())).expect("a path");
let events: u32 = libc::IN_MOVED_FROM | libc::IN_MOVED_TO | libc::IN_DELETE | libc::IN_CREATE;
let added: i32 = unsafe { libc::inotify_add_watch(watch, directory.as_ptr(), events) };
assert!(added >= 0, "a watch on the directory");
let _outcome = reclaim(&SYSTEM, &lock);
let mut buffer: Vec<u8> = vec![0; 4096];
let read: isize = unsafe { libc::read(watch, buffer.as_mut_ptr().cast(), buffer.len()) };
unsafe { libc::close(watch) };
let read_error: std::io::Error = std::io::Error::last_os_error();
assert!(read >= 0 || read_error.kind() == std::io::ErrorKind::WouldBlock, "the events could not be read: {read_error}");
let seen: String = if read > 0 { String::from_utf8_lossy(&buffer[..read as usize]).into_owned() } else { String::new() };
assert!(!seen.contains("index.lock"), "the live lock's name saw events: {seen:?}");
assert_eq!(std::fs::read_to_string(&lock).expect("the live lock stands"), marker_text(7, &crate::service_index_marker::own_identity).expect("this process has an identity"));
drop(live);
remove(root.as_path());
}
#[test]
fn the_notice_names_the_judged_stale_lock_and_not_a_live_replacement() {
let (root, index) = index_in("reclaim-notice-replaced");
let lock: PathBuf = lock_path_of(&index);
let stale: String = stale_lock_text(5);
let stale_marked: Marked = parse_marker(stale.as_str()).expect("a marker");
let stale_pid: i64 = stale_marked.holder.as_ref().expect("a holder").0;
std::fs::write(&lock, &stale).expect("stale lock");
let live_cell: std::cell::RefCell<Option<IndexLock>> = std::cell::RefCell::new(None);
let replacing = |_: &std::fs::File| -> std::io::Result<()> {
if live_cell.borrow().is_none() {
std::fs::remove_file(&lock).expect("the other reclaimer removes the stale lock");
*live_cell.borrow_mut() = Some(try_acquire(&index, 8).expect("the live landing places its lock"));
}
return Err(std::io::Error::from_raw_os_error(libc::ENOLCK));
};
let platform: Platform = Platform { flock: &replacing, ..SYSTEM };
let said = std::cell::RefCell::new(Vec::<String>::new());
let ceiling: std::time::Duration = crate::service_index_watch::POLL_PAUSE * 4;
let _failure = acquire_within(&platform, &index, 9, ceiling, &|announce| {
if let Announce::Waiting(line) = announce {
said.borrow_mut().push(line);
}
})
.expect_err("the live lock is held to the ceiling");
let first: String = said.borrow().first().cloned().expect("a notice");
assert!(first.contains(&format!("process {stale_pid}")), "the notice names the judged stale lock's process: {first}");
assert!(!first.contains(&format!("process {}", std::process::id())), "the notice calls the live lock's process gone: {first}");
drop(live_cell.borrow_mut().take());
remove(root.as_path());
}