use std::fs::{File, OpenOptions, Permissions};
use std::io;
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::PathBuf;
use nix::errno::Errno;
use nix::fcntl::{Flock, FlockArg};
use crate::error::Error;
use crate::paths::Tree;
const LOCK_MODE: u32 = 0o600;
#[derive(Debug)]
pub struct Deploying {
_held: Flock<File>,
}
pub fn hold(tree: &Tree) -> Result<Deploying, Error> {
let path = tree.lock_file();
let file = open_lock_file(&path)?;
match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
Ok(held) => Ok(Deploying { _held: held }),
Err((_, errno)) => Err(refusal(tree.sheep(), path, errno)),
}
}
#[derive(Debug)]
pub struct Recording {
_held: Flock<File>,
}
pub fn hold_record(tree: &Tree) -> Result<Recording, Error> {
let path = tree.record_lock();
let file = open_lock_file(&path)?;
match Flock::lock(file, FlockArg::LockExclusive) {
Ok(held) => Ok(Recording { _held: held }),
Err((_, errno)) => Err(Error::Io {
path,
source: io::Error::from_raw_os_error(errno as i32),
}),
}
}
fn open_lock_file(path: &std::path::Path) -> Result<File, Error> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(Error::at(parent))?;
}
let file = OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.mode(LOCK_MODE)
.custom_flags(crate::shared::o_nofollow())
.open(path)
.map_err(Error::at(path))?;
let _ = file.set_permissions(Permissions::from_mode(LOCK_MODE));
Ok(file)
}
fn refusal(sheep: &str, path: PathBuf, errno: Errno) -> Error {
match errno {
Errno::EWOULDBLOCK => Error::AlreadyDeploying {
sheep: sheep.to_owned(),
},
other => Error::Io {
path,
source: io::Error::from_raw_os_error(other as i32),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_record_lock_waits_for_the_holder_and_then_takes_over() {
let home = tempfile::tempdir().expect("tempdir");
let tree = Tree::for_sheep(home.path(), "web");
let held = hold_record(&tree).expect("first hold");
let (tx, rx) = std::sync::mpsc::channel();
let waiting = {
let tree = Tree::for_sheep(home.path(), "web");
std::thread::spawn(move || {
let second = hold_record(&tree).expect("second hold");
tx.send(()).expect("report");
drop(second);
})
};
assert!(
rx.recv_timeout(std::time::Duration::from_millis(300))
.is_err(),
"the second taker must wait while the first holds"
);
drop(held);
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("the second taker gets the lock once the first lets go");
waiting.join().expect("thread");
}
use crate::paths::Tree;
#[test]
fn a_second_hold_on_the_same_tree_is_refused() {
let home = tempfile::tempdir().expect("tempdir");
let tree = Tree::for_sheep(home.path(), "web");
let first = hold(&tree).expect("the first hold");
let err = hold(&tree).expect_err("the second must be refused");
assert!(
matches!(&err, Error::AlreadyDeploying { sheep } if sheep == "web"),
"must name the sheep: {err:?}"
);
drop(first);
}
#[test]
fn a_dropped_hold_frees_the_tree() {
let home = tempfile::tempdir().expect("tempdir");
let tree = Tree::for_sheep(home.path(), "web");
drop(hold(&tree).expect("the first hold"));
hold(&tree).expect("the tree must be free once the first is dropped");
}
#[test]
fn a_lock_failure_that_is_not_contention_says_what_it_was() {
let path = std::path::PathBuf::from("/x/deploy.lock");
let contended = refusal("web", path.clone(), Errno::EWOULDBLOCK);
assert!(
matches!(&contended, Error::AlreadyDeploying { sheep } if sheep == "web"),
"contention is the one case that claim is true for: {contended:?}"
);
for errno in [Errno::ENOLCK, Errno::EIO, Errno::EOPNOTSUPP] {
let err = refusal("web", path.clone(), errno);
assert!(
matches!(&err, Error::Io { path: named, .. } if named == &path),
"{errno:?} must name the lock file, not a rival process: {err:?}"
);
assert!(
!format!("{err}").contains("already running"),
"{errno:?} must not claim contention: {err}"
);
}
}
#[test]
fn the_lock_file_is_owner_only() {
let home = tempfile::tempdir().expect("tempdir");
let tree = Tree::for_sheep(home.path(), "web");
let mode = |path: &std::path::Path| {
std::fs::metadata(path)
.expect("lock file")
.permissions()
.mode()
& 0o777
};
drop(hold(&tree).expect("creates the lock file"));
assert_eq!(mode(&tree.lock_file()), LOCK_MODE, "as created");
std::fs::set_permissions(tree.lock_file(), Permissions::from_mode(0o644))
.expect("loosen, as an older version left it");
drop(hold(&tree).expect("reopens the lock file"));
assert_eq!(mode(&tree.lock_file()), LOCK_MODE, "tightened on reopen");
}
#[test]
fn two_sheep_do_not_contend() {
let home = tempfile::tempdir().expect("tempdir");
let web = hold(&Tree::for_sheep(home.path(), "web")).expect("web");
let worker = hold(&Tree::for_sheep(home.path(), "worker")).expect("worker");
drop((web, worker));
}
}