Skip to main content

dialcache/
local.rs

1//! Process-local storage.
2
3use std::any::Any;
4use std::num::NonZeroUsize;
5use std::sync::Arc;
6
7use crate::error::BoxError;
8
9/// A type-erased cached value shared by reference.
10pub type StoredValue = Arc<dyn Any + Send + Sync>;
11
12/// One process-local entry with its insertion-time TTL.
13#[derive(Clone)]
14pub struct LocalEntry {
15    /// The cached value, shared by reference with every reader.
16    pub value: StoredValue,
17    /// Whole-millisecond elapsed reading at insertion.
18    pub inserted_ms: i64,
19    /// Lifetime in whole milliseconds; the entry is expired once
20    /// `now_ms - inserted_ms >= ttl_ms`.
21    pub ttl_ms: i64,
22}
23
24/// Process-local storage for one cache instance.
25///
26/// The default [`LruLocalStore`] evicts the least recently used entry at
27/// capacity regardless of its remaining TTL, checks expiry lazily on read
28/// against whole elapsed milliseconds, promotes on read and write, and never
29/// renews a TTL on read. Implementations must preserve those rules.
30pub trait LocalStore: Send + 'static {
31    /// Read one key. A live entry is promoted and returned; an expired entry
32    /// is removed without promotion and handed back so the cache can drop it
33    /// outside its lock.
34    fn get(&mut self, key: &str, now_ms: i64) -> Result<LocalRead, BoxError>;
35    /// Insert or replace an entry, promoting it and evicting at capacity.
36    /// Returns the displaced entry (the replaced value or the evicted tail),
37    /// which the cache drops outside its lock.
38    fn put(&mut self, key: String, entry: LocalEntry) -> Result<Option<LocalEntry>, BoxError>;
39}
40
41/// The outcome of one [`LocalStore::get`].
42///
43/// Removed entries travel back to the cache instead of dropping inside the
44/// store, because a value's destructor may call back into the cache and the
45/// store runs under the cache's lock.
46#[derive(Debug)]
47pub enum LocalRead {
48    /// No entry under the key.
49    Absent,
50    /// The entry had expired and was removed.
51    Expired(LocalEntry),
52    /// A live entry, promoted.
53    Live(StoredValue),
54}
55
56/// The default LRU store.
57pub struct LruLocalStore {
58    entries: lru::LruCache<String, LocalEntry>,
59}
60
61impl LruLocalStore {
62    /// A store holding at most `capacity` entries. Capacity must be positive;
63    /// storage is allocated as entries arrive rather than for the full limit.
64    pub fn new(capacity: NonZeroUsize) -> Self {
65        LruLocalStore {
66            entries: lru::LruCache::sparse(capacity),
67        }
68    }
69
70    /// Entries currently held, expired ones included until a read removes them.
71    pub fn len(&self) -> usize {
72        self.entries.len()
73    }
74
75    /// Whether no entries are held.
76    pub fn is_empty(&self) -> bool {
77        self.entries.is_empty()
78    }
79}
80
81impl LocalStore for LruLocalStore {
82    fn get(&mut self, key: &str, now_ms: i64) -> Result<LocalRead, BoxError> {
83        // Peek, check freshness, then promote: an expired entry leaves the LRU
84        // order untouched apart from its own removal.
85        let expired = match self.entries.peek(key) {
86            None => return Ok(LocalRead::Absent),
87            Some(entry) => now_ms.saturating_sub(entry.inserted_ms) >= entry.ttl_ms,
88        };
89        if expired {
90            return Ok(match self.entries.pop(key) {
91                Some(entry) => LocalRead::Expired(entry),
92                None => LocalRead::Absent,
93            });
94        }
95        Ok(match self.entries.get(key) {
96            Some(entry) => LocalRead::Live(entry.value.clone()),
97            None => LocalRead::Absent,
98        })
99    }
100
101    fn put(&mut self, key: String, entry: LocalEntry) -> Result<Option<LocalEntry>, BoxError> {
102        Ok(self
103            .entries
104            .push(key, entry)
105            .map(|(_, displaced)| displaced))
106    }
107}
108
109impl std::fmt::Debug for LocalEntry {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_struct("LocalEntry")
112            .field("inserted_ms", &self.inserted_ms)
113            .field("ttl_ms", &self.ttl_ms)
114            .finish_non_exhaustive()
115    }
116}
117
118impl std::fmt::Debug for LruLocalStore {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("LruLocalStore")
121            .field("len", &self.entries.len())
122            .field("cap", &self.entries.cap())
123            .finish()
124    }
125}