use std::fs::{File, OpenOptions};
use std::path::Path;
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("ConcurrencyConflict: could not acquire exclusive lock on {path} within 2s")]
pub struct ConcurrencyConflictError {
pub path: String,
}
pub struct DirLock {
_file: File,
}
const RETRY_DELAYS: &[Duration] = &[
Duration::from_millis(50),
Duration::from_millis(100),
Duration::from_millis(250),
Duration::from_millis(500),
Duration::from_millis(1_000),
];
pub async fn acquire_dir_lock(mailbox_dir: &Path) -> anyhow::Result<DirLock> {
use fs2::FileExt;
let lock_path = mailbox_dir.join(".rusmes.lock");
tokio::fs::create_dir_all(mailbox_dir).await?;
let file = OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.read(true)
.open(&lock_path)
.map_err(|e| anyhow::anyhow!("Failed to open lockfile {}: {}", lock_path.display(), e))?;
for &delay in RETRY_DELAYS {
match file.try_lock_exclusive() {
Ok(()) => {
tracing::trace!("Acquired exclusive lock on {}", lock_path.display());
return Ok(DirLock { _file: file });
}
Err(_) => {
tracing::trace!(
"Lock contention on {}, retrying in {:?}",
lock_path.display(),
delay
);
tokio::time::sleep(delay).await;
}
}
}
match file.try_lock_exclusive() {
Ok(()) => Ok(DirLock { _file: file }),
Err(_) => Err(anyhow::anyhow!(ConcurrencyConflictError {
path: lock_path.display().to_string(),
})),
}
}
impl Drop for DirLock {
fn drop(&mut self) {
if let Err(e) = fs2::FileExt::unlock(&self._file) {
tracing::warn!("Failed to release directory lock: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_lock_acquire_and_release() {
let dir = tempfile::tempdir().expect("tempdir");
let lock = acquire_dir_lock(dir.path()).await;
assert!(lock.is_ok(), "Should acquire lock");
drop(lock.unwrap());
let lock2 = acquire_dir_lock(dir.path()).await;
assert!(lock2.is_ok(), "Should acquire lock after release");
}
#[tokio::test]
async fn test_lock_file_created() {
let dir = tempfile::tempdir().expect("tempdir");
let _lock = acquire_dir_lock(dir.path()).await.expect("lock");
assert!(dir.path().join(".rusmes.lock").exists());
}
}