use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use dashmap::DashMap;
use crate::error::Result;
use crate::time::{Clock, Timeout, Timestamp};
#[async_trait]
pub trait DistributedLocker: Send + Sync {
async fn acquire(&self, key: &str, ttl: Duration, timeout: Timeout) -> Result<Option<String>>;
async fn release(&self, key: &str, token: &str) -> Result<()>;
}
pub struct InMemoryDistributedLocker {
locks: Arc<DashMap<String, Held>>,
clock: Arc<dyn Clock>,
}
#[derive(Clone)]
struct Held {
token: String,
expires_at: Timestamp,
}
impl InMemoryDistributedLocker {
#[must_use]
pub fn new(clock: Arc<dyn Clock>) -> Self {
Self {
locks: Arc::new(DashMap::new()),
clock,
}
}
fn try_once(&self, key: &str, ttl: Duration, now: Timestamp) -> Option<String> {
use dashmap::mapref::entry::Entry;
let expires_at = now.saturating_add(ttl);
match self.locks.entry(key.to_owned()) {
Entry::Occupied(mut occupied) => {
if now.is_before(occupied.get().expires_at) {
None } else {
let token = new_token();
occupied.insert(Held {
token: token.clone(),
expires_at,
});
Some(token)
}
}
Entry::Vacant(vacant) => {
let token = new_token();
vacant.insert(Held {
token: token.clone(),
expires_at,
});
Some(token)
}
}
}
}
fn new_token() -> String {
format!("{:016x}{:016x}", fastrand::u64(..), fastrand::u64(..))
}
#[async_trait]
impl DistributedLocker for InMemoryDistributedLocker {
async fn acquire(&self, key: &str, ttl: Duration, timeout: Timeout) -> Result<Option<String>> {
let deadline = match timeout {
Timeout::After(d) => Some(std::time::Instant::now() + d),
Timeout::Infinite => None,
};
loop {
if let Some(token) = self.try_once(key, ttl, self.clock.now()) {
return Ok(Some(token));
}
if let Some(deadline) = deadline
&& std::time::Instant::now() >= deadline
{
return Ok(None);
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
async fn release(&self, key: &str, token: &str) -> Result<()> {
self.locks.remove_if(key, |_, held| held.token == token);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::ManualClock;
#[tokio::test]
async fn second_acquire_blocks_until_release() {
let clock = Arc::new(ManualClock::default());
let locker = InMemoryDistributedLocker::new(clock);
let token = locker
.acquire("k", Duration::from_secs(30), Timeout::Infinite)
.await
.unwrap()
.expect("first acquire succeeds");
let second = locker
.acquire("k", Duration::from_secs(30), Timeout::After(Duration::ZERO))
.await
.unwrap();
assert!(second.is_none());
locker.release("k", &token).await.unwrap();
assert!(
locker
.acquire("k", Duration::from_secs(30), Timeout::After(Duration::ZERO))
.await
.unwrap()
.is_some()
);
}
}