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}