use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
use async_trait::async_trait;
use tokio::time::Instant;
use crate::backend::{InMemoryBackend, LockBackend};
use crate::client::{LockClient, LockConfig};
use crate::error::{Error, Result};
struct PartitionableBackend {
inner: InMemoryBackend,
partitioned: AtomicBool,
}
impl PartitionableBackend {
fn new() -> Self {
Self {
inner: InMemoryBackend::new(),
partitioned: AtomicBool::new(false),
}
}
fn set_partitioned(&self, v: bool) {
self.partitioned.store(v, Ordering::SeqCst);
}
}
#[async_trait]
impl LockBackend for PartitionableBackend {
async fn try_acquire(&self, key: &str, ttl: Duration) -> Result<(u64, std::time::Instant)> {
self.inner.try_acquire(key, ttl).await
}
async fn try_renew(&self, key: &str, token: u64, ttl: Duration) -> Result<std::time::Instant> {
if self.partitioned.load(Ordering::SeqCst) {
return Err(Error::cluster("simulated network partition: renew dropped"));
}
self.inner.try_renew(key, token, ttl).await
}
async fn release(&self, key: &str, token: u64) -> Result<()> {
self.inner.release(key, token).await
}
fn name(&self) -> &'static str {
"partitionable(dst)"
}
}
fn test_config() -> LockConfig {
LockConfig {
cluster_seed: vec![],
default_ttl: Duration::from_secs(10),
watchdog_interval: Duration::from_secs(4),
}
}
#[tokio::test(start_paused = true)]
async fn dst_ten_tasks_mutual_exclusion_and_monotonic_tokens() {
let live_holders = Arc::new(AtomicU64::new(0));
let max_token = Arc::new(AtomicU64::new(0));
let violations = Arc::new(AtomicU64::new(0));
let token_regressions = Arc::new(AtomicU64::new(0));
let backend = Arc::new(InMemoryBackend::new());
let mut tasks = Vec::new();
for _ in 0..10 {
let backend = Arc::clone(&backend) as Arc<dyn LockBackend>;
let live_holders = Arc::clone(&live_holders);
let max_token = Arc::clone(&max_token);
let violations = Arc::clone(&violations);
let token_regressions = Arc::clone(&token_regressions);
tasks.push(tokio::spawn(async move {
let client = LockClient::connect_with(test_config(), backend)
.await
.expect("connect");
for _ in 0..3 {
let guard = client
.lock("shared/resource")
.await
.with_ttl(Duration::from_secs(8))
.acquire()
.await
.expect("acquire within budget");
let now_live = live_holders.fetch_add(1, Ordering::SeqCst) + 1;
if now_live > 1 {
violations.fetch_add(1, Ordering::SeqCst);
}
let token = guard.fencing_token().expect("token valid");
let prev = max_token.fetch_max(token, Ordering::SeqCst);
if token < prev {
token_regressions.fetch_add(1, Ordering::SeqCst);
}
tokio::time::sleep(Duration::from_millis(1)).await;
live_holders.fetch_sub(1, Ordering::SeqCst);
guard.unlock().await.expect("unlock");
}
}));
}
let deadline = Instant::now() + Duration::from_secs(120);
for t in tasks {
t.await.expect("task panicked");
}
assert!(Instant::now() <= deadline, "DST took too long in sim time");
assert_eq!(
violations.load(Ordering::SeqCst),
0,
"mutual exclusion violated: two tasks held the lock simultaneously"
);
assert_eq!(
token_regressions.load(Ordering::SeqCst),
0,
"fencing token regressed"
);
assert!(
max_token.load(Ordering::SeqCst) >= 30,
"expected >=30 acquisitions"
);
}
#[tokio::test]
async fn acquired_at_stamp_is_monotonic() {
let backend = Arc::new(InMemoryBackend::new()) as Arc<dyn LockBackend>;
let client = LockClient::connect_with(test_config(), backend)
.await
.unwrap();
let mut prev = None;
for _ in 0..5 {
let g = client.lock("stamped/key").await.acquire().await.unwrap();
let s = g.acquired_at();
if let Some(p) = prev {
assert!(
s > p,
"acquired_at stamp must be strictly monotonic: {s:?} !> {p:?}"
);
}
prev = Some(s);
g.unlock().await.unwrap();
}
#[cfg(feature = "ash-time")]
assert!(
prev.unwrap().physical > 0,
"ash-time stamp should carry a wall-clock component"
);
}
struct RenewRecordingBackend {
inner: InMemoryBackend,
last_renew_ttl_ms: AtomicU64,
}
impl RenewRecordingBackend {
fn new() -> Self {
Self {
inner: InMemoryBackend::new(),
last_renew_ttl_ms: AtomicU64::new(0),
}
}
}
#[async_trait]
impl LockBackend for RenewRecordingBackend {
async fn try_acquire(&self, key: &str, ttl: Duration) -> Result<(u64, std::time::Instant)> {
self.inner.try_acquire(key, ttl).await
}
async fn try_renew(&self, key: &str, token: u64, ttl: Duration) -> Result<std::time::Instant> {
self.last_renew_ttl_ms
.store(ttl.as_millis() as u64, Ordering::SeqCst);
self.inner.try_renew(key, token, ttl).await
}
async fn release(&self, key: &str, token: u64) -> Result<()> {
self.inner.release(key, token).await
}
fn name(&self) -> &'static str {
"renew-recording(dst)"
}
}
#[tokio::test(start_paused = true)]
async fn extend_updates_watchdog_renewal_ttl() {
let backend = Arc::new(RenewRecordingBackend::new());
let client =
LockClient::connect_with(test_config(), Arc::clone(&backend) as Arc<dyn LockBackend>)
.await
.unwrap();
let guard = client
.lock("extended/key")
.await
.with_ttl(Duration::from_secs(8))
.acquire()
.await
.unwrap();
tokio::task::yield_now().await;
guard.extend(Duration::from_secs(600)).await.unwrap();
tokio::time::advance(Duration::from_secs(5)).await;
tokio::task::yield_now().await;
tokio::task::yield_now().await;
assert!(guard.state_renewals() >= 1, "watchdog should have renewed");
assert_eq!(
backend.last_renew_ttl_ms.load(Ordering::SeqCst),
600_000,
"watchdog must renew with the extended TTL"
);
guard.unlock().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn short_ttl_lock_is_renewed_before_it_lapses() {
let backend = Arc::new(InMemoryBackend::new()) as Arc<dyn LockBackend>;
let client = LockClient::connect_with(test_config(), backend)
.await
.unwrap();
let guard = client
.lock("short/key")
.await
.with_ttl(Duration::from_secs(2))
.acquire()
.await
.unwrap();
tokio::task::yield_now().await;
for _ in 0..10 {
tokio::time::advance(Duration::from_secs(1)).await;
tokio::task::yield_now().await;
}
assert!(
guard.fencing_token().is_ok(),
"short-TTL lease must be kept alive by the watchdog"
);
assert!(guard.state_renewals() >= 4, "expected multiple renewals");
guard.unlock().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn dst_partition_poisons_guard_no_zombie_lock() {
let backend = Arc::new(PartitionableBackend::new());
let client =
LockClient::connect_with(test_config(), Arc::clone(&backend) as Arc<dyn LockBackend>)
.await
.unwrap();
let guard = client
.lock("partitioned/key")
.await
.with_ttl(Duration::from_secs(8))
.acquire()
.await
.unwrap();
assert!(
guard.fencing_token().is_ok(),
"healthy guard has a valid token"
);
tokio::task::yield_now().await;
backend.set_partitioned(true);
tokio::time::advance(Duration::from_secs(5)).await;
tokio::task::yield_now().await;
tokio::task::yield_now().await;
assert!(
matches!(guard.fencing_token(), Err(Error::LeaseExpired { .. })),
"after a partition, the guard must be poisoned (crash-fast fencing)"
);
assert!(
matches!(
guard.extend(Duration::from_secs(8)).await,
Err(Error::LeaseExpired { .. })
),
"extend on a poisoned guard must fail"
);
}