use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError, Weak};
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
use std::sync::atomic::AtomicBool;
#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
use std::sync::atomic::{AtomicU64, Ordering};
use crate::client::SharedBackend;
const SWEEP_THRESHOLD: usize = 128;
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
const FILL_LOCK_TIMEOUT_MS: u64 = 5_000;
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
const FILL_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);
#[derive(Default)]
pub(crate) struct FlightMap {
entries: Mutex<Entries>,
}
#[derive(Default)]
struct Entries {
slots: HashMap<String, Slot>,
sweep_above: usize,
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
swept_at: Option<tokio::time::Instant>,
}
impl Entries {
fn sweep_due(&self) -> bool {
let len = self.slots.len();
if len <= SWEEP_THRESHOLD {
return false;
}
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
if self
.swept_at
.is_none_or(|at| at.elapsed() >= std::time::Duration::from_millis(FILL_LOCK_TIMEOUT_MS))
{
return true;
}
len > self.sweep_above
}
fn sweep(&mut self) {
self.slots.retain(|_, slot| {
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
slot.expire_pin();
slot.entry.strong_count() > 0
});
self.sweep_above = 2 * self.slots.len();
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
{
self.swept_at = Some(tokio::time::Instant::now());
}
}
}
struct Slot {
entry: Weak<tokio::sync::Mutex<KeyState>>,
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
pin: Option<Pin>,
}
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
struct Pin {
until: tokio::time::Instant,
answered: Arc<AtomicBool>,
_entry: Arc<tokio::sync::Mutex<KeyState>>,
}
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
impl Slot {
fn expire_pin(&mut self) {
if self
.pin
.as_ref()
.is_some_and(|pin| pin.until <= tokio::time::Instant::now())
{
self.pin = None;
}
}
}
#[derive(Default)]
struct KeyState {
stand_down: bool,
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
pending_release: Option<PendingRelease>,
}
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
struct PendingRelease {
lock_id: String,
answered: Arc<AtomicBool>,
}
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
impl PendingRelease {
async fn settle(&self, backend: &SharedBackend, full_key: &str) {
if !self.answered.load(Ordering::Acquire) {
release_lock(backend, full_key, &self.lock_id).await;
}
}
}
impl FlightMap {
fn handle(&self, key: &str) -> Arc<tokio::sync::Mutex<KeyState>> {
let mut entries = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
if entries.sweep_due() {
entries.sweep();
}
if let Some(existing) = entries.slots.get(key).and_then(|slot| slot.entry.upgrade()) {
return existing;
}
let fresh = Arc::new(tokio::sync::Mutex::new(KeyState::default()));
entries.slots.insert(
key.to_owned(),
Slot {
entry: Arc::downgrade(&fresh),
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
pin: None,
},
);
fresh
}
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
fn pin(&self, key: &str, entry: Arc<tokio::sync::Mutex<KeyState>>, answered: Arc<AtomicBool>) {
let until =
tokio::time::Instant::now() + std::time::Duration::from_millis(FILL_LOCK_TIMEOUT_MS);
let mut entries = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(slot) = entries.slots.get_mut(key) {
slot.pin = Some(Pin {
until,
answered,
_entry: entry,
});
}
}
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
fn unpin(&self, key: &str, answered: &Arc<AtomicBool>) {
let mut entries = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(slot) = entries.slots.get_mut(key) {
if slot
.pin
.as_ref()
.is_some_and(|pin| Arc::ptr_eq(&pin.answered, answered))
{
slot.pin = None;
}
}
}
}
#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
pub(crate) struct MutationState {
lock: Arc<tokio::sync::Mutex<()>>,
version: AtomicU64,
}
#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
impl MutationState {
fn new() -> Self {
Self {
lock: Arc::new(tokio::sync::Mutex::new(())),
version: AtomicU64::new(0),
}
}
pub(crate) fn version(&self) -> u64 {
self.version.load(Ordering::Acquire)
}
}
#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
#[derive(Default)]
pub(crate) struct MutationMap {
entries: Mutex<HashMap<String, Weak<MutationState>>>,
}
#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
impl MutationMap {
pub(crate) fn state(&self, key: &str) -> Arc<MutationState> {
let mut map = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
if map.len() > SWEEP_THRESHOLD {
map.retain(|_, weak| weak.strong_count() > 0);
}
if let Some(existing) = map.get(key).and_then(Weak::upgrade) {
return existing;
}
let fresh = Arc::new(MutationState::new());
map.insert(key.to_owned(), Arc::downgrade(&fresh));
fresh
}
pub(crate) async fn lock(&self, key: &str) -> MutationGuard {
let state = self.state(key);
let lock = Arc::clone(&state.lock).lock_owned().await;
MutationGuard { state, _lock: lock }
}
}
#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
pub(crate) struct MutationGuard {
state: Arc<MutationState>,
_lock: tokio::sync::OwnedMutexGuard<()>,
}
#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
impl MutationGuard {
pub(crate) fn snapshot(&self) -> (Arc<MutationState>, u64) {
(Arc::clone(&self.state), self.state.version())
}
pub(crate) fn is_current(&self, state: &Arc<MutationState>, version: u64) -> bool {
Arc::ptr_eq(&self.state, state) && self.state.version() == version
}
pub(crate) fn advance(&self) {
self.state.version.fetch_add(1, Ordering::Release);
}
}
enum Role {
Leader,
LocalFollower {
rechecked: bool,
#[cfg_attr(
not(all(feature = "reliability", not(target_arch = "wasm32"))),
allow(dead_code)
)]
contest_lease: bool,
},
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
RemoteContested { deadline: tokio::time::Instant },
}
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
enum LockAttempt {
Granted(String),
Unlockable,
Contested { deadline: tokio::time::Instant },
Failed,
}
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
impl LockAttempt {
async fn run(state: &mut KeyState, backend: &SharedBackend, full_key: &str) -> Self {
let Some(lockable) = backend.as_lockable() else {
return Self::Unlockable;
};
#[cfg(all(feature = "l1", not(feature = "unsync")))]
if let Some(pending) = &state.pending_release {
pending.settle(backend, full_key).await;
state.pending_release = None;
}
#[cfg(not(all(feature = "l1", not(feature = "unsync"))))]
let _ = state;
let timeout = std::time::Duration::from_millis(FILL_LOCK_TIMEOUT_MS);
let deadline = tokio::time::Instant::now() + timeout;
match lockable.acquire_lock(full_key, FILL_LOCK_TIMEOUT_MS).await {
Ok(Some(lock_id)) => Self::Granted(lock_id),
Ok(None) => Self::Contested { deadline },
Err(err) => {
crate::metrics::trace_lock_error(full_key, "lock", &err);
Self::Failed
}
}
}
fn cold_role(self) -> (Role, Option<String>) {
match self {
Self::Granted(lock_id) => (Role::Leader, Some(lock_id)),
Self::Unlockable | Self::Failed => (Role::Leader, None),
Self::Contested { deadline } => (Role::RemoteContested { deadline }, None),
}
}
}
pub struct SingleFlight {
local: tokio::sync::OwnedMutexGuard<KeyState>,
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
map: crate::client::SharedFlight,
role: Role,
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
backend: SharedBackend,
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
full_key: String,
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
lock_id: Option<String>,
}
impl SingleFlight {
pub async fn wait_for_fill(&mut self) -> bool {
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
if let Role::LocalFollower {
rechecked: true,
contest_lease: true,
} = self.role
{
(self.role, self.lock_id) =
LockAttempt::run(&mut self.local, &self.backend, &self.full_key)
.await
.cold_role();
self.note_role();
}
match &mut self.role {
Role::Leader => false,
Role::LocalFollower { rechecked, .. } => {
let first = !*rechecked;
*rechecked = true;
first
}
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
Role::RemoteContested { deadline } => {
let deadline = *deadline;
let now = tokio::time::Instant::now();
if now >= deadline {
return false;
}
tokio::time::sleep(FILL_POLL_INTERVAL.min(deadline - now)).await;
tokio::time::Instant::now() < deadline
}
}
}
pub async fn release(self) {
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
if let Some(lock_id) = &self.lock_id {
release_lock(&self.backend, &self.full_key, lock_id).await;
}
}
#[doc(hidden)]
#[cfg_attr(
not(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
)),
allow(unused_mut)
)]
pub async fn __release_detached(mut self) {
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
if let Ok(runtime) = tokio::runtime::Handle::try_current() {
if let Some(lock_id) = self.lock_id.take() {
let answered = Arc::new(AtomicBool::new(false));
let full_key = std::mem::take(&mut self.full_key);
self.map.pin(
&full_key,
Arc::clone(tokio::sync::OwnedMutexGuard::mutex(&self.local)),
Arc::clone(&answered),
);
let unlock = {
let (map, backend, lock_id, answered) = (
self.map.clone(),
self.backend.clone(),
lock_id.clone(),
Arc::clone(&answered),
);
async move {
if release_lock(&backend, &full_key, &lock_id).await {
answered.store(true, Ordering::Release);
map.unpin(&full_key, &answered);
}
}
};
#[cfg(feature = "tracing")]
let unlock = tracing::Instrument::instrument(unlock, tracing::Span::current());
drop(runtime.spawn(unlock));
self.local.pending_release = Some(PendingRelease { lock_id, answered });
}
return;
}
self.release().await;
}
#[doc(hidden)]
pub async fn __bounded_poll<F: std::future::Future>(&self, read: F) -> Option<F::Output> {
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
if let Role::RemoteContested { deadline } = self.role {
return tokio::time::timeout_at(deadline, read).await.ok();
}
Some(read.await)
}
fn note_role(&mut self) {
match self.role {
Role::Leader => self.local.stand_down = false,
Role::LocalFollower { .. } => {}
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
Role::RemoteContested { .. } => self.local.stand_down = true,
}
}
fn new(
map: &crate::client::SharedFlight,
local: tokio::sync::OwnedMutexGuard<KeyState>,
role: Role,
backend: &SharedBackend,
full_key: &str,
) -> Self {
#[cfg(not(all(feature = "reliability", not(target_arch = "wasm32"))))]
let _ = (backend, full_key);
#[cfg(not(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
)))]
let _ = map;
let mut flight = Self {
local,
#[cfg(all(
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
map: map.clone(),
role,
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
backend: backend.clone(),
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
full_key: full_key.to_owned(),
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
lock_id: None,
};
flight.note_role();
flight
}
pub(crate) async fn acquire(
map: &crate::client::SharedFlight,
backend: &SharedBackend,
full_key: &str,
) -> Self {
let handle = map.handle(full_key);
#[cfg_attr(
not(all(feature = "reliability", not(target_arch = "wasm32"))),
allow(unused_mut)
)]
let Ok(mut local) = Arc::clone(&handle).try_lock_owned() else {
let local = handle.lock_owned().await;
let role = Role::LocalFollower {
rechecked: false,
contest_lease: local.stand_down,
};
return Self::new(map, local, role, backend, full_key);
};
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
{
let (role, lock_id) = LockAttempt::run(&mut local, backend, full_key)
.await
.cold_role();
let mut flight = Self::new(map, local, role, backend, full_key);
flight.lock_id = lock_id;
flight
}
#[cfg(not(all(feature = "reliability", not(target_arch = "wasm32"))))]
Self::new(map, local, Role::Leader, backend, full_key)
}
pub(crate) async fn try_lead(
map: &crate::client::SharedFlight,
backend: &SharedBackend,
full_key: &str,
) -> Option<Self> {
let local = map.handle(full_key).try_lock_owned().ok()?;
#[cfg_attr(
not(all(feature = "reliability", not(target_arch = "wasm32"))),
allow(unused_mut)
)]
let mut flight = Self::new(map, local, Role::Leader, backend, full_key);
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
match LockAttempt::run(&mut flight.local, backend, full_key).await {
LockAttempt::Granted(lock_id) => flight.lock_id = Some(lock_id),
LockAttempt::Unlockable => {}
LockAttempt::Contested { .. } => {
flight.local.stand_down = true;
return None;
}
LockAttempt::Failed => return None,
}
Some(flight)
}
}
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
async fn release_lock(backend: &SharedBackend, full_key: &str, lock_id: &str) -> bool {
let Some(lockable) = backend.as_lockable() else {
return false;
};
match lockable.release_lock(full_key, lock_id).await {
Ok(_) => true,
Err(err) => {
crate::metrics::trace_lock_error(full_key, "unlock", &err);
false
}
}
}
#[cfg(all(
test,
feature = "reliability",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
mod tests {
use std::sync::atomic::AtomicUsize;
use async_trait::async_trait;
use super::*;
use crate::backend::{Backend, HealthStatus, LockableBackend};
use crate::error::BackendError;
#[derive(Default)]
struct Locks {
unlocks: AtomicUsize,
}
#[async_trait]
impl Backend for Locks {
async fn get(&self, _key: &str) -> Result<Option<Vec<u8>>, BackendError> {
Ok(None)
}
async fn set(
&self,
_key: &str,
_value: Vec<u8>,
_ttl: Option<std::time::Duration>,
) -> Result<(), BackendError> {
Ok(())
}
async fn delete(&self, _key: &str) -> Result<bool, BackendError> {
Ok(false)
}
async fn exists(&self, _key: &str) -> Result<bool, BackendError> {
Ok(false)
}
async fn health(&self) -> Result<HealthStatus, BackendError> {
Ok(HealthStatus {
is_healthy: true,
latency_ms: 0.0,
backend_type: "locks".to_owned(),
details: HashMap::new(),
})
}
fn as_lockable(&self) -> Option<&dyn LockableBackend> {
Some(self)
}
}
#[async_trait]
impl LockableBackend for Locks {
async fn acquire_lock(
&self,
_key: &str,
_timeout_ms: u64,
) -> Result<Option<String>, BackendError> {
Ok(Some("lease".to_owned()))
}
async fn release_lock(&self, _key: &str, _lock_id: &str) -> Result<bool, BackendError> {
self.unlocks.fetch_add(1, Ordering::SeqCst);
Ok(true)
}
}
#[tokio::test(start_paused = true)]
async fn a_burst_of_dead_slots_is_swept_after_a_lease_timeout() {
let map = FlightMap::default();
let burst: Vec<_> = (0..200)
.map(|i| map.handle(&format!("burst:{i}")))
.collect();
drop(burst);
map.handle("early");
assert!(
map.entries.lock().unwrap().slots.len() > 200,
"swept before a lease timeout"
);
tokio::time::advance(std::time::Duration::from_millis(FILL_LOCK_TIMEOUT_MS)).await;
map.handle("late");
assert!(
map.entries.lock().unwrap().slots.len() <= 2,
"dead slots retained"
);
}
#[tokio::test(start_paused = true)]
async fn an_answered_unlock_is_not_resent_while_the_entry_lives() {
let locks = Arc::new(Locks::default());
let backend: SharedBackend = locks.clone();
let map: crate::client::SharedFlight = Arc::default();
let _held = map.handle("k");
SingleFlight::acquire(&map, &backend, "k")
.await
.__release_detached()
.await;
while locks.unlocks.load(Ordering::SeqCst) == 0 {
tokio::task::yield_now().await;
}
let flight = SingleFlight::acquire(&map, &backend, "k").await;
assert_eq!(flight.lock_id.as_deref(), Some("lease"));
assert_eq!(
locks.unlocks.load(Ordering::SeqCst),
1,
"answered unlock re-sent"
);
}
}