use super::*;
use std::fs;
use std::path::PathBuf;
#[cfg(unix)]
use std::sync::mpsc;
#[cfg(unix)]
use std::time::{Duration, Instant};
fn temp_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("quvyta-instances-{name}-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("test directory");
dir
}
#[cfg(unix)]
const PATIENCE: Duration = Duration::from_secs(5);
#[cfg(unix)]
const A_WHILE: Duration = Duration::from_millis(300);
#[cfg(unix)]
fn exclusive_soon(path: &Path) -> Option<InstanceLock> {
let deadline = Instant::now() + PATIENCE;
loop {
match InstanceLock::try_exclusive(path).expect("attempt") {
Some(lock) => return Some(lock),
None if Instant::now() >= deadline => return None,
None => std::thread::sleep(Duration::from_millis(5)),
}
}
}
#[cfg(unix)]
fn wait_on_a_thread(path: &Path) -> mpsc::Receiver<(io::Result<InstanceLock>, Duration)> {
let (send, receive) = mpsc::channel();
let path = path.to_path_buf();
std::thread::spawn(move || {
let busy = || {
let time = rustix::time::clock_gettime(rustix::time::ClockId::ThreadCPUTime);
Duration::new(u64::try_from(time.tv_sec).unwrap_or(0), u32::try_from(time.tv_nsec).unwrap_or(0))
};
let before = busy();
let lock = InstanceLock::wait_exclusive(&path);
let _ = send.send((lock, busy().saturating_sub(before)));
});
receive
}
#[cfg(unix)]
#[test]
fn many_shared_locks_are_held_at_once_and_keep_the_exclusive_one_out() {
let dir = temp_dir("shared");
let path = dir.join("instances");
let first = InstanceLock::shared(&path).expect("first instance");
let second = InstanceLock::shared(&path).expect("second instance, without waiting");
assert!(InstanceLock::try_exclusive(&path).expect("attempt").is_none(), "two instances are running");
drop(first);
assert!(InstanceLock::try_exclusive(&path).expect("attempt").is_none(), "one is still running");
drop(second);
let lock = exclusive_soon(&path).expect("with nobody running the lock is free");
drop(lock);
fs::remove_dir_all(&dir).expect("clean");
}
#[cfg(unix)]
#[test]
fn the_waiter_wakes_when_the_last_instance_closes_and_not_before() {
let dir = temp_dir("wait");
let path = dir.join("instances");
let first = InstanceLock::shared(&path).expect("first instance");
let second = InstanceLock::shared(&path).expect("second instance");
let waiter = wait_on_a_thread(&path);
assert!(waiter.recv_timeout(A_WHILE).is_err(), "two instances run, so it waits");
drop(first);
assert!(waiter.recv_timeout(A_WHILE).is_err(), "one instance still runs");
drop(second);
let (lock, busy) = waiter.recv_timeout(PATIENCE).expect("the last close woke it");
let lock = lock.expect("the exclusive lock");
assert!(busy < Duration::from_millis(100), "it slept in the kernel, busy for {busy:?} of about 600 ms");
let (send, receive) = mpsc::channel();
let starting = path.clone();
std::thread::spawn(move || {
let _ = send.send(InstanceLock::shared(&starting));
});
assert!(receive.recv_timeout(A_WHILE).is_err(), "an instance waits for the cleanup");
drop(lock);
receive.recv_timeout(PATIENCE).expect("the cleanup ended").expect("the instance's lock");
fs::remove_dir_all(&dir).expect("clean");
}
#[cfg(unix)]
#[test]
fn nobody_running_means_the_exclusive_lock_comes_at_once() {
let dir = temp_dir("nobody");
let path = dir.join("instances");
let waiter = wait_on_a_thread(&path);
let (lock, _) = waiter.recv_timeout(PATIENCE).expect("no wait");
lock.expect("the exclusive lock");
fs::remove_dir_all(&dir).expect("clean");
}
#[cfg(unix)]
const CHILD: &str = "QUVYTA_INSTANCE_LOCK_CHILD";
#[cfg(unix)]
#[test]
fn a_child_process_holds_a_shared_lock() {
let Some(path) = std::env::var_os(CHILD) else {
return;
};
let _lock = InstanceLock::shared(Path::new(&path)).expect("the child's lock");
println!("holding");
std::thread::sleep(Duration::from_secs(60));
}
#[cfg(unix)]
#[test]
fn an_instance_that_is_killed_releases_its_lock_and_wakes_the_waiter() {
use std::io::BufRead;
let dir = temp_dir("killed");
let path = dir.join("instances");
let mut child = std::process::Command::new(std::env::current_exe().expect("the test binary"))
.args(["--exact", "storage::instance_lock::tests::a_child_process_holds_a_shared_lock", "--nocapture"])
.env(CHILD, &path)
.stdout(std::process::Stdio::piped())
.spawn()
.expect("the child process");
let output = child.stdout.take().expect("the child's output");
let holding = std::io::BufReader::new(output).lines().map_while(Result::ok).any(|line| line == "holding");
assert!(holding, "the child took its lock");
let waiter = wait_on_a_thread(&path);
assert!(waiter.recv_timeout(A_WHILE).is_err(), "the child's instance runs");
child.kill().expect("kill the child");
child.wait().expect("the child ended");
let (lock, _) = waiter.recv_timeout(PATIENCE).expect("the kernel released the dead child's lock");
lock.expect("the exclusive lock");
fs::remove_dir_all(&dir).expect("clean");
}
#[test]
fn a_file_that_cannot_be_opened_is_an_error() {
let dir = temp_dir("missing");
let path = dir.join("absent").join("instances");
let expected = if cfg!(unix) { io::ErrorKind::NotFound } else { io::ErrorKind::Unsupported };
assert_eq!(InstanceLock::shared(&path).expect_err("no directory").kind(), expected);
assert_eq!(InstanceLock::try_exclusive(&path).expect_err("no directory").kind(), expected);
assert_eq!(InstanceLock::wait_exclusive(&path).expect_err("no directory").kind(), expected);
fs::remove_dir_all(&dir).expect("clean");
}