use std::fs::{File, OpenOptions};
use std::io;
use std::path::PathBuf;
use nix::errno::Errno;
use nix::fcntl::{Flock, FlockArg};
use crate::error::Error;
use crate::paths::Tree;
#[derive(Debug)]
pub struct Deploying {
_held: Flock<File>,
}
pub fn hold(tree: &Tree) -> Result<Deploying, Error> {
let path = tree.lock_file();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|source| Error::Io {
path: parent.to_owned(),
source,
})?;
}
let file = OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&path)
.map_err(|source| Error::Io {
path: path.clone(),
source,
})?;
match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
Ok(held) => Ok(Deploying { _held: held }),
Err((_, errno)) => Err(refusal(tree.sheep(), path, errno)),
}
}
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::*;
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 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));
}
}