use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard};
#[derive(Debug, Default)]
pub struct RepoLocks {
map: Mutex<HashMap<String, Arc<AsyncMutex<()>>>>,
}
impl RepoLocks {
pub async fn lock(&self, owner: &str, repo: &str) -> OwnedMutexGuard<()> {
let key = format!("{owner}/{repo}");
let m = {
let mut map = self
.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if map.len() > 256 {
map.retain(|_, v| Arc::strong_count(v) > 1);
}
map.entry(key).or_default().clone()
};
m.lock_owned().await
}
}
#[derive(Debug, Default)]
pub struct ForgeLocks {
pub issues: RepoLocks,
pub pulls: RepoLocks,
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
#[tokio::test]
async fn same_key_serialises_and_other_keys_do_not() {
let locks = Arc::new(RepoLocks::default());
let counter = Arc::new(AtomicU32::new(0));
let g = locks.lock("a", "r").await;
let other = locks.lock("a", "s").await;
drop(other);
let (l2, c2) = (locks.clone(), counter.clone());
let waiter = tokio::spawn(async move {
let _g = l2.lock("a", "r").await;
c2.fetch_add(1, Ordering::SeqCst);
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert_eq!(counter.load(Ordering::SeqCst), 0, "still blocked");
drop(g);
waiter.await.unwrap();
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn read_modify_write_does_not_lose_updates() {
let locks = Arc::new(RepoLocks::default());
let value = Arc::new(tokio::sync::Mutex::new(0u32));
let mut tasks = Vec::new();
for _ in 0..50 {
let (l, v) = (locks.clone(), value.clone());
tasks.push(tokio::spawn(async move {
let _g = l.lock("o", "r").await;
let cur = *v.lock().await;
tokio::task::yield_now().await;
*v.lock().await = cur + 1;
}));
}
for t in tasks {
t.await.unwrap();
}
assert_eq!(*value.lock().await, 50);
}
}