use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use fs2::FileExt;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum LockError {
#[error("another socket-patch process is operating in this directory")]
Held,
#[error("failed to open lock file at {path:?}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
#[derive(Debug)]
#[must_use = "the lock is released when this guard is dropped"]
pub struct LockGuard {
_file: std::fs::File,
}
pub fn acquire(socket_dir: &Path, timeout: Duration) -> Result<LockGuard, LockError> {
let path = socket_dir.join("apply.lock");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(|source| LockError::Io {
path: path.clone(),
source,
})?;
let deadline = Instant::now() + timeout;
loop {
match file.try_lock_exclusive() {
Ok(()) => return Ok(LockGuard { _file: file }),
Err(_) => {
if Instant::now() >= deadline {
return Err(LockError::Held);
}
std::thread::sleep(Duration::from_millis(100));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_acquire_succeeds() {
let dir = tempfile::tempdir().unwrap();
let guard = acquire(dir.path(), Duration::ZERO).unwrap();
assert!(dir.path().join("apply.lock").is_file());
drop(guard);
}
#[test]
fn second_concurrent_acquire_is_held() {
let dir = tempfile::tempdir().unwrap();
let _first = acquire(dir.path(), Duration::ZERO).unwrap();
let err = acquire(dir.path(), Duration::ZERO).unwrap_err();
assert!(matches!(err, LockError::Held));
}
#[test]
fn drop_releases_lock() {
let dir = tempfile::tempdir().unwrap();
{
let _g = acquire(dir.path(), Duration::ZERO).unwrap();
} let again = acquire(dir.path(), Duration::ZERO);
assert!(again.is_ok());
}
#[test]
fn missing_socket_dir_surfaces_io() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("does-not-exist");
let err = acquire(&missing, Duration::ZERO).unwrap_err();
match err {
LockError::Io { source, .. } => {
assert_eq!(source.kind(), std::io::ErrorKind::NotFound);
}
_ => panic!("expected Io error, got {:?}", err),
}
}
#[test]
fn timeout_held() {
let dir = tempfile::tempdir().unwrap();
let _first = acquire(dir.path(), Duration::ZERO).unwrap();
let start = Instant::now();
let err = acquire(dir.path(), Duration::from_millis(250)).unwrap_err();
let elapsed = start.elapsed();
assert!(matches!(err, LockError::Held));
assert!(
elapsed >= Duration::from_millis(200),
"expected at least 200ms wait, got {:?}",
elapsed
);
}
}