use std::hash::{Hash, Hasher};
use std::sync::Arc;
use tokio::sync::{Mutex, OwnedMutexGuard};
const STRIPES: usize = 64;
pub(crate) struct KeyLocks {
stripes: Vec<Arc<Mutex<()>>>,
}
#[must_use = "the lock is released when the guard is dropped"]
pub(crate) struct KeyGuard {
_held: Vec<OwnedMutexGuard<()>>,
}
impl Default for KeyLocks {
fn default() -> Self {
Self {
stripes: (0..STRIPES).map(|_| Arc::new(Mutex::new(()))).collect(),
}
}
}
impl KeyLocks {
fn stripe(keyspace: &str, key: &[u8]) -> usize {
let mut h = std::collections::hash_map::DefaultHasher::new();
keyspace.hash(&mut h);
key.hash(&mut h);
(h.finish() as usize) % STRIPES
}
pub(crate) async fn lock(&self, keyspace: &str, keys: &[&[u8]]) -> KeyGuard {
let mut idx: Vec<usize> = keys.iter().map(|k| Self::stripe(keyspace, k)).collect();
idx.sort_unstable();
idx.dedup();
let mut held = Vec::with_capacity(idx.len());
for i in idx {
held.push(Arc::clone(&self.stripes[i]).lock_owned().await);
}
KeyGuard { _held: held }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn same_key_excludes() {
let locks = Arc::new(KeyLocks::default());
let g = locks.lock("ks", &[b"k"]).await;
let l2 = locks.clone();
let waiter = tokio::spawn(async move {
let _g = l2.lock("ks", &[b"k"]).await;
});
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(
!waiter.is_finished(),
"second lock on the same key must wait"
);
drop(g);
tokio::time::timeout(Duration::from_secs(1), waiter)
.await
.expect("released")
.unwrap();
}
#[tokio::test]
async fn two_keys_on_one_stripe_do_not_self_deadlock() {
let locks = KeyLocks::default();
let _g = tokio::time::timeout(Duration::from_secs(1), locks.lock("ks", &[b"a", b"a"]))
.await
.expect("an operation never waits for itself");
}
#[tokio::test]
async fn crossed_two_key_operations_do_not_deadlock() {
let locks = Arc::new(KeyLocks::default());
let mut tasks = Vec::new();
for i in 0..200u32 {
let l = locks.clone();
tasks.push(tokio::spawn(async move {
let (x, y) = if i % 2 == 0 {
(b"a", b"b")
} else {
(b"b", b"a")
};
let _g = l.lock("ks", &[x, y]).await;
tokio::task::yield_now().await;
}));
}
tokio::time::timeout(Duration::from_secs(5), async {
for t in tasks {
t.await.unwrap();
}
})
.await
.expect("no deadlock");
}
}