use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex, Weak};
use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard};
type LockRegistry = Mutex<HashMap<PathBuf, Weak<AsyncMutex<()>>>>;
static REGISTRY: LazyLock<LockRegistry> = LazyLock::new(|| Mutex::new(HashMap::new()));
fn lock_for(key: &Path) -> Arc<AsyncMutex<()>> {
let mut map = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
if let Some(existing) = map.get(key).and_then(Weak::upgrade) {
return existing;
}
let fresh = Arc::new(AsyncMutex::new(()));
map.retain(|_, w| w.strong_count() > 0);
map.insert(key.to_path_buf(), Arc::downgrade(&fresh));
fresh
}
pub(crate) async fn lock_path(canonical: &Path) -> OwnedMutexGuard<()> {
lock_for(canonical).lock_owned().await
}
pub(crate) async fn lock_paths(sorted_unique: &[PathBuf]) -> Vec<OwnedMutexGuard<()>> {
let mut guards = Vec::with_capacity(sorted_unique.len());
for path in sorted_unique {
guards.push(lock_for(path).lock_owned().await);
}
guards
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
#[tokio::test]
async fn same_path_serializes_no_lost_update() {
let key = PathBuf::from("/lock-test/same");
let counter = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let key = key.clone();
let counter = Arc::clone(&counter);
handles.push(tokio::spawn(async move {
let _g = lock_path(&key).await;
let v = counter.load(Ordering::Relaxed);
tokio::task::yield_now().await;
counter.store(v + 1, Ordering::Relaxed);
}));
}
for h in handles {
h.await.unwrap();
}
assert_eq!(counter.load(Ordering::Relaxed), 8, "no updates may be lost");
}
#[tokio::test]
async fn different_paths_do_not_serialize() {
let res = tokio::time::timeout(Duration::from_secs(5), async {
let _a = lock_path(Path::new("/lock-test/a")).await;
let _b = lock_path(Path::new("/lock-test/b")).await;
})
.await;
assert!(res.is_ok(), "distinct paths must not serialize/deadlock");
}
#[tokio::test]
async fn registry_prunes_dropped_locks() {
let key = PathBuf::from("/lock-test/prune/me");
{
let _g = lock_path(&key).await;
assert!(REGISTRY.lock().unwrap().contains_key(&key));
} let _other = lock_path(Path::new("/lock-test/prune/other")).await;
assert!(
!REGISTRY.lock().unwrap().contains_key(&key),
"a dropped lock's entry must be pruned"
);
}
}