use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use super::Storage;
use crate::error::{Error, Result, StorageError};
use async_trait::async_trait;
use tokio_util::sync::CancellationToken;
#[async_trait]
pub trait Locker: Send + Sync + Debug {
async fn lock(&self, ct: &CancellationToken, name: &str) -> Result<LockGuard>;
async fn unlock(&self, name: &str) -> Result<()>;
async fn try_lock(&self, _ct: &CancellationToken, _name: &str) -> Result<Option<LockGuard>> {
Err(Error::Storage(StorageError::Other(
"try_lock not supported by this storage".into(),
)))
}
async fn lock_with_timeout(&self, name: &str, timeout: Duration) -> Result<LockGuard> {
let ct = CancellationToken::new();
match tokio::time::timeout(timeout, self.lock(&ct, name)).await {
Ok(result) => result,
Err(_) => Err(Error::Storage(StorageError::LockUnavailable(
name.to_owned(),
))),
}
}
async fn try_lock_with_timeout(
&self,
name: &str,
timeout: Duration,
) -> Result<Option<LockGuard>> {
const RETRY_INTERVAL: Duration = Duration::from_millis(25);
let deadline = tokio::time::Instant::now()
.checked_add(timeout)
.ok_or_else(|| Error::Storage(StorageError::Other("lock timeout overflow".into())))?;
let ct = CancellationToken::new();
loop {
match tokio::time::timeout_at(deadline, self.try_lock(&ct, name)).await {
Ok(Ok(Some(guard))) => return Ok(Some(guard)),
Ok(Ok(None)) => {}
Ok(Err(error)) => return Err(error),
Err(_) => return Ok(None),
}
let now = tokio::time::Instant::now();
if now >= deadline {
return Ok(None);
}
let delay = (deadline - now).min(RETRY_INTERVAL);
tokio::time::sleep(delay).await;
}
}
async fn acquire(&self, name: &str, timeout: Duration) -> Result<LockGuard> {
self.lock_with_timeout(name, timeout).await
}
async fn try_acquire(&self, name: &str, timeout: Duration) -> Result<Option<LockGuard>> {
self.try_lock_with_timeout(name, timeout).await
}
async fn renew_lock_lease(&self, _name: &str, _lease: Duration) -> Result<()> {
Err(Error::Storage(StorageError::Other(
"lock lease renewal not supported by this storage".into(),
)))
}
}
pub struct LockGuard {
key: String,
release: Arc<ReleaseState>,
id: Option<u64>,
}
pub type LockHandle = LockGuard;
pub trait LockRelease: Send + Sync {
fn write_fence(&self) -> Option<&(dyn std::any::Any + Send + Sync)> {
None
}
fn release(&self);
fn is_valid(&self) -> bool {
true
}
fn release_async(&self) -> futures::future::BoxFuture<'_, Result<()>> {
Box::pin(async move {
self.release();
Ok(())
})
}
}
const ACTIVE: u8 = 0;
const RELEASING: u8 = 1;
const FALLBACK_REQUESTED: u8 = 2;
const ACKNOWLEDGED: u8 = 3;
struct ReleaseState {
callback: Box<dyn LockRelease>,
phase: AtomicU8,
waiter: tokio::sync::Mutex<()>,
}
impl ReleaseState {
fn request(&self) {
if self
.phase
.try_update(Ordering::AcqRel, Ordering::Acquire, |phase| {
(phase < FALLBACK_REQUESTED).then_some(FALLBACK_REQUESTED)
})
.is_ok()
{
self.callback.release();
}
}
async fn wait(&self) -> Result<()> {
let _ = self
.phase
.compare_exchange(ACTIVE, RELEASING, Ordering::AcqRel, Ordering::Acquire);
let _waiter = self.waiter.lock().await;
if self.phase.load(Ordering::Acquire) != ACKNOWLEDGED {
self.callback.release_async().await?;
self.phase.store(ACKNOWLEDGED, Ordering::Release);
}
Ok(())
}
}
impl LockGuard {
#[must_use]
pub fn new(key: impl Into<String>, release: Box<dyn LockRelease>) -> Self {
Self {
key: key.into(),
release: Arc::new(ReleaseState {
callback: release,
phase: AtomicU8::new(ACTIVE),
waiter: tokio::sync::Mutex::new(()),
}),
id: None,
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.release.phase.load(Ordering::Acquire) == ACTIVE && self.release.callback.is_valid()
}
pub fn write_fence(&self) -> Option<&(dyn std::any::Any + Send + Sync)> {
self.release.callback.write_fence()
}
pub fn ensure_valid(&self) -> Result<()> {
if self.is_valid() {
Ok(())
} else {
Err(Error::Storage(StorageError::StaleLock(self.key.clone())).no_retry())
}
}
pub fn check_unfenced_write(&self) -> Result<()> {
self.ensure_valid()?;
if self.write_fence().is_some() {
return Err(Error::Storage(StorageError::UnsupportedFencing).no_retry());
}
Ok(())
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
#[must_use]
pub fn name(&self) -> &str {
self.key()
}
}
impl Drop for LockGuard {
fn drop(&mut self) {
self.release.request();
if let Some(id) = self.id {
let removed = registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.scoped
.remove(&id);
drop(removed);
}
}
}
impl LockGuard {
pub async fn release_and_wait(self) -> Result<()> {
self.release.wait().await
}
pub fn release(self) {
}
}
impl Debug for LockGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LockGuard").field("key", &self.key).finish()
}
}
static NEXT_LOCK_ID: AtomicU64 = AtomicU64::new(1);
struct OwnedGuard {
key: String,
release: Arc<ReleaseState>,
release_on_drop: bool,
}
impl Drop for OwnedGuard {
fn drop(&mut self) {
if self.release_on_drop {
self.release.request();
}
}
}
#[derive(Default)]
struct LockRegistry {
scoped: HashMap<u64, OwnedGuard>,
manual: HashMap<String, Arc<dyn Storage>>,
}
fn registry() -> &'static Mutex<LockRegistry> {
static REGISTRY: OnceLock<Mutex<LockRegistry>> = OnceLock::new();
REGISTRY.get_or_init(Mutex::default)
}
fn track_guard(guard: &mut LockGuard) {
let id = NEXT_LOCK_ID.fetch_add(1, Ordering::Relaxed);
registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.scoped
.insert(
id,
OwnedGuard {
key: guard.key.clone(),
release: Arc::clone(&guard.release),
release_on_drop: true,
},
);
guard.id = Some(id);
}
pub fn track_lock(storage: &Arc<dyn Storage>, key: &str) {
let old = registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.manual
.insert(key.to_owned(), Arc::clone(storage));
drop(old);
}
pub fn untrack_lock(key: &str) -> bool {
let mut locks = registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let manual = locks.manual.remove(key);
let mut removed = manual.is_some();
locks.scoped.retain(|_, guard| {
if guard.key == key {
guard.release_on_drop = false;
removed = true;
false
} else {
true
}
});
drop(locks);
drop(manual);
removed
}
pub async fn acquire_lock(
ct: &CancellationToken,
storage: &Arc<dyn Storage>,
key: &str,
) -> Result<LockGuard> {
let mut guard = storage.lock(ct, key).await?;
track_guard(&mut guard);
Ok(guard)
}
pub async fn try_acquire_lock(
ct: &CancellationToken,
storage: &Arc<dyn Storage>,
key: &str,
) -> Result<Option<LockGuard>> {
if let Some(mut guard) = storage.try_lock(ct, key).await? {
track_guard(&mut guard);
return Ok(Some(guard));
}
Ok(None)
}
pub async fn acquire(storage: Arc<dyn Storage>, key: &str) -> Result<LockGuard> {
let ct = CancellationToken::new();
acquire_lock(&ct, &storage, key).await
}
pub async fn try_acquire(
storage: Arc<dyn Storage>,
key: &str,
timeout: Duration,
) -> Result<Option<LockGuard>> {
let result = storage.try_lock_with_timeout(key, timeout).await?;
if let Some(mut guard) = result {
track_guard(&mut guard);
Ok(Some(guard))
} else {
Ok(None)
}
}
pub async fn acquire_with_timeout(
storage: Arc<dyn Storage>,
key: &str,
timeout: Duration,
) -> Result<LockGuard> {
let mut guard = storage.lock_with_timeout(key, timeout).await?;
track_guard(&mut guard);
Ok(guard)
}
pub async fn release_lock(guard: LockGuard) {
guard.release();
}
pub async fn clean_up_own_locks() {
let guards = {
let mut locks = registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
locks.scoped.drain().map(|(_, guard)| guard).collect()
};
release_owned_guards(guards).await;
let entries: Vec<_> = registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.manual
.drain()
.collect();
for (key, storage) in entries {
if let Err(err) = storage.unlock(&key).await {
tracing::warn!(key = %key, error = %err, "failed to clean up lock");
}
}
}
async fn release_owned_guards(guards: Vec<OwnedGuard>) {
for guard in guards {
if let Err(error) = guard.release.wait().await {
tracing::warn!(key = %guard.key, %error, "failed to clean up owned acquisition");
guard.release.request();
}
}
}
pub async fn cleanup_own_locks() {
clean_up_own_locks().await;
}
#[cfg(test)]
mod guard_release_tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(feature = "file-storage")]
#[tokio::test]
async fn dropping_an_acquired_guard_untracks_it_before_reacquire() {
let directory = tempfile::tempdir().unwrap();
let storage: Arc<dyn Storage> = crate::storage::FileStorage::new(directory.path());
let key = "raii-untrack-test";
let first = acquire(Arc::clone(&storage), key).await.unwrap();
drop(first);
assert!(
!registry()
.lock()
.unwrap()
.scoped
.values()
.any(|guard| guard.key == key)
);
let second = acquire(Arc::clone(&storage), key).await.unwrap();
assert!(
registry()
.lock()
.unwrap()
.scoped
.values()
.any(|guard| guard.key == key)
);
drop(second);
}
struct PendingRelease(Arc<AtomicUsize>);
impl LockRelease for PendingRelease {
fn release(&self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
fn release_async(&self) -> futures::future::BoxFuture<'_, Result<()>> {
Box::pin(std::future::pending())
}
}
#[tokio::test]
async fn a_background_release_request_does_not_acknowledge_an_explicit_waiter() {
let calls = Arc::new(AtomicUsize::new(0));
let guard = LockGuard::new("queued", Box::new(PendingRelease(calls.clone())));
guard.release.request();
assert!(!guard.is_valid());
assert_eq!(calls.load(Ordering::SeqCst), 1);
let mut acknowledged = Box::pin(guard.release_and_wait());
assert!(futures::poll!(&mut acknowledged).is_pending());
drop(acknowledged);
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn cancelling_shutdown_cleanup_requests_release_for_every_drained_guard() {
let calls = Arc::new(AtomicUsize::new(0));
let first = LockGuard::new("first", Box::new(PendingRelease(calls.clone())));
let second = LockGuard::new("second", Box::new(PendingRelease(calls.clone())));
let entries = [&first, &second]
.into_iter()
.map(|guard| OwnedGuard {
key: guard.key.clone(),
release: guard.release.clone(),
release_on_drop: true,
})
.collect();
let mut cleanup = Box::pin(release_owned_guards(entries));
assert!(futures::poll!(&mut cleanup).is_pending());
drop(cleanup);
assert_eq!(calls.load(Ordering::SeqCst), 2);
assert!(!first.is_valid());
assert!(!second.is_valid());
drop(first);
drop(second);
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
struct AcknowledgedRelease {
calls: Arc<AtomicUsize>,
permit: Arc<tokio::sync::Semaphore>,
}
impl LockRelease for AcknowledgedRelease {
fn release(&self) {}
fn release_async(&self) -> futures::future::BoxFuture<'_, Result<()>> {
Box::pin(async move {
self.calls.fetch_add(1, Ordering::SeqCst);
self.permit.acquire().await.unwrap().forget();
Ok(())
})
}
}
#[tokio::test]
async fn concurrent_release_waiters_share_acknowledgement_and_can_retry_cancellation() {
let calls = Arc::new(AtomicUsize::new(0));
let permit = Arc::new(tokio::sync::Semaphore::new(0));
let guard = LockGuard::new(
"shared",
Box::new(AcknowledgedRelease {
calls: calls.clone(),
permit: permit.clone(),
}),
);
let release = guard.release.clone();
let mut first = Box::pin(release.wait());
let mut second = Box::pin(guard.release_and_wait());
assert!(futures::poll!(&mut first).is_pending());
assert!(futures::poll!(&mut second).is_pending());
assert_eq!(calls.load(Ordering::SeqCst), 1);
drop(first);
assert!(futures::poll!(&mut second).is_pending());
assert_eq!(calls.load(Ordering::SeqCst), 2);
let mut third = Box::pin(release.wait());
assert!(futures::poll!(&mut third).is_pending());
permit.add_permits(1);
second.await.unwrap();
third.await.unwrap();
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
struct FailingRelease(Arc<AtomicUsize>);
impl LockRelease for FailingRelease {
fn release(&self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
fn release_async(&self) -> futures::future::BoxFuture<'_, Result<()>> {
Box::pin(async {
Err(Error::Storage(StorageError::Other(
"test release failure".into(),
)))
})
}
}
#[tokio::test]
async fn failed_awaited_release_reports_error_and_requests_drop_fallback() {
let calls = Arc::new(AtomicUsize::new(0));
let guard = LockGuard::new("failed", Box::new(FailingRelease(calls.clone())));
assert!(guard.release_and_wait().await.is_err());
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
}