use super::*;
use crate::service_fault::hooks;
use crate::service_index_claim::{CLAIM_INFIX, claim_key, claim_path};
use crate::service_index_marker::{LOCK_MARKER, marker_of_file, own_identity};
use crate::service_index_placement::rename_noreplace;
use crate::test_support::{fixture, remove};
pub(super) fn no_flock(_: &std::fs::File) -> std::io::Result<()> {
return Err(std::io::Error::from_raw_os_error(libc::ENOLCK));
}
pub(super) fn rename_unless_claim(from: &Path, to: &Path) -> std::io::Result<()> {
if to.to_string_lossy().contains(CLAIM_INFIX) {
return Err(std::io::Error::from_raw_os_error(libc::EINVAL));
}
return rename_noreplace(from, to);
}
pub(super) fn link_unless_claim(from: &Path, to: &Path) -> std::io::Result<()> {
if to.to_string_lossy().contains(CLAIM_INFIX) {
return Err(std::io::Error::from_raw_os_error(libc::EPERM));
}
return std::fs::hard_link(from, to);
}
fn gone_marker(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 live_marker() -> String {
return marker_text(0, &own_identity).expect("this process has an identity");
}
fn index_in(name: &str) -> (PathBuf, PathBuf, PathBuf) {
let root: PathBuf = fixture(name);
let index: PathBuf = root.join("index");
std::fs::write(&index, "old index").expect("index");
let lock: PathBuf = lock_path_of(&index);
std::fs::write(&lock, gone_marker(5)).expect("stale lock");
return (root, index, lock);
}
fn claim_at(lock: &Path, slot: usize) -> PathBuf {
let file = std::fs::File::open(lock).expect("the stale lock");
let marked = marker_of_file(&file).expect("a marker");
let key: String = claim_key(&file, &marked).expect("its identity");
return claim_path(lock, key.as_str(), slot);
}
fn names_in(root: &Path) -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(root).expect("listing").flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).collect();
names.sort();
return names;
}
#[test]
fn a_stale_lock_is_removed_where_flock_is_not_available() {
let (root, _index, lock) = index_in("claim-plain");
let platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Removed);
assert!(!lock.exists());
assert_eq!(names_in(&root), vec![String::from("index")], "no claim and no staging file is left");
remove(root.as_path());
}
#[test]
fn a_landing_takes_a_stale_lock_where_flock_is_not_available_without_waiting() {
let (root, index, _lock) = index_in("claim-landing");
let platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
let ceiling: std::time::Duration = crate::service_index_watch::POLL_PAUSE * 20;
let began: std::time::Instant = std::time::Instant::now();
let taken = acquire_within(&platform, &index, 9, ceiling, &|_| {}).expect("the stale lock is reclaimed and the lock is taken");
assert!(began.elapsed() < ceiling, "the acquisition waited for its ceiling");
drop(taken);
assert_eq!(names_in(&root), vec![String::from("index")], "no claim and no lock is left");
remove(root.as_path());
}
#[test]
fn a_second_reclaimer_without_flock_backs_off_while_the_first_holds() {
let (root, _index, lock) = index_in("claim-two-reclaimers");
let platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
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 || {
let other: Platform = Platform { flock: &no_flock, ..SYSTEM };
*hook_second.borrow_mut() = Some(reclaim(&other, &hook_lock));
}));
let first: Reclaimed = reclaim(&platform, &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_eq!(names_in(&root), vec![String::from("index")], "the claims of both are gone");
remove(root.as_path());
}
#[test]
fn a_dead_claimers_claims_are_passed_over_and_cleared() {
let (root, _index, lock) = index_in("claim-dead");
std::fs::write(claim_at(&lock, 0), gone_marker(0)).expect("a dead claimer's claim");
std::fs::write(claim_at(&lock, 1), gone_marker(0)).expect("another dead claimer's claim");
let platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Removed);
assert!(!lock.exists());
assert_eq!(names_in(&root), vec![String::from("index")], "the dead claims are cleared with the own one");
remove(root.as_path());
}
#[test]
fn a_claim_without_a_whole_marker_is_passed_over() {
let (root, _index, lock) = index_in("claim-torn");
std::fs::write(claim_at(&lock, 0), b"").expect("an empty claim");
std::fs::write(claim_at(&lock, 1), &gone_marker(0).as_bytes()[..10]).expect("a cut claim");
std::fs::write(claim_at(&lock, 2), [0xff_u8, 0xfe, 0x00]).expect("a claim that is not text");
let platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Removed);
assert_eq!(names_in(&root), vec![String::from("index")]);
remove(root.as_path());
}
#[test]
fn a_running_claimer_ahead_makes_the_reclaimer_back_off() {
let (root, _index, lock) = index_in("claim-live-ahead");
let ahead: PathBuf = claim_at(&lock, 0);
std::fs::write(&ahead, live_marker()).expect("a running claimer's claim");
let platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Busy);
assert!(lock.exists(), "the stale lock stays");
assert_eq!(std::fs::read_to_string(&ahead).expect("the claim stands"), live_marker());
assert_eq!(names_in(&root).len(), 3, "the index, the lock, the claim ahead: {:?}", names_in(&root));
remove(root.as_path());
}
#[test]
fn a_slot_below_that_turns_live_after_the_reclaimer_looked_makes_it_back_off() {
let (root, _index, lock) = index_in("claim-turned-live");
let below: PathBuf = claim_at(&lock, 0);
std::fs::write(&below, gone_marker(0)).expect("a dead claimer's claim");
let (hook_below, done) = (below.clone(), std::rc::Rc::new(std::cell::Cell::new(false)));
hooks::set("reclaim-claim-placed", std::rc::Rc::new(move || {
if !done.replace(true) {
std::fs::remove_file(&hook_below).expect("the dead claim is cleared");
std::fs::write(&hook_below, live_marker()).expect("the new reclaimer's claim");
}
}));
let platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Busy);
assert!(lock.exists(), "the stale lock stays");
assert_eq!(std::fs::read_to_string(&below).expect("the running claim stands"), live_marker());
assert_eq!(names_in(&root).len(), 3, "the index, the lock, the running claim: {:?}", names_in(&root));
remove(root.as_path());
}
#[test]
fn a_slot_below_that_vanishes_after_the_reclaimer_looked_makes_it_back_off() {
let (root, _index, lock) = index_in("claim-vanished");
let below: PathBuf = claim_at(&lock, 0);
std::fs::write(&below, gone_marker(0)).expect("a dead claimer's claim");
let (hook_below, done) = (below.clone(), std::rc::Rc::new(std::cell::Cell::new(false)));
hooks::set("reclaim-claim-placed", std::rc::Rc::new(move || {
if !done.replace(true) {
std::fs::remove_file(&hook_below).expect("the dead claim is cleared");
}
}));
let platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Busy);
assert!(lock.exists(), "the stale lock stays");
assert_eq!(names_in(&root), vec![String::from("index"), String::from("index.lock")], "the own claim is removed");
assert_eq!(reclaim(&platform, &lock), Reclaimed::Removed);
remove(root.as_path());
}
#[test]
fn a_replaced_name_without_flock_removes_nothing() {
let (root, index, lock) = index_in("claim-replaced");
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-held", 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 platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Changed);
assert_eq!(std::fs::read_to_string(&lock).expect("the live lock stands"), marker_text(8, &own_identity).expect("an identity"));
let live = live_cell.borrow_mut().take().expect("the live lock");
drop(live);
assert_eq!(names_in(&root), vec![String::from("index")], "the live lock was released and no claim is left");
remove(root.as_path());
}
#[test]
fn a_removal_that_fails_clears_only_the_own_claim() {
let (root, _index, lock) = index_in("claim-stuck");
let dead: PathBuf = claim_at(&lock, 0);
std::fs::write(&dead, gone_marker(0)).expect("a dead claimer's claim");
let refusing = |_: &Path| -> std::io::Result<()> { return Err(std::io::Error::from_raw_os_error(libc::EPERM)); };
let platform: Platform = Platform { flock: &no_flock, unlink: &refusing, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Stuck);
assert!(lock.exists());
assert!(dead.exists(), "a claim of another maker stays while the stale lock stands");
assert_eq!(names_in(&root).len(), 3, "the index, the lock, the dead claim: {:?}", names_in(&root));
remove(root.as_path());
}
#[test]
fn neither_flock_nor_claim_leaves_the_lock_and_says_so() {
let (root, _index, lock) = index_in("claim-none");
let platform: Platform = Platform { flock: &no_flock, rename: &rename_unless_claim, link: &link_unless_claim, ..SYSTEM };
let outcome: Reclaimed = reclaim(&platform, &lock);
assert!(matches!(outcome, Reclaimed::Unguarded(_)), "{outcome:?}");
assert!(lock.exists());
assert_eq!(names_in(&root), vec![String::from("index"), String::from("index.lock")], "no claim and no staging file is left");
remove(root.as_path());
}
#[test]
fn a_flock_that_fails_for_want_of_memory_is_a_back_off_not_a_fallback() {
let (root, _index, lock) = index_in("claim-enomem");
let transient = |_: &std::fs::File| -> std::io::Result<()> { return Err(std::io::Error::from_raw_os_error(libc::ENOMEM)); };
let platform: Platform = Platform { flock: &transient, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Busy);
assert_eq!(names_in(&root), vec![String::from("index"), String::from("index.lock")], "no claim was placed");
remove(root.as_path());
}
const NO_FLOCK_ERRNOS: [i32; 5] = [libc::ENOLCK, libc::EOPNOTSUPP, libc::ENOSYS, libc::EINVAL, libc::EBADF];
#[test]
fn every_refusal_that_means_no_flock_takes_the_claim_path() {
for errno in NO_FLOCK_ERRNOS {
let (root, _index, lock) = index_in("claim-errnos");
let refusing = move |_: &std::fs::File| -> std::io::Result<()> { return Err(std::io::Error::from_raw_os_error(errno)); };
let platform: Platform = Platform { flock: &refusing, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Removed, "errno {errno}");
remove(root.as_path());
}
}
#[test]
fn a_flock_that_fails_in_an_unrecognised_way_leaves_the_lock_and_says_so() {
let (root, _index, lock) = index_in("claim-eio");
let failing = |_: &std::fs::File| -> std::io::Result<()> { return Err(std::io::Error::from_raw_os_error(libc::EIO)); };
let platform: Platform = Platform { flock: &failing, ..SYSTEM };
let outcome: Reclaimed = reclaim(&platform, &lock);
assert!(matches!(outcome, Reclaimed::Unguarded(_)), "{outcome:?}");
assert_eq!(names_in(&root), vec![String::from("index"), String::from("index.lock")], "no claim was placed");
remove(root.as_path());
}
#[test]
fn a_placement_whose_answer_was_lost_is_still_this_attempts_claim() {
let (root, _index, lock) = index_in("claim-lost-answer");
let lost = |from: &Path, to: &Path| -> std::io::Result<()> {
rename_noreplace(from, to)?;
return Err(std::io::Error::from_raw_os_error(libc::EIO));
};
let platform: Platform = Platform { flock: &no_flock, rename: &lost, ..SYSTEM };
assert_eq!(reclaim(&platform, &lock), Reclaimed::Removed);
assert_eq!(names_in(&root), vec![String::from("index")], "no claim is left standing as a running claimer's");
remove(root.as_path());
}
#[test]
fn the_claim_name_comes_from_the_marker_and_not_from_the_files_device_and_inode() {
let root: PathBuf = fixture("claim-key");
let text: String = gone_marker(5);
let (first, second) = (root.join("first"), root.join("second"));
std::fs::write(&first, &text).expect("a lock");
std::fs::write(&second, &text).expect("the same lock seen by another mount");
let key_of = |path: &Path| -> String {
let file = std::fs::File::open(path).expect("the lock");
let marked = marker_of_file(&file).expect("a marker");
return claim_key(&file, &marked).expect("a key");
};
assert_eq!(key_of(&first), key_of(&second));
remove(root.as_path());
}
#[test]
fn a_claim_is_readable_by_other_accounts_whatever_the_makers_umask() {
let (root, _index, lock) = index_in("claim-mode");
let own: PathBuf = claim_at(&lock, 0);
let seen = std::rc::Rc::new(std::cell::Cell::new(0_u32));
let (hook_seen, hook_own) = (std::rc::Rc::clone(&seen), own.clone());
hooks::set("reclaim-claim-placed", std::rc::Rc::new(move || {
hook_seen.set(std::os::unix::fs::PermissionsExt::mode(&std::fs::metadata(&hook_own).expect("the claim").permissions()));
}));
let before: libc::mode_t = unsafe { libc::umask(0o077) };
let platform: Platform = Platform { flock: &no_flock, ..SYSTEM };
let outcome: Reclaimed = reclaim(&platform, &lock);
unsafe { libc::umask(before) };
assert_eq!(outcome, Reclaimed::Removed);
let readable: u32 = 0o444;
assert_eq!(seen.get() & readable, readable, "the claim's mode was {:o}", seen.get());
remove(root.as_path());
}