use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use redis::aio::ConnectionManager;
use tokio::task::JoinHandle;
use tokio::time::{MissedTickBehavior, interval};
use crate::DistkitError;
use crate::lock::rwlock_backend::AcquireOptions;
use crate::lock::{LockError, LockGuardState, LockMode, LockOptions, rwlock_backend};
#[derive(Debug)]
pub struct RwLock {
connection_manager: ConnectionManager,
writer_key: String,
readers_key: String,
pending_key: String,
pending_heartbeat_key: String,
owner: String,
ttl_ms: i64,
ttl_duration: Duration,
max_wait: Option<Duration>,
retry_interval: Duration,
}
impl RwLock {
pub fn new(options: LockOptions) -> Arc<Self> {
let LockOptions {
key,
connection_manager,
namespace,
ttl,
owner_id,
max_wait,
retry_interval,
..
} = options;
let base = format!("{}:{}", *namespace, *key);
let owner = owner_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
Arc::new(Self {
connection_manager,
writer_key: format!("{base}:w"),
readers_key: format!("{base}:r"),
pending_key: format!("{base}:pw"),
pending_heartbeat_key: format!("{base}:pwh"),
owner,
ttl_ms: ttl.as_millis() as i64,
ttl_duration: ttl,
max_wait,
retry_interval,
})
}
pub async fn read(&self) -> Result<RwLockReadGuard, DistkitError> {
self.acquire_shared(self.max_wait, None, self.retry_interval)
.await
}
pub async fn try_read(&self) -> Result<RwLockReadGuard, DistkitError> {
self.acquire_shared(Some(Duration::ZERO), None, Duration::ZERO)
.await
}
pub async fn try_read_with_timeout(&self, timeout: Duration) -> Result<RwLockReadGuard, DistkitError> {
self.acquire_shared(Some(timeout), None, self.retry_interval)
.await
}
pub async fn try_read_with_retries(
&self,
max_retries: usize,
) -> Result<RwLockReadGuard, DistkitError> {
self.acquire_shared(None, Some(max_retries), self.retry_interval)
.await
}
#[deprecated(
since = "0.5.3",
note = "use `try_read_with_timeout`, which sources the retry interval from the lock's configuration"
)]
pub async fn try_read_for(
&self,
timeout: Duration,
retry_interval: Duration,
) -> Result<RwLockReadGuard, DistkitError> {
self.acquire_shared(Some(timeout), None, retry_interval).await
}
pub async fn write(&self) -> Result<RwLockWriteGuard, DistkitError> {
self.acquire_exclusive(self.max_wait, None, self.retry_interval)
.await
}
pub async fn try_write(&self) -> Result<RwLockWriteGuard, DistkitError> {
self.acquire_exclusive(Some(Duration::ZERO), None, Duration::ZERO)
.await
}
pub async fn try_write_with_timeout(
&self,
timeout: Duration,
) -> Result<RwLockWriteGuard, DistkitError> {
self.acquire_exclusive(Some(timeout), None, self.retry_interval)
.await
}
pub async fn try_write_with_retries(
&self,
max_retries: usize,
) -> Result<RwLockWriteGuard, DistkitError> {
self.acquire_exclusive(None, Some(max_retries), self.retry_interval)
.await
}
#[deprecated(
since = "0.5.3",
note = "use `try_write_with_timeout`, which sources the retry interval from the lock's configuration"
)]
pub async fn try_write_for(
&self,
timeout: Duration,
retry_interval: Duration,
) -> Result<RwLockWriteGuard, DistkitError> {
self.acquire_exclusive(Some(timeout), None, retry_interval)
.await
}
async fn acquire_shared(
&self,
timeout: Option<Duration>,
max_retries: Option<usize>,
retry_interval: Duration,
) -> Result<RwLockReadGuard, DistkitError> {
let on_attempt = self
.acquire_loop(LockMode::Shared, timeout, max_retries, retry_interval)
.await?;
let lost = Arc::new(AtomicBool::new(false));
let refresh_handle = self.spawn_refresh(LockMode::Shared, lost.clone());
Ok(RwLockReadGuard {
connection_manager: self.connection_manager.clone(),
readers_key: self.readers_key.clone(),
owner: self.owner.clone(),
refresh_handle: Some(refresh_handle),
lost,
on_attempt,
})
}
async fn acquire_exclusive(
&self,
timeout: Option<Duration>,
max_retries: Option<usize>,
retry_interval: Duration,
) -> Result<RwLockWriteGuard, DistkitError> {
let on_attempt = self
.acquire_loop(LockMode::Exclusive, timeout, max_retries, retry_interval)
.await?;
let lost = Arc::new(AtomicBool::new(false));
let refresh_handle = self.spawn_refresh(LockMode::Exclusive, lost.clone());
Ok(RwLockWriteGuard {
connection_manager: self.connection_manager.clone(),
writer_key: self.writer_key.clone(),
pending_key: self.pending_key.clone(),
pending_heartbeat_key: self.pending_heartbeat_key.clone(),
owner: self.owner.clone(),
refresh_handle: Some(refresh_handle),
lost,
on_attempt,
})
}
async fn acquire_loop(
&self,
mode: LockMode,
timeout: Option<Duration>,
max_retries: Option<usize>,
retry_interval: Duration,
) -> Result<usize, DistkitError> {
let start = Instant::now();
let mut connection = self.connection_manager.clone();
let mark_pending = !retry_interval.is_zero();
let mut retry_interval = if retry_interval.is_zero() {
None
} else {
let mut retry_interval = interval(retry_interval);
retry_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
Some(retry_interval)
};
let mut attempt = 0usize;
loop {
if let Some(retry_interval) = &mut retry_interval {
retry_interval.tick().await;
}
let options = AcquireOptions {
owner: &self.owner,
ttl_ms: self.ttl_ms,
writer_key: &self.writer_key,
readers_key: &self.readers_key,
pending_writers_key: &self.pending_key,
pending_writers_heartbeat_key: &self.pending_heartbeat_key,
};
let acquired = match mode {
LockMode::Shared => rwlock_backend::acquire_read(&mut connection, options).await?,
LockMode::Exclusive => {
rwlock_backend::acquire_write(&mut connection, options, mark_pending).await?
}
};
if acquired {
return Ok(attempt);
}
if retry_interval.is_none() {
self.clear_pending_on_giveup(mode, mark_pending, &mut connection)
.await;
return Err(LockError::AcquireFail.into());
}
if let Some(deadline) = timeout {
if deadline.is_zero() {
self.clear_pending_on_giveup(mode, mark_pending, &mut connection)
.await;
return Err(LockError::AcquireFail.into());
}
let waited = start.elapsed();
if waited >= deadline {
self.clear_pending_on_giveup(mode, mark_pending, &mut connection)
.await;
return Err(LockError::Timeout { waited }.into());
}
}
if let Some(max_retries) = max_retries
&& attempt >= max_retries
{
self.clear_pending_on_giveup(mode, mark_pending, &mut connection)
.await;
return Err(LockError::RetriesExhausted { retries: attempt }.into());
}
attempt += 1;
}
}
async fn clear_pending_on_giveup(
&self,
mode: LockMode,
mark_pending: bool,
connection: &mut ConnectionManager,
) {
if mode != LockMode::Exclusive || !mark_pending {
return;
}
if let Err(error) = rwlock_backend::release_write(
connection,
&self.writer_key,
&self.pending_key,
&self.pending_heartbeat_key,
&self.owner,
)
.await
{
tracing::debug!(?error, writer_key = self.writer_key, "Error clearing pending writer slot on give-up");
}
}
fn spawn_refresh(&self, mode: LockMode, lost: Arc<AtomicBool>) -> JoinHandle<()> {
let mut connection_manager = self.connection_manager.clone();
let writer_key = self.writer_key.clone();
let readers_key = self.readers_key.clone();
let owner = self.owner.clone();
let ttl_ms = self.ttl_ms;
let ttl_duration = self.ttl_duration;
tokio::spawn(async move {
let mut ticker = interval(ttl_duration / 3);
ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
ticker.tick().await;
loop {
ticker.tick().await;
let refreshed = match mode {
LockMode::Shared => {
rwlock_backend::refresh_read(&mut connection_manager, &readers_key, &owner, ttl_ms).await
}
LockMode::Exclusive => {
rwlock_backend::refresh_write(&mut connection_manager, &writer_key, &owner, ttl_ms).await
}
};
match refreshed {
Ok(true) => {
if lost.swap(false, Ordering::AcqRel) {
tracing::debug!(owner, "Lost distributed rwlock reacquired during refresh");
}
}
Ok(false) => {
tracing::debug!(owner, "Lost distributed rwlock lease during refresh");
lost.store(true, Ordering::Release);
}
Err(error) => {
tracing::debug!(?error, owner, "Error refreshing distributed rwlock lease");
lost.store(true, Ordering::Release);
}
}
}
})
} }
#[derive(Debug)]
pub struct RwLockReadGuard {
connection_manager: ConnectionManager,
readers_key: String,
owner: String,
refresh_handle: Option<JoinHandle<()>>,
lost: Arc<AtomicBool>,
on_attempt: usize,
}
impl RwLockReadGuard {
pub fn get_state(&self) -> LockGuardState {
state_from(&self.refresh_handle, &self.lost)
}
pub fn get_on_attempt(&self) -> usize {
self.on_attempt
}
pub async fn release(mut self) -> Result<LockGuardState, DistkitError> {
if let Some(handle) = self.refresh_handle.take() {
handle.abort();
}
if self.lost.load(Ordering::Acquire) {
return Ok(LockGuardState::Lost);
}
let mut connection = self.connection_manager.clone();
rwlock_backend::release_read(&mut connection, &self.readers_key, &self.owner).await?;
Ok(LockGuardState::Released)
}
}
impl Drop for RwLockReadGuard {
fn drop(&mut self) {
let Some(handle) = self.refresh_handle.take() else {
return;
};
handle.abort();
let mut connection = self.connection_manager.clone();
let readers_key = self.readers_key.clone();
let owner = self.owner.clone();
tokio::spawn(async move {
if let Err(error) = rwlock_backend::release_read(&mut connection, &readers_key, &owner).await {
tracing::error!(?error, readers_key, "Error releasing distributed read lock on drop");
}
});
}
}
#[derive(Debug)]
pub struct RwLockWriteGuard {
connection_manager: ConnectionManager,
writer_key: String,
pending_key: String,
pending_heartbeat_key: String,
owner: String,
refresh_handle: Option<JoinHandle<()>>,
lost: Arc<AtomicBool>,
on_attempt: usize,
}
impl RwLockWriteGuard {
pub fn get_state(&self) -> LockGuardState {
state_from(&self.refresh_handle, &self.lost)
}
pub fn get_on_attempt(&self) -> usize {
self.on_attempt
}
pub async fn release(mut self) -> Result<LockGuardState, DistkitError> {
if let Some(handle) = self.refresh_handle.take() {
handle.abort();
}
if self.lost.load(Ordering::Acquire) {
return Ok(LockGuardState::Lost);
}
let mut connection = self.connection_manager.clone();
rwlock_backend::release_write(
&mut connection,
&self.writer_key,
&self.pending_key,
&self.pending_heartbeat_key,
&self.owner,
)
.await?;
Ok(LockGuardState::Released)
}
}
impl Drop for RwLockWriteGuard {
fn drop(&mut self) {
let Some(handle) = self.refresh_handle.take() else {
return;
};
handle.abort();
let mut connection = self.connection_manager.clone();
let writer_key = self.writer_key.clone();
let pending_key = self.pending_key.clone();
let pending_heartbeat_key = self.pending_heartbeat_key.clone();
let owner = self.owner.clone();
tokio::spawn(async move {
if let Err(error) = rwlock_backend::release_write(
&mut connection,
&writer_key,
&pending_key,
&pending_heartbeat_key,
&owner,
)
.await
{
tracing::error!(?error, writer_key, "Error releasing distributed write lock on drop");
}
});
}
}
fn state_from(refresh_handle: &Option<JoinHandle<()>>, lost: &AtomicBool) -> LockGuardState {
if refresh_handle.is_none() {
return LockGuardState::Released;
}
if lost.load(Ordering::Acquire) {
return LockGuardState::Lost;
}
LockGuardState::Acquired
}