use crate::trace::{Span, Tag};
use crate::{hashtable::ConcurrentHashTable, key::CacheHashKey, CacheKey, NoCacheReason};
use http::Extensions;
use pingora_timeout::timeout;
use std::sync::Arc;
use std::time::Duration;
pub type CacheKeyLockImpl = dyn CacheKeyLock + Send + Sync;
pub trait CacheKeyLock {
fn lock(&self, key: &CacheKey, stale_writer: bool) -> Locked;
fn release(&self, key: &CacheKey, permit: WritePermit, reason: LockStatus);
fn trace_lock_wait(&self, span: &mut Span, _read_lock: &ReadLock, lock_status: LockStatus) {
let tag_value: &'static str = lock_status.into();
span.set_tag(|| Tag::new("status", tag_value));
}
fn custom_lock_status(&self, _custom_no_cache: &'static str) -> LockStatus {
LockStatus::GiveUp
}
}
const N_SHARDS: usize = 16;
#[derive(Debug)]
pub struct CacheLock {
lock_table: ConcurrentHashTable<LockStub, N_SHARDS>,
age_timeout_default: Duration,
}
#[derive(Debug)]
pub enum Locked {
Write(WritePermit),
Read(ReadLock),
}
impl Locked {
pub fn is_write(&self) -> bool {
matches!(self, Self::Write(_))
}
}
impl CacheLock {
pub fn new_boxed(age_timeout: Duration) -> Box<Self> {
Box::new(CacheLock {
lock_table: ConcurrentHashTable::new(),
age_timeout_default: age_timeout,
})
}
pub fn new(age_timeout_default: Duration) -> Self {
CacheLock {
lock_table: ConcurrentHashTable::new(),
age_timeout_default,
}
}
}
impl CacheKeyLock for CacheLock {
fn lock(&self, key: &CacheKey, stale_writer: bool) -> Locked {
let hash = key.combined_bin();
let key = u128::from_be_bytes(hash); let table = self.lock_table.get(key);
if let Some(lock) = table.read().get(&key) {
if !matches!(
lock.0.lock_status(),
LockStatus::Dangling | LockStatus::AgeTimeout
) {
return Locked::Read(lock.read_lock());
}
}
let mut table = table.write();
if let Some(lock) = table.get(&key) {
if !matches!(
lock.0.lock_status(),
LockStatus::Dangling | LockStatus::AgeTimeout
) {
return Locked::Read(lock.read_lock());
}
}
let (permit, stub) =
WritePermit::new(self.age_timeout_default, stale_writer, Extensions::new());
table.insert(key, stub);
Locked::Write(permit)
}
fn release(&self, key: &CacheKey, mut permit: WritePermit, reason: LockStatus) {
let hash = key.combined_bin();
let key = u128::from_be_bytes(hash); if permit.lock.lock_status() == LockStatus::AgeTimeout {
permit.unlock(LockStatus::AgeTimeout);
} else if let Some(_lock) = self.lock_table.write(key).remove(&key) {
permit.unlock(reason);
}
}
}
use log::warn;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::time::Instant;
use strum::{FromRepr, IntoStaticStr};
use tokio::sync::{oneshot, Semaphore};
#[derive(Debug, Copy, Clone, PartialEq, Eq, IntoStaticStr, FromRepr)]
#[repr(u8)]
pub enum LockStatus {
Waiting = 0,
Done = 1,
TransientError = 2,
GiveUp = 3,
Dangling = 4,
WaitTimeout = 5,
AgeTimeout = 6,
}
impl From<LockStatus> for u8 {
fn from(l: LockStatus) -> u8 {
match l {
LockStatus::Waiting => 0,
LockStatus::Done => 1,
LockStatus::TransientError => 2,
LockStatus::GiveUp => 3,
LockStatus::Dangling => 4,
LockStatus::WaitTimeout => 5,
LockStatus::AgeTimeout => 6,
}
}
}
impl From<u8> for LockStatus {
fn from(v: u8) -> Self {
Self::from_repr(v).unwrap_or(Self::GiveUp)
}
}
#[derive(Debug)]
pub struct LockCore {
pub lock_start: Instant,
pub age_timeout: Duration,
pub(super) lock: Semaphore,
lock_status: AtomicU8,
stale_writer: bool,
extensions: Extensions,
fill: Mutex<FillState>,
published_any: AtomicBool,
}
#[derive(Debug, Default)]
struct FillState {
published: Vec<u64>,
waiters: Vec<TokenWaiter>,
sweep_cursor: usize,
}
const WAITER_SWEEP_BUDGET: usize = 8;
#[derive(Debug)]
struct TokenWaiter {
unusable: Arc<[UnusableFill]>,
tell: oneshot::Sender<UnusableFill>,
}
impl LockCore {
pub fn new_arc(timeout: Duration, stale_writer: bool, extensions: Extensions) -> Arc<Self> {
Arc::new(LockCore {
lock: Semaphore::new(0),
age_timeout: timeout,
lock_start: Instant::now(),
lock_status: AtomicU8::new(LockStatus::Waiting.into()),
stale_writer,
extensions,
fill: Mutex::new(FillState::default()),
published_any: AtomicBool::new(false),
})
}
pub fn locked(&self) -> bool {
self.lock.available_permits() == 0
}
fn watch_fill(&self, unusable: &Arc<[UnusableFill]>) -> Watching {
let mut fill = self.fill.lock();
if let Some(matched) = first_match(&fill.published, unusable) {
return Watching::AlreadyPublished(matched);
}
let mut budget = WAITER_SWEEP_BUDGET;
while budget > 0 && !fill.waiters.is_empty() {
if fill.sweep_cursor >= fill.waiters.len() {
fill.sweep_cursor = 0;
}
let at = fill.sweep_cursor;
if fill.waiters[at].tell.is_closed() {
fill.waiters.swap_remove(at);
} else {
fill.sweep_cursor += 1;
}
budget -= 1;
}
let (tell, told) = oneshot::channel();
fill.waiters.push(TokenWaiter {
unusable: unusable.clone(),
tell,
});
Watching::Registered(told)
}
fn publish(&self, tokens: &[u64]) {
let woken = {
let mut fill = self.fill.lock();
fill.published.clear();
fill.published.extend_from_slice(tokens);
self.published_any
.store(!tokens.is_empty(), Ordering::Relaxed);
let mut woken = Vec::new();
let mut i = 0;
while i < fill.waiters.len() {
if fill.waiters[i].tell.is_closed() {
fill.waiters.swap_remove(i);
continue;
}
match first_match(tokens, &fill.waiters[i].unusable) {
Some(matched) => woken.push((fill.waiters.swap_remove(i).tell, matched)),
None => i += 1,
}
}
woken
};
for (tell, matched) in woken {
let _ = tell.send(matched);
}
}
#[cfg(test)]
fn registered_waiters(&self) -> usize {
self.fill
.lock()
.waiters
.iter()
.filter(|waiter| !waiter.tell.is_closed())
.count()
}
#[cfg(test)]
fn retained_waiters(&self) -> usize {
self.fill.lock().waiters.len()
}
pub fn unlock(&self, reason: LockStatus) {
assert!(
reason != LockStatus::WaitTimeout,
"WaitTimeout is not stored in LockCore"
);
self.lock_status.store(reason.into(), Ordering::SeqCst);
self.lock.add_permits(10);
}
pub fn lock_status(&self) -> LockStatus {
self.lock_status.load(Ordering::SeqCst).into()
}
pub fn stale_writer(&self) -> bool {
self.stale_writer
}
pub fn extensions(&self) -> &Extensions {
&self.extensions
}
}
#[derive(Debug)]
pub struct ReadLock(Arc<LockCore>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnusableFill {
pub token: u64,
pub reason: &'static str,
}
fn first_match(published: &[u64], unusable: &[UnusableFill]) -> Option<UnusableFill> {
unusable
.iter()
.find(|candidate| published.contains(&candidate.token))
.copied()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockWaitOutcome {
Done,
TransientError,
Dangling,
WaitTimeout,
AgeTimeout,
GiveUp,
Abandoned {
reason: NoCacheReason,
token: u64,
},
}
impl LockWaitOutcome {
pub fn lock_status(&self) -> LockStatus {
match self {
LockWaitOutcome::Done => LockStatus::Done,
LockWaitOutcome::TransientError => LockStatus::TransientError,
LockWaitOutcome::Dangling => LockStatus::Dangling,
LockWaitOutcome::WaitTimeout => LockStatus::WaitTimeout,
LockWaitOutcome::AgeTimeout => LockStatus::AgeTimeout,
LockWaitOutcome::GiveUp | LockWaitOutcome::Abandoned { .. } => LockStatus::GiveUp,
}
}
}
#[derive(Debug, Clone)]
pub struct UnusableFills {
pub(crate) fills: Arc<[UnusableFill]>,
}
impl UnusableFills {
pub fn new(fills: impl Into<Arc<[UnusableFill]>>) -> Self {
UnusableFills {
fills: fills.into(),
}
}
pub fn fills(&self) -> &[UnusableFill] {
&self.fills
}
pub fn first_match(&self, published: &[u64]) -> Option<UnusableFill> {
first_match(published, &self.fills)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WaitOutcome {
Released,
AgeTimeout,
Abandoned(UnusableFill),
}
enum Watching {
Registered(oneshot::Receiver<UnusableFill>),
AlreadyPublished(UnusableFill),
}
impl ReadLock {
pub async fn wait(&self) {
self.wait_inner(None).await;
}
pub async fn wait_unless_published(&self, unusable: &UnusableFills) -> WaitOutcome {
self.wait_inner(Some(&unusable.fills)).await
}
async fn wait_inner(&self, unusable: Option<&Arc<[UnusableFill]>>) -> WaitOutcome {
if !self.locked() {
return WaitOutcome::Released;
}
let Some(duration) = self.0.age_timeout.checked_sub(self.0.lock_start.elapsed()) else {
self.0
.lock_status
.store(LockStatus::AgeTimeout.into(), Ordering::SeqCst);
return WaitOutcome::AgeTimeout;
};
let unusable = unusable.filter(|unusable| !unusable.is_empty());
let told = match unusable {
None => {
return Self::writer_done(&self.0, duration).await;
}
Some(unusable) => match self.0.watch_fill(unusable) {
Watching::AlreadyPublished(matched) => {
if !self.locked() {
return WaitOutcome::Released;
}
return WaitOutcome::Abandoned(matched);
}
Watching::Registered(told) => told,
},
};
let told_to_stop = async {
match told.await {
Ok(matched) => WaitOutcome::Abandoned(matched),
Err(_) => {
debug_assert!(false, "fill waiter sender dropped while registered");
WaitOutcome::Released
}
}
};
tokio::select! {
biased;
outcome = Self::writer_done(&self.0, duration) => outcome,
outcome = told_to_stop => outcome,
}
}
async fn writer_done(core: &LockCore, duration: Duration) -> WaitOutcome {
match timeout(duration, core.lock.acquire()).await {
Ok(Ok(_)) => {
WaitOutcome::Released
}
Ok(Err(e)) => {
warn!("error acquiring semaphore {e:?}");
WaitOutcome::Released
}
Err(_) => {
core.lock_status
.store(LockStatus::AgeTimeout.into(), Ordering::SeqCst);
WaitOutcome::AgeTimeout
}
}
}
pub fn locked(&self) -> bool {
self.0.locked()
}
pub fn expired(&self) -> bool {
self.0.lock_start.elapsed() >= self.0.age_timeout
}
pub fn lock_status(&self) -> LockStatus {
let status = self.0.lock_status();
if matches!(status, LockStatus::Waiting) && self.expired() {
LockStatus::AgeTimeout
} else {
status
}
}
pub fn extensions(&self) -> &Extensions {
self.0.extensions()
}
}
#[derive(Debug)]
pub struct WritePermit {
lock: Arc<LockCore>,
finished: bool,
}
impl WritePermit {
pub fn new(
timeout: Duration,
stale_writer: bool,
extensions: Extensions,
) -> (WritePermit, LockStub) {
let lock = LockCore::new_arc(timeout, stale_writer, extensions);
let stub = LockStub(lock.clone());
(
WritePermit {
lock,
finished: false,
},
stub,
)
}
pub fn stale_writer(&self) -> bool {
self.lock.stale_writer()
}
pub fn unlock(&mut self, reason: LockStatus) {
self.finished = true;
self.lock.unlock(reason);
}
pub fn lock_status(&self) -> LockStatus {
self.lock.lock_status()
}
pub fn extensions(&self) -> &Extensions {
self.lock.extensions()
}
pub fn publish(&self, tokens: &[u64]) {
if tokens.is_empty() && !self.lock.published_any.load(Ordering::Relaxed) {
return;
}
self.lock.publish(tokens);
}
}
impl Drop for WritePermit {
fn drop(&mut self) {
if !self.finished {
debug_assert!(false, "Dangling cache lock started!");
self.unlock(LockStatus::Dangling);
}
}
}
#[derive(Debug)]
pub struct LockStub(pub Arc<LockCore>);
impl LockStub {
pub fn read_lock(&self) -> ReadLock {
ReadLock(self.0.clone())
}
pub fn extensions(&self) -> &Extensions {
&self.0.extensions
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::CacheKey;
const WRONG_PLACE: u64 = 7;
const SOMEWHERE_ELSE: u64 = 9;
const NO_GOOD: &str = "NoGoodToThisReader";
fn cannot_use(token: u64) -> [UnusableFill; 1] {
[UnusableFill {
token,
reason: NO_GOOD,
}]
}
fn new_lock(age: Duration) -> (WritePermit, LockStub) {
WritePermit::new(age, false, Extensions::new())
}
fn reader(stub: &LockStub) -> ReadLock {
ReadLock(stub.0.clone())
}
async fn registered(stub: &LockStub, want: usize) {
for _ in 0..1_000 {
if stub.0.registered_waiters() >= want {
return;
}
tokio::task::yield_now().await;
}
panic!(
"expected {want} registered waiter(s), found {}",
stub.0.registered_waiters()
);
}
struct CountPolls {
inner: std::pin::Pin<Box<dyn std::future::Future<Output = WaitOutcome> + Send>>,
polls: Arc<std::sync::atomic::AtomicUsize>,
}
impl std::future::Future for CountPolls {
type Output = WaitOutcome;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<WaitOutcome> {
let this = self.get_mut();
this.polls.fetch_add(1, Ordering::Relaxed);
this.inner.as_mut().poll(cx)
}
}
#[tokio::test]
async fn only_the_reader_whose_tokens_match_is_woken() {
let (permit, stub) = new_lock(Duration::from_secs(30));
let polls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let leaves = reader(&stub);
let left = tokio::spawn(async move {
leaves
.wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
.await
});
let stays = reader(&stub);
let stayed = tokio::spawn(CountPolls {
inner: Box::pin(async move {
stays
.wait_unless_published(&UnusableFills::new(cannot_use(SOMEWHERE_ELSE)))
.await
}),
polls: polls.clone(),
});
registered(&stub, 2).await;
let polled_before = polls.load(Ordering::Relaxed);
permit.publish(&[WRONG_PLACE]);
assert_eq!(
left.await.unwrap(),
WaitOutcome::Abandoned(UnusableFill {
token: WRONG_PLACE,
reason: NO_GOOD,
})
);
assert!(!stayed.is_finished(), "the other reader still coalesces");
assert_eq!(
polls.load(Ordering::Relaxed),
polled_before,
"and was never woken by a publication that does not concern it"
);
assert_eq!(
stub.0.registered_waiters(),
1,
"the reader that left is no longer registered"
);
assert_eq!(
stub.0.lock_status(),
LockStatus::Waiting,
"giving up does not touch the shared status"
);
let mut permit = permit;
permit.unlock(LockStatus::Done);
assert_eq!(stayed.await.unwrap(), WaitOutcome::Released);
assert!(
polls.load(Ordering::Relaxed) > polled_before,
"it is woken when the writer actually releases"
);
}
#[tokio::test]
async fn a_reader_that_names_no_tokens_is_never_registered() {
let (permit, stub) = new_lock(Duration::from_secs(30));
let core = reader(&stub);
let waiting = tokio::spawn(async move { core.wait().await });
tokio::task::yield_now().await;
assert_eq!(stub.0.registered_waiters(), 0);
permit.publish(&[WRONG_PLACE, SOMEWHERE_ELSE]);
tokio::task::yield_now().await;
assert!(!waiting.is_finished());
assert_eq!(stub.0.registered_waiters(), 0);
let mut permit = permit;
permit.unlock(LockStatus::Done);
waiting.await.unwrap();
}
#[tokio::test]
async fn tokens_published_before_the_reader_arrives_are_not_missed() {
let (permit, stub) = new_lock(Duration::from_secs(30));
permit.publish(&[WRONG_PLACE]);
let outcome = reader(&stub)
.wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
.await;
assert_eq!(
outcome,
WaitOutcome::Abandoned(UnusableFill {
token: WRONG_PLACE,
reason: NO_GOOD,
})
);
assert_eq!(stub.0.registered_waiters(), 0, "and it did not register");
let mut permit = permit;
permit.unlock(LockStatus::Done);
}
#[tokio::test]
async fn a_reader_is_not_stopped_by_a_superseded_publication() {
let (permit, stub) = new_lock(Duration::from_secs(30));
permit.publish(&[WRONG_PLACE]);
permit.publish(&[SOMEWHERE_ELSE]);
let stays = reader(&stub);
let stayed = tokio::spawn(async move {
stays
.wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
.await
});
registered(&stub, 1).await;
assert!(!stayed.is_finished(), "the old attempt no longer matters");
permit.publish(&[WRONG_PLACE]);
assert_eq!(
stayed.await.unwrap(),
WaitOutcome::Abandoned(UnusableFill {
token: WRONG_PLACE,
reason: NO_GOOD,
})
);
let mut permit = permit;
permit.unlock(LockStatus::Done);
}
#[tokio::test]
async fn cancelling_the_wait_leaves_nothing_to_match() {
let (mut permit, stub) = new_lock(Duration::from_secs(30));
let waits = reader(&stub);
let interest = UnusableFills::new(cannot_use(WRONG_PLACE));
let mut wait = Box::pin(waits.wait_unless_published(&interest));
assert!(
futures::poll!(wait.as_mut()).is_pending(),
"the writer still holds the lock"
);
assert_eq!(stub.0.registered_waiters(), 1);
assert_eq!(stub.0.retained_waiters(), 1);
drop(wait);
assert_eq!(
stub.0.registered_waiters(),
0,
"a cancelled reader must not be left for the writer to test"
);
assert_eq!(
stub.0.retained_waiters(),
1,
"though the entry is still there until something sweeps it"
);
permit.publish(&[SOMEWHERE_ELSE]);
assert_eq!(
stub.0.retained_waiters(),
0,
"and a publication sweeps it even without matching it"
);
permit.unlock(LockStatus::Done);
}
#[tokio::test]
async fn departed_readers_are_swept_by_later_registrations() {
let (mut permit, stub) = new_lock(Duration::from_secs(30));
let interest = UnusableFills::new(cannot_use(WRONG_PLACE));
for _ in 0..200 {
let waits = reader(&stub);
let mut wait = Box::pin(waits.wait_unless_published(&interest));
assert!(
futures::poll!(wait.as_mut()).is_pending(),
"the writer still holds the lock"
);
drop(wait);
}
assert_eq!(
stub.0.registered_waiters(),
0,
"every one of those readers has gone"
);
assert!(
stub.0.retained_waiters() <= WAITER_SWEEP_BUDGET,
"swept back down, found {}",
stub.0.retained_waiters()
);
permit.unlock(LockStatus::Done);
}
#[tokio::test]
async fn departed_readers_behind_live_ones_are_still_reclaimed() {
let (mut permit, stub) = new_lock(Duration::from_secs(30));
let interest = UnusableFills::new(cannot_use(WRONG_PLACE));
let waits = reader(&stub);
let mut live = Vec::new();
for _ in 0..(WAITER_SWEEP_BUDGET * 2) {
let mut wait = Box::pin(waits.wait_unless_published(&interest));
assert!(futures::poll!(wait.as_mut()).is_pending());
live.push(wait);
}
assert_eq!(stub.0.registered_waiters(), live.len());
let departed = WAITER_SWEEP_BUDGET * 4;
for _ in 0..departed {
let mut wait = Box::pin(waits.wait_unless_published(&interest));
assert!(futures::poll!(wait.as_mut()).is_pending());
drop(wait);
}
assert_eq!(
stub.0.registered_waiters(),
live.len(),
"the readers that stayed are all still registered"
);
assert!(
stub.0.retained_waiters() <= live.len() + WAITER_SWEEP_BUDGET,
"departed readers must be reclaimed despite never being at the front, \
leaving an overhead set by the sweep budget rather than by how many \
readers have come and gone: found {} entries for {} live readers",
stub.0.retained_waiters(),
live.len()
);
drop(live);
permit.unlock(LockStatus::Done);
}
#[tokio::test]
async fn a_wait_cut_short_by_an_outer_timeout_leaves_nothing_to_match() {
let (mut permit, stub) = new_lock(Duration::from_secs(30));
let waits = reader(&stub);
let cut_short = tokio::time::timeout(
Duration::from_millis(10),
waits.wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE))),
)
.await;
assert!(cut_short.is_err(), "the writer never released");
assert_eq!(
stub.0.registered_waiters(),
0,
"nothing is left for a publication to match"
);
assert_eq!(
stub.0.retained_waiters(),
1,
"the entry outlives the wait, to be swept later"
);
permit.unlock(LockStatus::Done);
}
#[tokio::test]
async fn an_empty_interest_registers_no_waiter() {
let (mut permit, stub) = new_lock(Duration::from_secs(30));
let waits = reader(&stub);
let waited =
tokio::spawn(async move { waits.wait_unless_published(&UnusableFills::new([])).await });
let names_something = reader(&stub);
let named = tokio::spawn(async move {
names_something
.wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
.await
});
registered(&stub, 1).await;
assert_eq!(
stub.0.registered_waiters(),
1,
"an interest that names nothing has nothing to be told about"
);
drop(named);
permit.unlock(LockStatus::Done);
assert_eq!(waited.await.unwrap(), WaitOutcome::Released);
}
#[tokio::test]
async fn publishing_nothing_clears_a_previous_publication() {
let (permit, stub) = new_lock(Duration::from_secs(30));
permit.publish(&[WRONG_PLACE]);
permit.publish(&[]);
assert_eq!(
first_match(&stub.0.fill.lock().published, &cannot_use(WRONG_PLACE)),
None,
"the abandoned attempt no longer matters"
);
let stays = reader(&stub);
let stayed = tokio::spawn(async move {
stays
.wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
.await
});
let mut permit = permit;
permit.unlock(LockStatus::Done);
assert_eq!(stayed.await.unwrap(), WaitOutcome::Released);
}
#[tokio::test]
async fn a_reader_that_times_out_reports_the_age_timeout() {
let (permit, stub) = new_lock(Duration::from_millis(50));
let outcome = reader(&stub)
.wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
.await;
assert_eq!(outcome, WaitOutcome::AgeTimeout);
let mut permit = permit;
permit.unlock(LockStatus::Done);
}
#[test]
fn test_get_release() {
let cache_lock = CacheLock::new_boxed(Duration::from_secs(1000));
let key1 = CacheKey::new("a", "1");
let locked1 = cache_lock.lock(&key1, false);
assert!(locked1.is_write()); let locked2 = cache_lock.lock(&key1, false);
assert!(!locked2.is_write()); if let Locked::Write(permit) = locked1 {
cache_lock.release(&key1, permit, LockStatus::Done);
}
let locked3 = cache_lock.lock(&key1, false);
assert!(locked3.is_write()); if let Locked::Write(permit) = locked3 {
cache_lock.release(&key1, permit, LockStatus::Done);
}
}
#[tokio::test]
async fn test_lock() {
let cache_lock = CacheLock::new_boxed(Duration::from_secs(1000));
let key1 = CacheKey::new("a", "1");
let mut permit = match cache_lock.lock(&key1, false) {
Locked::Write(w) => w,
_ => panic!(),
};
let lock = match cache_lock.lock(&key1, false) {
Locked::Read(r) => r,
_ => panic!(),
};
assert!(lock.locked());
let handle = tokio::spawn(async move {
lock.wait().await;
assert_eq!(lock.lock_status(), LockStatus::Done);
});
permit.unlock(LockStatus::Done);
handle.await.unwrap(); }
#[tokio::test]
async fn test_lock_timeout() {
let cache_lock = CacheLock::new_boxed(Duration::from_secs(1));
let key1 = CacheKey::new("a", "1");
let mut permit = match cache_lock.lock(&key1, false) {
Locked::Write(w) => w,
_ => panic!(),
};
let lock = match cache_lock.lock(&key1, false) {
Locked::Read(r) => r,
_ => panic!(),
};
assert!(lock.locked());
let handle = tokio::spawn(async move {
lock.wait().await;
assert_eq!(lock.lock_status(), LockStatus::AgeTimeout);
});
tokio::time::sleep(Duration::from_millis(2100)).await;
handle.await.unwrap();
let mut permit2 = match cache_lock.lock(&key1, false) {
Locked::Write(w) => w,
_ => panic!(),
};
let lock2 = match cache_lock.lock(&key1, false) {
Locked::Read(r) => r,
_ => panic!(),
};
assert!(lock2.locked());
let handle = tokio::spawn(async move {
lock2.wait().await;
assert_eq!(lock2.lock_status(), LockStatus::Done);
});
permit.unlock(LockStatus::Done);
permit2.unlock(LockStatus::Done);
handle.await.unwrap();
}
#[tokio::test]
async fn test_lock_expired_release() {
let cache_lock = CacheLock::new_boxed(Duration::from_secs(1));
let key1 = CacheKey::new("a", "1");
let permit = match cache_lock.lock(&key1, false) {
Locked::Write(w) => w,
_ => panic!(),
};
let lock = match cache_lock.lock(&key1, false) {
Locked::Read(r) => r,
_ => panic!(),
};
assert!(lock.locked());
let handle = tokio::spawn(async move {
lock.wait().await;
assert_eq!(lock.lock_status(), LockStatus::AgeTimeout);
});
tokio::time::sleep(Duration::from_millis(1100)).await; handle.await.unwrap();
cache_lock.release(&key1, permit, LockStatus::Done);
let mut permit = match cache_lock.lock(&key1, false) {
Locked::Write(w) => w,
_ => panic!(),
};
assert_eq!(permit.lock.lock_status(), LockStatus::Waiting);
let lock2 = match cache_lock.lock(&key1, false) {
Locked::Read(r) => r,
_ => panic!(),
};
assert!(lock2.locked());
let handle = tokio::spawn(async move {
lock2.wait().await;
assert_eq!(lock2.lock_status(), LockStatus::Done);
});
permit.unlock(LockStatus::Done);
handle.await.unwrap();
}
#[tokio::test]
async fn test_lock_expired_no_reader() {
let cache_lock = CacheLock::new_boxed(Duration::from_secs(1));
let key1 = CacheKey::new("a", "1");
let mut permit = match cache_lock.lock(&key1, false) {
Locked::Write(w) => w,
_ => panic!(),
};
tokio::time::sleep(Duration::from_millis(1100)).await;
assert_eq!(permit.lock.lock_status(), LockStatus::Waiting);
let lock = match cache_lock.lock(&key1, false) {
Locked::Read(r) => r,
_ => panic!(),
};
lock.wait().await;
assert_eq!(lock.lock_status(), LockStatus::AgeTimeout);
assert_eq!(permit.lock.lock_status(), LockStatus::AgeTimeout);
permit.unlock(LockStatus::AgeTimeout);
}
#[tokio::test]
async fn test_lock_concurrent() {
let _ = env_logger::builder().is_test(true).try_init();
let cache_lock = Arc::new(CacheLock::new_boxed(Duration::from_secs(1)));
let key1 = CacheKey::new("a", "1");
let mut handles = vec![];
const READERS: usize = 30;
for _ in 0..READERS {
let key1 = key1.clone();
let cache_lock = cache_lock.clone();
handles.push(tokio::spawn(async move {
loop {
match cache_lock.lock(&key1, false) {
Locked::Write(permit) => {
let _ = tokio::time::sleep(Duration::from_millis(5)).await;
cache_lock.release(&key1, permit, LockStatus::Done);
break;
}
Locked::Read(r) => {
r.wait().await;
}
}
}
}));
}
for handle in handles {
handle.await.unwrap();
}
}
#[tokio::test]
async fn an_expired_lock_times_out_rather_than_abandoning() {
let (mut permit, stub) = new_lock(Duration::from_millis(10));
stub.0.publish(&[WRONG_PLACE]);
tokio::time::sleep(Duration::from_millis(30)).await;
let outcome = reader(&stub)
.wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
.await;
assert_eq!(
outcome,
WaitOutcome::AgeTimeout,
"expiry is decided before any published token is consulted"
);
assert_eq!(
stub.0.lock_status(),
LockStatus::AgeTimeout,
"the dead lock must not be left reading Waiting"
);
permit.unlock(LockStatus::Done);
}
}