use std::sync::Arc;
use std::time::Duration;
use rainier_support::{Error, Result};
use crate::cache::Cache;
const POLL_INTERVAL: Duration = Duration::from_millis(100);
pub struct LockManager {
cache: Arc<dyn Cache>,
prefix: String,
declared_shared: bool,
}
impl LockManager {
pub fn new(cache: Arc<dyn Cache>) -> Self {
Self { cache, prefix: "lock:".to_string(), declared_shared: false }
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
pub fn lock(&self, name: impl Into<String>, ttl: Duration) -> Lock {
Lock {
cache: Arc::clone(&self.cache),
key: format!("{}{}", self.prefix, name.into()),
ttl,
wait: None,
}
}
pub fn is_shared(&self) -> bool {
self.declared_shared || (self.cache.is_shared() && self.cache.supports_atomic_add())
}
#[must_use = "this returns a configured manager rather than configuring in place"]
pub fn declared_shared(mut self) -> Self {
self.declared_shared = true;
self
}
pub fn cache(&self) -> &Arc<dyn Cache> {
&self.cache
}
}
impl std::fmt::Debug for LockManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LockManager")
.field("cache", &self.cache.name())
.field("prefix", &self.prefix)
.finish()
}
}
pub struct Lock {
cache: Arc<dyn Cache>,
key: String,
ttl: Duration,
wait: Option<Duration>,
}
impl Lock {
pub fn wait_for(mut self, wait: Duration) -> Self {
self.wait = Some(wait);
self
}
pub async fn acquire(&self) -> Result<Option<LockGuard>> {
let deadline = self.wait.map(|wait| std::time::Instant::now() + wait);
loop {
let owner = mint_owner();
if self.cache.add(&self.key, owner.as_bytes(), Some(self.ttl)).await? {
return Ok(Some(LockGuard {
cache: Arc::clone(&self.cache),
key: self.key.clone(),
owner,
released: false,
}));
}
match deadline {
Some(deadline) if std::time::Instant::now() + POLL_INTERVAL < deadline => {
tokio::time::sleep(POLL_INTERVAL).await;
}
_ => return Ok(None),
}
}
}
pub async fn run<T, F>(&self, work: F) -> Result<Option<T>>
where
F: std::future::Future<Output = Result<T>>,
{
let Some(guard) = self.acquire().await? else {
return Ok(None);
};
let outcome = work.await;
guard.release().await?;
outcome.map(Some)
}
pub async fn force_release(&self) -> Result<bool> {
self.cache.forget(&self.key).await
}
pub async fn is_held(&self) -> Result<bool> {
self.cache.has(&self.key).await
}
pub fn key(&self) -> &str {
&self.key
}
}
impl std::fmt::Debug for Lock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Lock").field("key", &self.key).field("ttl", &self.ttl).finish()
}
}
#[must_use = "a dropped guard holds its lock until the TTL expires; call `release`"]
pub struct LockGuard {
cache: Arc<dyn Cache>,
key: String,
owner: String,
released: bool,
}
impl LockGuard {
pub async fn release(mut self) -> Result<bool> {
self.released = true;
self.cache.forget_if(&self.key, self.owner.as_bytes()).await
}
pub async fn extend(&self, ttl: Duration) -> Result<bool> {
match self.cache.get(&self.key).await? {
Some(current) if current == self.owner.as_bytes() => {
self.cache.put(&self.key, self.owner.as_bytes(), Some(ttl)).await?;
Ok(true)
}
_ => Ok(false),
}
}
pub fn keep(mut self) {
self.released = true;
}
pub async fn is_still_held(&self) -> Result<bool> {
Ok(self.cache.get(&self.key).await?.as_deref() == Some(self.owner.as_bytes()))
}
pub fn owner(&self) -> &str {
&self.owner
}
pub fn key(&self) -> &str {
&self.key
}
}
impl Drop for LockGuard {
fn drop(&mut self) {
if !self.released {
tracing::debug!(
key = %self.key,
"a lock guard was dropped without being released; it will expire on its TTL"
);
}
}
}
impl std::fmt::Debug for LockGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LockGuard").field("key", &self.key).finish()
}
}
fn mint_owner() -> String {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
let mut hasher = RandomState::new().build_hasher();
hasher.write_u128(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or_default(),
);
hasher.write_usize(&hasher as *const _ as usize);
format!("{:016x}{:016x}", hasher.finish(), std::process::id())
}
pub fn contended(name: &str) -> Error {
Error::internal(format!("`{name}` is already running elsewhere"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemoryCache;
fn locks() -> LockManager {
LockManager::new(Arc::new(MemoryCache::new()))
}
#[tokio::test]
async fn exactly_one_caller_gets_the_lock() {
let locks = locks();
let first = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap();
let second = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap();
assert!(first.is_some());
assert!(second.is_none());
}
#[tokio::test]
async fn releasing_lets_the_next_caller_in() {
let locks = locks();
let guard = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
assert!(guard.release().await.unwrap());
assert!(locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().is_some());
}
#[tokio::test]
async fn a_release_after_the_lock_moved_on_does_not_take_it_from_the_new_holder() {
let locks = locks();
let cache = Arc::clone(locks.cache());
let stale = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
cache.forget("lock:job").await.unwrap();
let fresh = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
assert!(!stale.release().await.unwrap(), "the stale release should be a no-op");
assert!(fresh.is_still_held().await.unwrap(), "the new holder should still hold it");
assert!(locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().is_none());
}
#[tokio::test]
async fn two_holders_get_different_tokens() {
let locks = locks();
let first = locks.lock("a", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
let second = locks.lock("b", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
assert_ne!(first.owner(), second.owner());
}
#[tokio::test]
async fn an_expired_lock_is_available_again() {
let locks = locks();
let _held = locks.lock("job", Duration::from_millis(30)).acquire().await.unwrap().unwrap();
assert!(locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().is_none());
tokio::time::sleep(Duration::from_millis(60)).await;
assert!(
locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().is_some(),
"the TTL is what stops a dead holder locking a task out forever"
);
}
#[tokio::test]
async fn run_releases_on_success() {
let locks = locks();
let lock = locks.lock("job", Duration::from_secs(60));
assert_eq!(lock.run(async { Ok(7) }).await.unwrap(), Some(7));
assert!(!lock.is_held().await.unwrap());
}
#[tokio::test]
async fn run_releases_on_failure_too() {
let locks = locks();
let lock = locks.lock("job", Duration::from_secs(3600));
let outcome: Result<Option<()>> = lock.run(async { Err(Error::internal("boom")) }).await;
assert!(outcome.is_err());
assert!(!lock.is_held().await.unwrap(), "the lock should have been released");
}
#[tokio::test]
async fn run_skips_the_work_when_somebody_else_holds_it() {
use std::sync::atomic::{AtomicBool, Ordering};
let locks = locks();
let _held = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
let ran = AtomicBool::new(false);
let outcome = locks
.lock("job", Duration::from_secs(60))
.run(async {
ran.store(true, Ordering::SeqCst);
Ok(())
})
.await
.unwrap();
assert!(outcome.is_none());
assert!(!ran.load(Ordering::SeqCst), "the work must not run");
}
#[tokio::test]
async fn waiting_gets_the_lock_once_it_is_released() {
let locks = locks();
let guard = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
let cache = Arc::clone(locks.cache());
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
guard.release().await.unwrap();
drop(cache);
});
let waited = locks
.lock("job", Duration::from_secs(60))
.wait_for(Duration::from_secs(2))
.acquire()
.await
.unwrap();
assert!(waited.is_some(), "waiting should have outlasted the holder");
}
#[tokio::test]
async fn waiting_gives_up_at_the_deadline() {
let locks = locks();
let _held = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
let started = std::time::Instant::now();
let waited = locks
.lock("job", Duration::from_secs(60))
.wait_for(Duration::from_millis(250))
.acquire()
.await
.unwrap();
assert!(waited.is_none());
assert!(started.elapsed() < Duration::from_secs(2), "it should not wait forever");
}
#[tokio::test]
async fn extending_keeps_a_long_job_from_losing_its_lock() {
let locks = locks();
let guard = locks.lock("job", Duration::from_millis(50)).acquire().await.unwrap().unwrap();
assert!(guard.extend(Duration::from_secs(60)).await.unwrap());
tokio::time::sleep(Duration::from_millis(80)).await;
assert!(
locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().is_none(),
"the original TTL would have expired by now"
);
}
#[tokio::test]
async fn extending_a_lock_that_moved_on_says_so() {
let locks = locks();
let guard = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
locks.lock("job", Duration::from_secs(60)).force_release().await.unwrap();
let _other = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
assert!(!guard.extend(Duration::from_secs(60)).await.unwrap());
assert!(!guard.is_still_held().await.unwrap());
}
#[tokio::test]
async fn keep_leaves_the_lock_standing_until_the_ttl() {
let locks = locks();
locks.lock("minute", Duration::from_millis(60)).acquire().await.unwrap().unwrap().keep();
assert!(
locks.lock("minute", Duration::from_secs(60)).acquire().await.unwrap().is_none(),
"the claim should still stand"
);
tokio::time::sleep(Duration::from_millis(90)).await;
assert!(
locks.lock("minute", Duration::from_secs(60)).acquire().await.unwrap().is_some(),
"and expire on its own"
);
}
#[tokio::test]
async fn a_memory_cache_is_not_a_shared_lock_and_admits_it() {
assert!(!locks().is_shared());
}
#[tokio::test]
async fn different_names_do_not_contend() {
let locks = locks();
let a = locks.lock("a", Duration::from_secs(60)).acquire().await.unwrap();
let b = locks.lock("b", Duration::from_secs(60)).acquire().await.unwrap();
assert!(a.is_some() && b.is_some());
}
#[tokio::test]
async fn the_prefix_keeps_locks_out_of_the_caches_own_namespace() {
let locks = locks();
let guard = locks.lock("job", Duration::from_secs(60)).acquire().await.unwrap().unwrap();
assert_eq!(guard.key(), "lock:job");
assert!(Arc::clone(locks.cache()).has("lock:job").await.unwrap());
assert!(!Arc::clone(locks.cache()).has("job").await.unwrap());
}
}