Skip to main content

pingora_cache/
lock.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Cache lock
16
17use crate::trace::{Span, Tag};
18use crate::{hashtable::ConcurrentHashTable, key::CacheHashKey, CacheKey, NoCacheReason};
19
20use http::Extensions;
21use pingora_timeout::timeout;
22use std::sync::Arc;
23use std::time::Duration;
24
25pub type CacheKeyLockImpl = dyn CacheKeyLock + Send + Sync;
26
27pub trait CacheKeyLock {
28    /// Try to lock a cache fetch
29    ///
30    /// If `stale_writer` is true, this fetch is to revalidate an asset already in cache.
31    /// Else this fetch was a cache miss (i.e. not found via lookup, or force missed).
32    ///
33    /// Users should call after a cache miss before fetching the asset.
34    /// The returned [Locked] will tell the caller either to fetch or wait.
35    fn lock(&self, key: &CacheKey, stale_writer: bool) -> Locked;
36
37    /// Release a lock for the given key
38    ///
39    /// When the write lock is dropped without being released, the read lock holders will consider
40    /// it to be failed so that they will compete for the write lock again.
41    fn release(&self, key: &CacheKey, permit: WritePermit, reason: LockStatus);
42
43    /// Set tags on a trace span for the cache lock wait.
44    fn trace_lock_wait(&self, span: &mut Span, _read_lock: &ReadLock, lock_status: LockStatus) {
45        let tag_value: &'static str = lock_status.into();
46        span.set_tag(|| Tag::new("status", tag_value));
47    }
48
49    /// Set a lock status for a custom `NoCacheReason`.
50    fn custom_lock_status(&self, _custom_no_cache: &'static str) -> LockStatus {
51        // treat custom no cache reasons as GiveUp by default
52        // (like OriginNotCache)
53        LockStatus::GiveUp
54    }
55}
56
57const N_SHARDS: usize = 16;
58
59/// The global cache locking manager
60#[derive(Debug)]
61pub struct CacheLock {
62    lock_table: ConcurrentHashTable<LockStub, N_SHARDS>,
63    // fixed lock timeout values for now
64    age_timeout_default: Duration,
65}
66
67/// A struct representing locked cache access
68#[derive(Debug)]
69pub enum Locked {
70    /// The writer is allowed to fetch the asset
71    Write(WritePermit),
72    /// The reader waits for the writer to fetch the asset
73    Read(ReadLock),
74}
75
76impl Locked {
77    /// Is this a write lock
78    pub fn is_write(&self) -> bool {
79        matches!(self, Self::Write(_))
80    }
81}
82
83impl CacheLock {
84    /// Create a new [CacheLock] with the given lock timeout
85    ///
86    /// Age timeout refers to how long a writer has been holding onto a particular lock, and wait
87    /// timeout refers to how long a reader may hold onto any number of locks before giving up.
88    /// When either timeout is reached, the read locks are automatically unlocked.
89    pub fn new_boxed(age_timeout: Duration) -> Box<Self> {
90        Box::new(CacheLock {
91            lock_table: ConcurrentHashTable::new(),
92            age_timeout_default: age_timeout,
93        })
94    }
95
96    /// Create a new [CacheLock] with the given lock timeout
97    ///
98    /// Age timeout refers to how long a writer has been holding onto a particular lock, and wait
99    /// timeout refers to how long a reader may hold onto any number of locks before giving up.
100    /// When either timeout is reached, the read locks are automatically unlocked.
101    pub fn new(age_timeout_default: Duration) -> Self {
102        CacheLock {
103            lock_table: ConcurrentHashTable::new(),
104            age_timeout_default,
105        }
106    }
107}
108
109impl CacheKeyLock for CacheLock {
110    fn lock(&self, key: &CacheKey, stale_writer: bool) -> Locked {
111        let hash = key.combined_bin();
112        let key = u128::from_be_bytes(hash); // endianness doesn't matter
113        let table = self.lock_table.get(key);
114        if let Some(lock) = table.read().get(&key) {
115            // already has an ongoing request
116            // If the lock status is dangling or timeout, the lock will _remain_ in the table
117            // and readers should attempt to replace it.
118            // In the case of writer timeout, any remaining readers that were waiting on THIS
119            // LockCore should have (or are about to) timed out on their own.
120            // Finding a Timeout status means that THIS writer's lock already expired, so future
121            // requests ought to recreate the lock.
122            if !matches!(
123                lock.0.lock_status(),
124                LockStatus::Dangling | LockStatus::AgeTimeout
125            ) {
126                return Locked::Read(lock.read_lock());
127            }
128            // Dangling: the previous writer quit without unlocking the lock. Requests should
129            // compete for the write lock again.
130        }
131
132        let mut table = table.write();
133        // check again in case another request already added it
134        if let Some(lock) = table.get(&key) {
135            if !matches!(
136                lock.0.lock_status(),
137                LockStatus::Dangling | LockStatus::AgeTimeout
138            ) {
139                return Locked::Read(lock.read_lock());
140            }
141        }
142        let (permit, stub) =
143            WritePermit::new(self.age_timeout_default, stale_writer, Extensions::new());
144        table.insert(key, stub);
145        Locked::Write(permit)
146    }
147
148    fn release(&self, key: &CacheKey, mut permit: WritePermit, reason: LockStatus) {
149        let hash = key.combined_bin();
150        let key = u128::from_be_bytes(hash); // endianness doesn't matter
151        if permit.lock.lock_status() == LockStatus::AgeTimeout {
152            // if lock age timed out, then readers are capable of
153            // replacing the lock associated with this permit from the lock table
154            // (see lock() implementation)
155            // keep the lock status as Timeout accordingly when unlocking
156            // (because we aren't removing it from the lock_table)
157            permit.unlock(LockStatus::AgeTimeout);
158        } else if let Some(_lock) = self.lock_table.write(key).remove(&key) {
159            permit.unlock(reason);
160        }
161        // these situations above should capture all possible options,
162        // else dangling cache lock may start
163    }
164}
165
166use log::warn;
167use parking_lot::Mutex;
168use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
169use std::time::Instant;
170use strum::{FromRepr, IntoStaticStr};
171use tokio::sync::{oneshot, Semaphore};
172
173/// Status which the read locks could possibly see.
174#[derive(Debug, Copy, Clone, PartialEq, Eq, IntoStaticStr, FromRepr)]
175#[repr(u8)]
176pub enum LockStatus {
177    /// Waiting for the writer to populate the asset
178    Waiting = 0,
179    /// The writer finishes, readers can start
180    Done = 1,
181    /// The writer encountered error, such as network issue. A new writer will be elected.
182    TransientError = 2,
183    /// The writer observed that no cache lock is needed (e.g., uncacheable), readers should start
184    /// to fetch independently without a new writer
185    GiveUp = 3,
186    /// The write lock is dropped without being unlocked
187    Dangling = 4,
188    /// Reader has held onto cache locks for too long, give up
189    WaitTimeout = 5,
190    /// The lock is held for too long by the writer
191    AgeTimeout = 6,
192}
193
194impl From<LockStatus> for u8 {
195    fn from(l: LockStatus) -> u8 {
196        match l {
197            LockStatus::Waiting => 0,
198            LockStatus::Done => 1,
199            LockStatus::TransientError => 2,
200            LockStatus::GiveUp => 3,
201            LockStatus::Dangling => 4,
202            LockStatus::WaitTimeout => 5,
203            LockStatus::AgeTimeout => 6,
204        }
205    }
206}
207
208impl From<u8> for LockStatus {
209    fn from(v: u8) -> Self {
210        Self::from_repr(v).unwrap_or(Self::GiveUp)
211    }
212}
213
214#[derive(Debug)]
215pub struct LockCore {
216    pub lock_start: Instant,
217    pub age_timeout: Duration,
218    pub(super) lock: Semaphore,
219    // use u8 for Atomic enum
220    lock_status: AtomicU8,
221    stale_writer: bool,
222    extensions: Extensions,
223    /// What the writer has said about its fill, and who is waiting to hear it. One
224    /// mutex for both, so nothing publishes in the gap between a reader checking and
225    /// registering.
226    fill: Mutex<FillState>,
227    /// Lets `publish` skip the lock when replacing nothing with nothing. Written
228    /// under `fill` so it cannot disagree with `published`; a stale `false` read only
229    /// means a racing publication.
230    published_any: AtomicBool,
231}
232
233#[derive(Debug, Default)]
234struct FillState {
235    /// Replaced, not accumulated: see [`WritePermit::publish`].
236    published: Vec<u64>,
237    /// Readers to be told about a later publication, in no meaningful order. Nobody
238    /// removes themselves: a departure is a closed sender, swept by whoever next
239    /// holds this lock.
240    waiters: Vec<TokenWaiter>,
241    /// Where the next sweep resumes. Sweeps examine a bounded window, so the cursor
242    /// advances past survivors and wraps to stop entries hiding behind live ones.
243    sweep_cursor: usize,
244}
245
246/// Entries examined per registration. The vector settles near
247/// `live * BUDGET / (BUDGET - 1)`.
248const WAITER_SWEEP_BUDGET: usize = 8;
249
250#[derive(Debug)]
251struct TokenWaiter {
252    /// Shared with the reader's [`UnusableFills`], so registering is a refcount bump.
253    unusable: Arc<[UnusableFill]>,
254    /// Also the liveness signal: closed once the reader drops its receiver.
255    tell: oneshot::Sender<UnusableFill>,
256}
257
258impl LockCore {
259    pub fn new_arc(timeout: Duration, stale_writer: bool, extensions: Extensions) -> Arc<Self> {
260        Arc::new(LockCore {
261            lock: Semaphore::new(0),
262            age_timeout: timeout,
263            lock_start: Instant::now(),
264            lock_status: AtomicU8::new(LockStatus::Waiting.into()),
265            stale_writer,
266            extensions,
267            fill: Mutex::new(FillState::default()),
268            published_any: AtomicBool::new(false),
269        })
270    }
271
272    pub fn locked(&self) -> bool {
273        self.lock.available_permits() == 0
274    }
275
276    /// Check what is published, and if none of it matters, register for later
277    /// publications. One lock for both, or a publication lands in the gap.
278    fn watch_fill(&self, unusable: &Arc<[UnusableFill]>) -> Watching {
279        let mut fill = self.fill.lock();
280        if let Some(matched) = first_match(&fill.published, unusable) {
281            return Watching::AlreadyPublished(matched);
282        }
283
284        // On the lock this registration takes anyway, and bounded so one
285        // registration cannot end up scanning every reader ahead of it.
286        let mut budget = WAITER_SWEEP_BUDGET;
287        while budget > 0 && !fill.waiters.is_empty() {
288            if fill.sweep_cursor >= fill.waiters.len() {
289                fill.sweep_cursor = 0;
290            }
291            let at = fill.sweep_cursor;
292            if fill.waiters[at].tell.is_closed() {
293                // Cursor stays: `swap_remove` moves an unexamined entry here.
294                fill.waiters.swap_remove(at);
295            } else {
296                fill.sweep_cursor += 1;
297            }
298            budget -= 1;
299        }
300
301        let (tell, told) = oneshot::channel();
302        fill.waiters.push(TokenWaiter {
303            unusable: unusable.clone(),
304            tell,
305        });
306        Watching::Registered(told)
307    }
308
309    /// Replace what is published and wake only the readers it matters to.
310    fn publish(&self, tokens: &[u64]) {
311        let woken = {
312            let mut fill = self.fill.lock();
313            fill.published.clear();
314            fill.published.extend_from_slice(tokens);
315            // Same lock as `published`, so racing publications cannot disagree.
316            self.published_any
317                .store(!tokens.is_empty(), Ordering::Relaxed);
318
319            // One pass in place: departed readers dropped, matched readers taken out
320            // to be told once the lock is released. An empty `woken` never allocates.
321            let mut woken = Vec::new();
322            let mut i = 0;
323            while i < fill.waiters.len() {
324                if fill.waiters[i].tell.is_closed() {
325                    fill.waiters.swap_remove(i);
326                    continue;
327                }
328                match first_match(tokens, &fill.waiters[i].unusable) {
329                    Some(matched) => woken.push((fill.waiters.swap_remove(i).tell, matched)),
330                    None => i += 1,
331                }
332            }
333            woken
334        };
335
336        // Sent after releasing the lock.
337        for (tell, matched) in woken {
338            let _ = tell.send(matched);
339        }
340    }
341
342    /// Live readers only, so an assertion does not depend on when a sweep last ran.
343    #[cfg(test)]
344    fn registered_waiters(&self) -> usize {
345        self.fill
346            .lock()
347            .waiters
348            .iter()
349            .filter(|waiter| !waiter.tell.is_closed())
350            .count()
351    }
352
353    /// Entries held, swept or not. Only the sweep bound should assert on this.
354    #[cfg(test)]
355    fn retained_waiters(&self) -> usize {
356        self.fill.lock().waiters.len()
357    }
358
359    pub fn unlock(&self, reason: LockStatus) {
360        assert!(
361            reason != LockStatus::WaitTimeout,
362            "WaitTimeout is not stored in LockCore"
363        );
364        self.lock_status.store(reason.into(), Ordering::SeqCst);
365        // Any small positive number will do, 10 is used for RwLock as well.
366        // No need to wake up all at once.
367        self.lock.add_permits(10);
368    }
369
370    pub fn lock_status(&self) -> LockStatus {
371        self.lock_status.load(Ordering::SeqCst).into()
372    }
373
374    /// Was this lock for a stale cache fetch writer?
375    pub fn stale_writer(&self) -> bool {
376        self.stale_writer
377    }
378
379    pub fn extensions(&self) -> &Extensions {
380        &self.extensions
381    }
382}
383
384// all 3 structs below are just Arc<LockCore> with different interfaces
385
386/// ReadLock: the requests who get it need to wait until it is released
387#[derive(Debug)]
388pub struct ReadLock(Arc<LockCore>);
389
390/// One fill a reader cannot use, and why. Per token rather than per set, because a
391/// key has one [`UnusableFills`] and a set-wide reason would attribute one part of
392/// an application's give-up to another's cause.
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394pub struct UnusableFill {
395    /// Names a fill this reader cannot use. Opaque to the cache: whatever the
396    /// writer and reader agree it means.
397    pub token: u64,
398    /// Why, as the [`crate::NoCacheReason::Custom`] label the cache disables with.
399    /// Not a whole [`crate::NoCacheReason`]: the cause belongs to the application,
400    /// and a caller could otherwise pass [`crate::NoCacheReason::NeverEnabled`],
401    /// which [`crate::HttpCache::disable`] rejects.
402    pub reason: &'static str,
403}
404
405/// The first published fill this reader cannot use, in the *reader's* order. Both
406/// sides are a handful of tokens, so a plain scan.
407fn first_match(published: &[u64], unusable: &[UnusableFill]) -> Option<UnusableFill> {
408    unusable
409        .iter()
410        .find(|candidate| published.contains(&candidate.token))
411        .copied()
412}
413
414/// What happened to *this* reader's cache-lock wait, and so what to do next.
415///
416/// Distinct from [`LockStatus`], the lock's *shared* state, which cannot carry a
417/// payload. This is per-reader and never stored, which is what lets
418/// [`Self::Abandoned`] carry its cause.
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub enum LockWaitOutcome {
421    /// The writer populated the asset. Look it up again.
422    Done,
423    /// The writer hit a transient error such as a network failure. Compete to be
424    /// the new writer.
425    TransientError,
426    /// The write lock was dropped without being unlocked. A bug, but the request
427    /// recovers by competing to be the new writer.
428    Dangling,
429    /// This reader has spent too long waiting on cache locks.
430    WaitTimeout,
431    /// The writer held the lock past its age. Compete for it rather than waiting,
432    /// so a stuck writer cannot pin every reader to it.
433    AgeTimeout,
434    /// The writer found no lock was needed, for instance because the asset turned
435    /// out to be uncacheable. Every reader proceeds uncached.
436    GiveUp,
437    /// This reader stopped waiting because the writer published a fill it cannot
438    /// use. Local to this reader: the lock status is untouched and every other
439    /// reader goes on coalescing behind the same writer.
440    Abandoned {
441        /// Why, from the matched [`UnusableFill`].
442        reason: NoCacheReason,
443        /// The published token it matched.
444        token: u64,
445    },
446}
447
448impl LockWaitOutcome {
449    /// The nearest shared-status label, for tracing and metrics only.
450    /// [`Self::Abandoned`] reports [`LockStatus::GiveUp`] because it shares that
451    /// handling, but is never *stored* as such.
452    pub fn lock_status(&self) -> LockStatus {
453        match self {
454            LockWaitOutcome::Done => LockStatus::Done,
455            LockWaitOutcome::TransientError => LockStatus::TransientError,
456            LockWaitOutcome::Dangling => LockStatus::Dangling,
457            LockWaitOutcome::WaitTimeout => LockStatus::WaitTimeout,
458            LockWaitOutcome::AgeTimeout => LockStatus::AgeTimeout,
459            LockWaitOutcome::GiveUp | LockWaitOutcome::Abandoned { .. } => LockStatus::GiveUp,
460        }
461    }
462}
463
464/// The fills a reader cannot use. Put one in a [`crate::CacheKey`]'s extensions
465/// and [`crate::HttpCache::cache_lock_wait`] honours it. Empty waits as normal.
466///
467/// One set and one publisher per key: extensions hold one value per type, and each
468/// [`WritePermit::publish`] supersedes the last, so two features on a key must agree
469/// on a combined set. Distinct token ranges stop tokens colliding but the space is
470/// still flat and shared.
471///
472/// A match costs the reader its caching -- [`LockWaitOutcome::Abandoned`] means an
473/// uncached fetch, where [`LockWaitOutcome::AgeTimeout`] recompetes and can still
474/// cache. Name only fills that genuinely cannot be waited for.
475#[derive(Debug, Clone)]
476pub struct UnusableFills {
477    pub(crate) fills: Arc<[UnusableFill]>,
478}
479
480impl UnusableFills {
481    /// `fills` in precedence order: if several are published at once, the earliest
482    /// here is reported back.
483    pub fn new(fills: impl Into<Arc<[UnusableFill]>>) -> Self {
484        UnusableFills {
485            fills: fills.into(),
486        }
487    }
488
489    /// The fills this reader cannot use, in precedence order.
490    pub fn fills(&self) -> &[UnusableFill] {
491        &self.fills
492    }
493
494    /// Which of `published` this reader cannot use, or `None` to keep waiting. The
495    /// same function the wait uses, so a caller can check its token convention
496    /// against the real rule rather than a copy.
497    pub fn first_match(&self, published: &[u64]) -> Option<UnusableFill> {
498        first_match(published, &self.fills)
499    }
500}
501
502/// How a wait behind a writer ended.
503#[derive(Debug, Clone, Copy, PartialEq, Eq)]
504pub enum WaitOutcome {
505    /// The writer released the lock. Read [`ReadLock::lock_status`] for what it left
506    /// behind.
507    Released,
508    /// The writer held the lock for longer than its age allows.
509    AgeTimeout,
510    /// The writer published a fill this reader cannot use.
511    Abandoned(UnusableFill),
512}
513
514enum Watching {
515    Registered(oneshot::Receiver<UnusableFill>),
516    AlreadyPublished(UnusableFill),
517}
518
519impl ReadLock {
520    /// Wait for the writer to release the lock
521    pub async fn wait(&self) {
522        self.wait_inner(None).await;
523    }
524
525    /// Wait for the writer to release the lock, unless it publishes a fill in
526    /// `unusable` first — which lets a reader act on what the writer only learns
527    /// after the reader committed to waiting.
528    ///
529    /// Giving up is local to this reader: the lock status is untouched and every
530    /// other reader goes on coalescing. Tokens never published never match.
531    pub async fn wait_unless_published(&self, unusable: &UnusableFills) -> WaitOutcome {
532        self.wait_inner(Some(&unusable.fills)).await
533    }
534
535    async fn wait_inner(&self, unusable: Option<&Arc<[UnusableFill]>>) -> WaitOutcome {
536        if !self.locked() {
537            return WaitOutcome::Released;
538        }
539
540        // FIXME: for now it is the awkward responsibility of the ReadLock to set the
541        // timeout status on the lock itself because the write permit cannot lock age
542        // timeout on its own
543        // TODO: need to be careful not to wake everyone up at the same time
544        // (maybe not an issue because regular cache lock release behaves that way)
545        //
546        // Checked before any published token: an expired lock has to be replaced
547        // whatever the writer said, and the caller can then recompete and still
548        // cache. Also the only path that stores `AgeTimeout`.
549        let Some(duration) = self.0.age_timeout.checked_sub(self.0.lock_start.elapsed()) else {
550            // expiration has already occurred, store timeout status
551            self.0
552                .lock_status
553                .store(LockStatus::AgeTimeout.into(), Ordering::SeqCst);
554            return WaitOutcome::AgeTimeout;
555        };
556
557        // Naming nothing is the same as no interest: an empty set matches no token,
558        // so registering would buy a wakeup nothing could deliver.
559        let unusable = unusable.filter(|unusable| !unusable.is_empty());
560
561        let told = match unusable {
562            // The ordinary case: nothing named, so nothing to register or select on.
563            None => {
564                return Self::writer_done(&self.0, duration).await;
565            }
566            Some(unusable) => match self.0.watch_fill(unusable) {
567                // Already true on arrival; no later publication would repeat it.
568                Watching::AlreadyPublished(matched) => {
569                    // The writer can release between the check above and here, and
570                    // releasing does not clear what it published. Let the release
571                    // win, as the `biased` select below does: the asset is in cache,
572                    // so abandoning would fetch it uncached for nothing.
573                    if !self.locked() {
574                        return WaitOutcome::Released;
575                    }
576                    return WaitOutcome::Abandoned(matched);
577                }
578                Watching::Registered(told) => told,
579            },
580        };
581        let told_to_stop = async {
582            match told.await {
583                Ok(matched) => WaitOutcome::Abandoned(matched),
584                // Unreachable: the registry parts with a sender only by sending to
585                // it, or by sweeping one already closed, which this cannot be while
586                // this await holds its receiver. So this is a bug in this file
587                // rather than a race a caller can provoke.
588                //
589                // `Released` rather than a panic in release builds: the caller then
590                // takes the dangling-lock path, recompetes, and can still cache.
591                Err(_) => {
592                    debug_assert!(false, "fill waiter sender dropped while registered");
593                    WaitOutcome::Released
594                }
595            }
596        };
597
598        // Biased so a writer that finishes at the same moment wins: completing the
599        // wait normally is always at least as good as giving up on it.
600        tokio::select! {
601            biased;
602            outcome = Self::writer_done(&self.0, duration) => outcome,
603            outcome = told_to_stop => outcome,
604        }
605    }
606
607    /// Wait out the writer, or the lock's remaining age.
608    async fn writer_done(core: &LockCore, duration: Duration) -> WaitOutcome {
609        match timeout(duration, core.lock.acquire()).await {
610            Ok(Ok(_)) => {
611                // permit is returned to Semaphore right away
612                WaitOutcome::Released
613            }
614            Ok(Err(e)) => {
615                warn!("error acquiring semaphore {e:?}");
616                WaitOutcome::Released
617            }
618            Err(_) => {
619                core.lock_status
620                    .store(LockStatus::AgeTimeout.into(), Ordering::SeqCst);
621                WaitOutcome::AgeTimeout
622            }
623        }
624    }
625
626    /// Test if it is still locked
627    pub fn locked(&self) -> bool {
628        self.0.locked()
629    }
630
631    /// Whether the lock is expired, e.g., the writer has been holding the lock for too long
632    pub fn expired(&self) -> bool {
633        // NOTE: this is whether the lock is currently expired
634        // not whether it was timed out during wait()
635        self.0.lock_start.elapsed() >= self.0.age_timeout
636    }
637
638    /// The current status of the lock
639    pub fn lock_status(&self) -> LockStatus {
640        let status = self.0.lock_status();
641        if matches!(status, LockStatus::Waiting) && self.expired() {
642            LockStatus::AgeTimeout
643        } else {
644            status
645        }
646    }
647
648    pub fn extensions(&self) -> &Extensions {
649        self.0.extensions()
650    }
651}
652
653/// WritePermit: requires who get it need to populate the cache and then release it
654#[derive(Debug)]
655pub struct WritePermit {
656    lock: Arc<LockCore>,
657    finished: bool,
658}
659
660impl WritePermit {
661    /// Create a new lock, with a permit to be given to the associated writer.
662    pub fn new(
663        timeout: Duration,
664        stale_writer: bool,
665        extensions: Extensions,
666    ) -> (WritePermit, LockStub) {
667        let lock = LockCore::new_arc(timeout, stale_writer, extensions);
668        let stub = LockStub(lock.clone());
669        (
670            WritePermit {
671                lock,
672                finished: false,
673            },
674            stub,
675        )
676    }
677
678    /// Was this lock for a stale cache fetch writer?
679    pub fn stale_writer(&self) -> bool {
680        self.lock.stale_writer()
681    }
682
683    pub fn unlock(&mut self, reason: LockStatus) {
684        self.finished = true;
685        self.lock.unlock(reason);
686    }
687
688    pub fn lock_status(&self) -> LockStatus {
689        self.lock.lock_status()
690    }
691
692    pub fn extensions(&self) -> &Extensions {
693        self.lock.extensions()
694    }
695
696    /// Describe what this fill involves, so readers that cannot use it stop
697    /// waiting. Tokens are opaque: writer and readers need only agree on meaning.
698    ///
699    /// Each call **replaces** the previous description, since a writer that moved
700    /// on is no longer doing what it said and a reader giving up over an
701    /// abandoned attempt would lose its caching for nothing. Empty describes
702    /// nothing. Only matching readers are woken.
703    pub fn publish(&self, tokens: &[u64]) {
704        // Most writers never publish anything, and this is called on every
705        // upstream request. Replacing nothing with nothing cannot wake a reader,
706        // since an empty set matches no token, so it does not need the lock.
707        // Racing readers are unaffected either way: one that registers around
708        // this point takes the lock, sees nothing published, and waits.
709        if tokens.is_empty() && !self.lock.published_any.load(Ordering::Relaxed) {
710            return;
711        }
712        self.lock.publish(tokens);
713    }
714}
715
716impl Drop for WritePermit {
717    fn drop(&mut self) {
718        // Writer exited without properly unlocking. We let others to compete for the write lock again
719        if !self.finished {
720            debug_assert!(false, "Dangling cache lock started!");
721            self.unlock(LockStatus::Dangling);
722        }
723    }
724}
725
726#[derive(Debug)]
727pub struct LockStub(pub Arc<LockCore>);
728impl LockStub {
729    pub fn read_lock(&self) -> ReadLock {
730        ReadLock(self.0.clone())
731    }
732
733    pub fn extensions(&self) -> &Extensions {
734        &self.0.extensions
735    }
736}
737
738#[cfg(test)]
739mod test {
740    use super::*;
741    use crate::CacheKey;
742
743    const WRONG_PLACE: u64 = 7;
744    const SOMEWHERE_ELSE: u64 = 9;
745
746    /// Reasons are the application's; the cache never interprets them.
747    const NO_GOOD: &str = "NoGoodToThisReader";
748
749    /// A reader that cannot use `token`, for the usual single-reason case.
750    fn cannot_use(token: u64) -> [UnusableFill; 1] {
751        [UnusableFill {
752            token,
753            reason: NO_GOOD,
754        }]
755    }
756
757    fn new_lock(age: Duration) -> (WritePermit, LockStub) {
758        WritePermit::new(age, false, Extensions::new())
759    }
760
761    fn reader(stub: &LockStub) -> ReadLock {
762        ReadLock(stub.0.clone())
763    }
764
765    /// Wait for readers to register, giving up with a useful message rather than
766    /// spinning forever. An unbounded spin turns a registration regression into a
767    /// CI job timeout that says nothing about what broke.
768    async fn registered(stub: &LockStub, want: usize) {
769        for _ in 0..1_000 {
770            if stub.0.registered_waiters() >= want {
771                return;
772            }
773            tokio::task::yield_now().await;
774        }
775        panic!(
776            "expected {want} registered waiter(s), found {}",
777            stub.0.registered_waiters()
778        );
779    }
780
781    /// Wraps a future to count how many times it is polled, which is how a test
782    /// can tell a reader was never woken rather than merely still waiting.
783    struct CountPolls {
784        inner: std::pin::Pin<Box<dyn std::future::Future<Output = WaitOutcome> + Send>>,
785        polls: Arc<std::sync::atomic::AtomicUsize>,
786    }
787
788    impl std::future::Future for CountPolls {
789        type Output = WaitOutcome;
790
791        fn poll(
792            self: std::pin::Pin<&mut Self>,
793            cx: &mut std::task::Context<'_>,
794        ) -> std::task::Poll<WaitOutcome> {
795            let this = self.get_mut();
796            this.polls.fetch_add(1, Ordering::Relaxed);
797            this.inner.as_mut().poll(cx)
798        }
799    }
800
801    /// Only the matching reader is woken -- the other is not even polled. Waking
802    /// everyone to self-test would look the same from the outside.
803    #[tokio::test]
804    async fn only_the_reader_whose_tokens_match_is_woken() {
805        let (permit, stub) = new_lock(Duration::from_secs(30));
806        let polls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
807
808        let leaves = reader(&stub);
809        let left = tokio::spawn(async move {
810            leaves
811                .wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
812                .await
813        });
814        let stays = reader(&stub);
815        let stayed = tokio::spawn(CountPolls {
816            inner: Box::pin(async move {
817                stays
818                    .wait_unless_published(&UnusableFills::new(cannot_use(SOMEWHERE_ELSE)))
819                    .await
820            }),
821            polls: polls.clone(),
822        });
823        registered(&stub, 2).await;
824        let polled_before = polls.load(Ordering::Relaxed);
825
826        permit.publish(&[WRONG_PLACE]);
827
828        assert_eq!(
829            left.await.unwrap(),
830            WaitOutcome::Abandoned(UnusableFill {
831                token: WRONG_PLACE,
832                reason: NO_GOOD,
833            })
834        );
835        assert!(!stayed.is_finished(), "the other reader still coalesces");
836        assert_eq!(
837            polls.load(Ordering::Relaxed),
838            polled_before,
839            "and was never woken by a publication that does not concern it"
840        );
841        assert_eq!(
842            stub.0.registered_waiters(),
843            1,
844            "the reader that left is no longer registered"
845        );
846        assert_eq!(
847            stub.0.lock_status(),
848            LockStatus::Waiting,
849            "giving up does not touch the shared status"
850        );
851
852        let mut permit = permit;
853        permit.unlock(LockStatus::Done);
854        assert_eq!(stayed.await.unwrap(), WaitOutcome::Released);
855        assert!(
856            polls.load(Ordering::Relaxed) > polled_before,
857            "it is woken when the writer actually releases"
858        );
859    }
860
861    /// A reader that names no tokens never registers, so no amount of publishing
862    /// can wake it.
863    #[tokio::test]
864    async fn a_reader_that_names_no_tokens_is_never_registered() {
865        let (permit, stub) = new_lock(Duration::from_secs(30));
866        let core = reader(&stub);
867        let waiting = tokio::spawn(async move { core.wait().await });
868        tokio::task::yield_now().await;
869
870        assert_eq!(stub.0.registered_waiters(), 0);
871        permit.publish(&[WRONG_PLACE, SOMEWHERE_ELSE]);
872        tokio::task::yield_now().await;
873        assert!(!waiting.is_finished());
874        assert_eq!(stub.0.registered_waiters(), 0);
875
876        let mut permit = permit;
877        permit.unlock(LockStatus::Done);
878        waiting.await.unwrap();
879    }
880
881    /// What was published before a reader arrived still reaches it. No later
882    /// publication is coming to tell it.
883    #[tokio::test]
884    async fn tokens_published_before_the_reader_arrives_are_not_missed() {
885        let (permit, stub) = new_lock(Duration::from_secs(30));
886        permit.publish(&[WRONG_PLACE]); // nobody is waiting yet
887
888        let outcome = reader(&stub)
889            .wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
890            .await;
891
892        assert_eq!(
893            outcome,
894            WaitOutcome::Abandoned(UnusableFill {
895                token: WRONG_PLACE,
896                reason: NO_GOOD,
897            })
898        );
899        assert_eq!(stub.0.registered_waiters(), 0, "and it did not register");
900
901        let mut permit = permit;
902        permit.unlock(LockStatus::Done);
903    }
904
905    /// Publishing replaces, so a reader is not stopped over an attempt the writer
906    /// has already moved on from, which would cost its caching for nothing.
907    #[tokio::test]
908    async fn a_reader_is_not_stopped_by_a_superseded_publication() {
909        let (permit, stub) = new_lock(Duration::from_secs(30));
910        permit.publish(&[WRONG_PLACE]);
911        permit.publish(&[SOMEWHERE_ELSE]); // the first attempt is done with
912
913        let stays = reader(&stub);
914        let stayed = tokio::spawn(async move {
915            stays
916                .wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
917                .await
918        });
919        registered(&stub, 1).await;
920        assert!(!stayed.is_finished(), "the old attempt no longer matters");
921
922        // And a reader already waiting is stopped if the writer moves back.
923        permit.publish(&[WRONG_PLACE]);
924        assert_eq!(
925            stayed.await.unwrap(),
926            WaitOutcome::Abandoned(UnusableFill {
927                token: WRONG_PLACE,
928                reason: NO_GOOD,
929            })
930        );
931
932        let mut permit = permit;
933        permit.unlock(LockStatus::Done);
934    }
935
936    /// Cancelling the wait leaves nothing for a publication to match. Cancellation,
937    /// not an ordinary return -- what `HttpCache::cache_lock_wait` does whenever its
938    /// own `wait_timeout` fires.
939    ///
940    /// The entry itself outlives the cancellation, since nobody deregisters, so the
941    /// reclaiming is asserted through a publication that does *not* name this
942    /// reader's token: matching would remove the entry too, and then this would pass
943    /// without the closed-sender sweep it is here to cover.
944    #[tokio::test]
945    async fn cancelling_the_wait_leaves_nothing_to_match() {
946        let (mut permit, stub) = new_lock(Duration::from_secs(30));
947
948        let waits = reader(&stub);
949        let interest = UnusableFills::new(cannot_use(WRONG_PLACE));
950        // Boxed rather than `tokio::pin!`, which shadows the future with a
951        // `Pin<&mut _>`; dropping that drops a reference and the future would
952        // outlive the assertion below, passing for the wrong reason.
953        let mut wait = Box::pin(waits.wait_unless_published(&interest));
954
955        assert!(
956            futures::poll!(wait.as_mut()).is_pending(),
957            "the writer still holds the lock"
958        );
959        assert_eq!(stub.0.registered_waiters(), 1);
960        assert_eq!(stub.0.retained_waiters(), 1);
961
962        drop(wait);
963        assert_eq!(
964            stub.0.registered_waiters(),
965            0,
966            "a cancelled reader must not be left for the writer to test"
967        );
968        assert_eq!(
969            stub.0.retained_waiters(),
970            1,
971            "though the entry is still there until something sweeps it"
972        );
973
974        permit.publish(&[SOMEWHERE_ELSE]);
975        assert_eq!(
976            stub.0.retained_waiters(),
977            0,
978            "and a publication sweeps it even without matching it"
979        );
980
981        permit.unlock(LockStatus::Done);
982    }
983
984    /// Nobody deregisters, so later registrations sweep what earlier readers left.
985    /// Without that a long fill accumulates an entry per reader that ever waited.
986    #[tokio::test]
987    async fn departed_readers_are_swept_by_later_registrations() {
988        let (mut permit, stub) = new_lock(Duration::from_secs(30));
989        let interest = UnusableFills::new(cannot_use(WRONG_PLACE));
990
991        for _ in 0..200 {
992            let waits = reader(&stub);
993            let mut wait = Box::pin(waits.wait_unless_published(&interest));
994            assert!(
995                futures::poll!(wait.as_mut()).is_pending(),
996                "the writer still holds the lock"
997            );
998            drop(wait);
999        }
1000
1001        assert_eq!(
1002            stub.0.registered_waiters(),
1003            0,
1004            "every one of those readers has gone"
1005        );
1006        assert!(
1007            stub.0.retained_waiters() <= WAITER_SWEEP_BUDGET,
1008            "swept back down, found {}",
1009            stub.0.retained_waiters()
1010        );
1011
1012        permit.unlock(LockStatus::Done);
1013    }
1014
1015    /// A sweep is bounded, so the cursor is what stops departed readers hiding
1016    /// behind live ones: restarting at the front would examine a window of live
1017    /// readers and reclaim nothing, forever.
1018    #[tokio::test]
1019    async fn departed_readers_behind_live_ones_are_still_reclaimed() {
1020        let (mut permit, stub) = new_lock(Duration::from_secs(30));
1021        let interest = UnusableFills::new(cannot_use(WRONG_PLACE));
1022
1023        // One `ReadLock` shared by every wait below, so the futures that stay
1024        // registered can outlive the loop that made them. Each call registers
1025        // separately regardless of which handle it came from.
1026        let waits = reader(&stub);
1027
1028        // Enough to fill a sweep window and then some, all of them staying.
1029        let mut live = Vec::new();
1030        for _ in 0..(WAITER_SWEEP_BUDGET * 2) {
1031            let mut wait = Box::pin(waits.wait_unless_published(&interest));
1032            assert!(futures::poll!(wait.as_mut()).is_pending());
1033            live.push(wait);
1034        }
1035        assert_eq!(stub.0.registered_waiters(), live.len());
1036
1037        // Then readers that come and go behind them.
1038        let departed = WAITER_SWEEP_BUDGET * 4;
1039        for _ in 0..departed {
1040            let mut wait = Box::pin(waits.wait_unless_published(&interest));
1041            assert!(futures::poll!(wait.as_mut()).is_pending());
1042            drop(wait);
1043        }
1044
1045        assert_eq!(
1046            stub.0.registered_waiters(),
1047            live.len(),
1048            "the readers that stayed are all still registered"
1049        );
1050        assert!(
1051            stub.0.retained_waiters() <= live.len() + WAITER_SWEEP_BUDGET,
1052            "departed readers must be reclaimed despite never being at the front, \
1053             leaving an overhead set by the sweep budget rather than by how many \
1054             readers have come and gone: found {} entries for {} live readers",
1055            stub.0.retained_waiters(),
1056            live.len()
1057        );
1058
1059        drop(live);
1060        permit.unlock(LockStatus::Done);
1061    }
1062
1063    /// The same through an outer timeout, which is the shape
1064    /// `HttpCache::cache_lock_wait` actually uses: a `wait_timeout` shorter than
1065    /// the lock age, cutting the wait short while it is still registered.
1066    #[tokio::test]
1067    async fn a_wait_cut_short_by_an_outer_timeout_leaves_nothing_to_match() {
1068        let (mut permit, stub) = new_lock(Duration::from_secs(30));
1069
1070        let waits = reader(&stub);
1071        let cut_short = tokio::time::timeout(
1072            Duration::from_millis(10),
1073            waits.wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE))),
1074        )
1075        .await;
1076
1077        assert!(cut_short.is_err(), "the writer never released");
1078        assert_eq!(
1079            stub.0.registered_waiters(),
1080            0,
1081            "nothing is left for a publication to match"
1082        );
1083        assert_eq!(
1084            stub.0.retained_waiters(),
1085            1,
1086            "the entry outlives the wait, to be swept later"
1087        );
1088
1089        permit.unlock(LockStatus::Done);
1090    }
1091
1092    /// Naming nothing cannot be matched, so it should cost no registration.
1093    /// Asserted on the registry, since never-registering and never-matching look the
1094    /// same from outside.
1095    #[tokio::test]
1096    async fn an_empty_interest_registers_no_waiter() {
1097        let (mut permit, stub) = new_lock(Duration::from_secs(30));
1098
1099        let waits = reader(&stub);
1100        let waited =
1101            tokio::spawn(async move { waits.wait_unless_published(&UnusableFills::new([])).await });
1102
1103        // A second reader that does name something gives a deterministic point to
1104        // wait for. Once it has registered the runtime has run both, so an absent
1105        // registration is absence rather than lateness -- which a fixed number of
1106        // yields cannot distinguish.
1107        let names_something = reader(&stub);
1108        let named = tokio::spawn(async move {
1109            names_something
1110                .wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
1111                .await
1112        });
1113        registered(&stub, 1).await;
1114
1115        assert_eq!(
1116            stub.0.registered_waiters(),
1117            1,
1118            "an interest that names nothing has nothing to be told about"
1119        );
1120        drop(named);
1121
1122        // Still an ordinary wait: released by the writer, like a reader with no
1123        // interest at all.
1124        permit.unlock(LockStatus::Done);
1125        assert_eq!(waited.await.unwrap(), WaitOutcome::Released);
1126    }
1127
1128    /// Publishing nothing clears what came before, so a later reader waits rather
1129    /// than giving up over a fill that is no longer happening. Skipping empty
1130    /// publications outright would strand the stale token.
1131    #[tokio::test]
1132    async fn publishing_nothing_clears_a_previous_publication() {
1133        let (permit, stub) = new_lock(Duration::from_secs(30));
1134        permit.publish(&[WRONG_PLACE]);
1135        permit.publish(&[]); // retried onto the origin; no longer in any cycle
1136
1137        // Asserted directly rather than by spinning on a registration that a
1138        // stranded token would prevent, so this fails instead of hanging.
1139        assert_eq!(
1140            first_match(&stub.0.fill.lock().published, &cannot_use(WRONG_PLACE)),
1141            None,
1142            "the abandoned attempt no longer matters"
1143        );
1144
1145        let stays = reader(&stub);
1146        let stayed = tokio::spawn(async move {
1147            stays
1148                .wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
1149                .await
1150        });
1151
1152        let mut permit = permit;
1153        permit.unlock(LockStatus::Done);
1154        assert_eq!(stayed.await.unwrap(), WaitOutcome::Released);
1155    }
1156
1157    /// A reader whose tokens are never published waits for the writer as usual,
1158    /// and leaves no registration behind for the writer to keep testing.
1159    #[tokio::test]
1160    async fn a_reader_that_times_out_reports_the_age_timeout() {
1161        let (permit, stub) = new_lock(Duration::from_millis(50));
1162
1163        let outcome = reader(&stub)
1164            .wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
1165            .await;
1166
1167        assert_eq!(outcome, WaitOutcome::AgeTimeout);
1168
1169        let mut permit = permit;
1170        permit.unlock(LockStatus::Done);
1171    }
1172
1173    #[test]
1174    fn test_get_release() {
1175        let cache_lock = CacheLock::new_boxed(Duration::from_secs(1000));
1176        let key1 = CacheKey::new("a", "1");
1177        let locked1 = cache_lock.lock(&key1, false);
1178        assert!(locked1.is_write()); // write permit
1179        let locked2 = cache_lock.lock(&key1, false);
1180        assert!(!locked2.is_write()); // read lock
1181        if let Locked::Write(permit) = locked1 {
1182            cache_lock.release(&key1, permit, LockStatus::Done);
1183        }
1184        let locked3 = cache_lock.lock(&key1, false);
1185        assert!(locked3.is_write()); // write permit again
1186        if let Locked::Write(permit) = locked3 {
1187            cache_lock.release(&key1, permit, LockStatus::Done);
1188        }
1189    }
1190
1191    #[tokio::test]
1192    async fn test_lock() {
1193        let cache_lock = CacheLock::new_boxed(Duration::from_secs(1000));
1194        let key1 = CacheKey::new("a", "1");
1195        let mut permit = match cache_lock.lock(&key1, false) {
1196            Locked::Write(w) => w,
1197            _ => panic!(),
1198        };
1199        let lock = match cache_lock.lock(&key1, false) {
1200            Locked::Read(r) => r,
1201            _ => panic!(),
1202        };
1203        assert!(lock.locked());
1204        let handle = tokio::spawn(async move {
1205            lock.wait().await;
1206            assert_eq!(lock.lock_status(), LockStatus::Done);
1207        });
1208        permit.unlock(LockStatus::Done);
1209        handle.await.unwrap(); // check lock is unlocked and the task is returned
1210    }
1211
1212    #[tokio::test]
1213    async fn test_lock_timeout() {
1214        let cache_lock = CacheLock::new_boxed(Duration::from_secs(1));
1215        let key1 = CacheKey::new("a", "1");
1216        let mut permit = match cache_lock.lock(&key1, false) {
1217            Locked::Write(w) => w,
1218            _ => panic!(),
1219        };
1220        let lock = match cache_lock.lock(&key1, false) {
1221            Locked::Read(r) => r,
1222            _ => panic!(),
1223        };
1224        assert!(lock.locked());
1225
1226        let handle = tokio::spawn(async move {
1227            // timed out
1228            lock.wait().await;
1229            assert_eq!(lock.lock_status(), LockStatus::AgeTimeout);
1230        });
1231
1232        tokio::time::sleep(Duration::from_millis(2100)).await;
1233
1234        handle.await.unwrap(); // check lock is timed out
1235
1236        // expired lock - we will be able to install a new lock instead
1237        let mut permit2 = match cache_lock.lock(&key1, false) {
1238            Locked::Write(w) => w,
1239            _ => panic!(),
1240        };
1241        let lock2 = match cache_lock.lock(&key1, false) {
1242            Locked::Read(r) => r,
1243            _ => panic!(),
1244        };
1245        assert!(lock2.locked());
1246        let handle = tokio::spawn(async move {
1247            // timed out
1248            lock2.wait().await;
1249            assert_eq!(lock2.lock_status(), LockStatus::Done);
1250        });
1251
1252        permit.unlock(LockStatus::Done);
1253        permit2.unlock(LockStatus::Done);
1254        handle.await.unwrap();
1255    }
1256
1257    #[tokio::test]
1258    async fn test_lock_expired_release() {
1259        let cache_lock = CacheLock::new_boxed(Duration::from_secs(1));
1260        let key1 = CacheKey::new("a", "1");
1261        let permit = match cache_lock.lock(&key1, false) {
1262            Locked::Write(w) => w,
1263            _ => panic!(),
1264        };
1265
1266        let lock = match cache_lock.lock(&key1, false) {
1267            Locked::Read(r) => r,
1268            _ => panic!(),
1269        };
1270        assert!(lock.locked());
1271        let handle = tokio::spawn(async move {
1272            // timed out
1273            lock.wait().await;
1274            assert_eq!(lock.lock_status(), LockStatus::AgeTimeout);
1275        });
1276
1277        tokio::time::sleep(Duration::from_millis(1100)).await; // let lock age time out
1278        handle.await.unwrap(); // check lock is timed out
1279
1280        // writer finally finishes
1281        cache_lock.release(&key1, permit, LockStatus::Done);
1282
1283        // can reacquire after release
1284        let mut permit = match cache_lock.lock(&key1, false) {
1285            Locked::Write(w) => w,
1286            _ => panic!(),
1287        };
1288        assert_eq!(permit.lock.lock_status(), LockStatus::Waiting);
1289
1290        let lock2 = match cache_lock.lock(&key1, false) {
1291            Locked::Read(r) => r,
1292            _ => panic!(),
1293        };
1294        assert!(lock2.locked());
1295        let handle = tokio::spawn(async move {
1296            // timed out
1297            lock2.wait().await;
1298            assert_eq!(lock2.lock_status(), LockStatus::Done);
1299        });
1300
1301        permit.unlock(LockStatus::Done);
1302        handle.await.unwrap();
1303    }
1304
1305    #[tokio::test]
1306    async fn test_lock_expired_no_reader() {
1307        let cache_lock = CacheLock::new_boxed(Duration::from_secs(1));
1308        let key1 = CacheKey::new("a", "1");
1309        let mut permit = match cache_lock.lock(&key1, false) {
1310            Locked::Write(w) => w,
1311            _ => panic!(),
1312        };
1313        tokio::time::sleep(Duration::from_millis(1100)).await; // let lock age time out
1314
1315        // lock expired without reader, but status is not yet set
1316        assert_eq!(permit.lock.lock_status(), LockStatus::Waiting);
1317
1318        let lock = match cache_lock.lock(&key1, false) {
1319            Locked::Read(r) => r,
1320            _ => panic!(),
1321        };
1322        // reader expires write permit
1323        lock.wait().await;
1324        assert_eq!(lock.lock_status(), LockStatus::AgeTimeout);
1325        assert_eq!(permit.lock.lock_status(), LockStatus::AgeTimeout);
1326        permit.unlock(LockStatus::AgeTimeout);
1327    }
1328
1329    #[tokio::test]
1330    async fn test_lock_concurrent() {
1331        let _ = env_logger::builder().is_test(true).try_init();
1332        // Test that concurrent attempts to compete for a lock run without issues
1333        let cache_lock = Arc::new(CacheLock::new_boxed(Duration::from_secs(1)));
1334        let key1 = CacheKey::new("a", "1");
1335
1336        let mut handles = vec![];
1337
1338        const READERS: usize = 30;
1339        for _ in 0..READERS {
1340            let key1 = key1.clone();
1341            let cache_lock = cache_lock.clone();
1342            // simulate a cache lookup / lock attempt loop
1343            handles.push(tokio::spawn(async move {
1344                // timed out
1345                loop {
1346                    match cache_lock.lock(&key1, false) {
1347                        Locked::Write(permit) => {
1348                            let _ = tokio::time::sleep(Duration::from_millis(5)).await;
1349                            cache_lock.release(&key1, permit, LockStatus::Done);
1350                            break;
1351                        }
1352                        Locked::Read(r) => {
1353                            r.wait().await;
1354                        }
1355                    }
1356                }
1357            }));
1358        }
1359
1360        for handle in handles {
1361            handle.await.unwrap();
1362        }
1363    }
1364
1365    /// An expired lock is replaced rather than abandoned, even with an unusable
1366    /// token already published -- both are true at once for the case this feature
1367    /// exists for, since a stuck owner holds its lock for the full age. Recompeting
1368    /// can still cache, where abandoning could not.
1369    #[tokio::test]
1370    async fn an_expired_lock_times_out_rather_than_abandoning() {
1371        let (mut permit, stub) = new_lock(Duration::from_millis(10));
1372        stub.0.publish(&[WRONG_PLACE]);
1373        tokio::time::sleep(Duration::from_millis(30)).await;
1374
1375        let outcome = reader(&stub)
1376            .wait_unless_published(&UnusableFills::new(cannot_use(WRONG_PLACE)))
1377            .await;
1378
1379        assert_eq!(
1380            outcome,
1381            WaitOutcome::AgeTimeout,
1382            "expiry is decided before any published token is consulted"
1383        );
1384        assert_eq!(
1385            stub.0.lock_status(),
1386            LockStatus::AgeTimeout,
1387            "the dead lock must not be left reading Waiting"
1388        );
1389
1390        permit.unlock(LockStatus::Done);
1391    }
1392}