use std::collections::BTreeMap;
use std::fs::{File, TryLockError};
use std::path::Path;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use tokio::sync::OwnedMutexGuard;
use super::fs;
use crate::Result;
use crate::error::Error;
use crate::name::RunName;
const GRACE: Duration = Duration::from_secs(1);
const RETRY: Duration = Duration::from_millis(50);
#[derive(Debug, Default)]
pub(crate) struct RunLocks(Mutex<BTreeMap<RunName, Arc<tokio::sync::Mutex<()>>>>);
#[derive(Debug)]
pub(crate) struct RunLock {
_file: File,
_here: OwnedMutexGuard<()>,
}
impl RunLocks {
pub(crate) async fn acquire(&self, run: &RunName, path: &Path) -> Result<RunLock> {
let mutex = Arc::clone(self.0.lock().unwrap_or_else(PoisonError::into_inner).entry(run.clone()).or_default());
let here = mutex.lock_owned().await;
let file = fs::lock_file(path).map_err(Error::io(path, "locked"))?;
let deadline = tokio::time::Instant::now() + GRACE;
loop {
match file.try_lock() {
Ok(()) => {
return Ok(RunLock { _file: file, _here: here });
}
Err(TryLockError::WouldBlock) if tokio::time::Instant::now() < deadline => {
tokio::time::sleep(RETRY).await;
}
Err(TryLockError::WouldBlock) => {
return Err(Error::Locked { run: run.to_string() });
}
Err(TryLockError::Error(source)) => {
return Err(Error::Io {
path: path.to_path_buf(),
action: "locked",
source,
});
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run() -> RunName {
RunName::new("lab").unwrap()
}
#[tokio::test]
async fn another_process_holding_the_run_keeps_it_busy() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lock");
let other = File::create(&path).unwrap();
other.lock().unwrap();
let locks = RunLocks::default();
assert!(matches!(locks.acquire(&run(), &path).await, Err(Error::Locked { .. })));
drop(other);
let held = locks.acquire(&run(), &path).await.unwrap();
drop(held);
}
#[tokio::test]
async fn a_lock_that_cant_be_taken_fails_closed() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lock");
std::fs::create_dir_all(&path).unwrap();
let err = RunLocks::default().acquire(&run(), &path).await.unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::Io, "{err}");
let missing = dir.path().join("gone").join("lock");
assert!(RunLocks::default().acquire(&run(), &missing).await.is_err());
}
#[tokio::test(start_paused = true)]
async fn a_second_operation_of_this_runner_waits_for_the_first() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lock");
let locks = Arc::new(RunLocks::default());
let first = locks.acquire(&run(), &path).await.unwrap();
let waiting = tokio::spawn({
let (locks, path) = (Arc::clone(&locks), path.clone());
async move { locks.acquire(&run(), &path).await.map(drop) }
});
tokio::time::sleep(Duration::from_secs(5)).await;
assert!(!waiting.is_finished(), "waits while the first holds the run");
drop(first);
waiting.await.unwrap().unwrap();
}
}