Skip to main content

shardline_cache/memory/
mod.rs

1use std::{
2    future::Future,
3    num::{NonZeroU64, NonZeroUsize},
4    sync::Arc,
5    time::Duration,
6};
7
8use tokio::sync::{Notify, RwLock};
9use tokio::time::Instant;
10
11use crate::{
12    AsyncReconstructionCache, ReconstructionCacheError, ReconstructionCacheFuture,
13    ReconstructionCacheKey,
14};
15
16mod inner;
17
18#[cfg(test)]
19mod tests;
20
21use inner::{CacheInner, MemoryEntry};
22
23/// Bounded in-memory reconstruction cache adapter.
24#[derive(Debug, Clone)]
25pub struct MemoryReconstructionCache {
26    ttl: Duration,
27    max_entries: NonZeroUsize,
28    inner: Arc<RwLock<CacheInner>>,
29}
30
31impl MemoryReconstructionCache {
32    /// Creates a bounded in-memory reconstruction cache.
33    #[must_use]
34    pub fn new(ttl_seconds: NonZeroU64, max_entries: NonZeroUsize) -> Self {
35        Self {
36            ttl: Duration::from_secs(ttl_seconds.get()),
37            max_entries,
38            inner: Arc::new(RwLock::new(CacheInner::new())),
39        }
40    }
41
42    /// Returns the cached value for `key`, or computes it with `loader`.
43    ///
44    /// Concurrent calls for the same key are deduplicated — only one caller
45    /// runs the loader; the rest await the result.
46    ///
47    /// # Errors
48    ///
49    /// Returns [`ReconstructionCacheError`] when the loader fails or the
50    /// underlying cache operation encounters an error.
51    pub async fn get_or_load<F, Fut>(
52        &self,
53        key: &ReconstructionCacheKey,
54        loader: F,
55    ) -> Result<Option<Vec<u8>>, ReconstructionCacheError>
56    where
57        F: FnOnce() -> Fut,
58        Fut: Future<Output = Result<Vec<u8>, ReconstructionCacheError>>,
59    {
60        // Fast path: check the cache without a write lock.
61        {
62            let inner = self.inner.read().await;
63            let now = Instant::now();
64            if let Some(entry) = inner.entries.get(key)
65                && entry.expires_at > now
66            {
67                return Ok(Some(entry.payload.as_ref().clone()));
68            }
69        }
70
71        // Try to become the exclusive loader for this key.
72        let (should_load, notify) = {
73            let mut inner = self.inner.write().await;
74            let now = Instant::now();
75
76            // Re-check after acquiring the write lock.
77            if let Some(entry) = inner.entries.get(key)
78                && entry.expires_at > now
79            {
80                return Ok(Some(entry.payload.as_ref().clone()));
81            }
82
83            // Check if someone else is already loading this key.
84            // Use separate branches on .is_some() to avoid the clippy
85            // option_if_let_else lint (which flags both `if let Some`
86            // and `match { Some, None }` on Option).
87            let (should_load, notify) = if inner.loading.contains_key(key) {
88                // SAFETY: contains_key was just checked — get() returns Some.
89                // Use unwrap_or_else with a non-panicking default to avoid
90                // denied lints while satisfying the type system.
91                let dummy = Arc::new(Notify::new());
92                let existing = inner.loading.get(key).unwrap_or(&dummy);
93                (false, Arc::clone(existing))
94            } else {
95                let new_notify = Arc::new(Notify::new());
96                inner.loading.insert(key.clone(), Arc::clone(&new_notify));
97                // Clean up any expired entry so the loader can store fresh data.
98                if let Some(entry) = inner.entries.get(key)
99                    && entry.expires_at <= now
100                {
101                    inner.remove(key);
102                }
103                (true, new_notify)
104            };
105            (should_load, notify)
106        };
107
108        if should_load {
109            let result = loader().await;
110            match result {
111                Ok(payload) => {
112                    self.put(key, &payload).await?;
113                    Ok(Some(payload))
114                }
115                Err(e) => {
116                    // Clean up the loading entry so future callers can retry.
117                    let mut inner = self.inner.write().await;
118                    inner.loading.remove(key);
119                    notify.notify_waiters();
120                    Err(e)
121                }
122            }
123        } else {
124            // Someone else is loading — wait for them.
125            notify.notified().await;
126            let inner = self.inner.read().await;
127            let now = Instant::now();
128            if let Some(entry) = inner.entries.get(key)
129                && entry.expires_at > now
130            {
131                Ok(Some(entry.payload.as_ref().clone()))
132            } else {
133                Ok(None)
134            }
135        }
136    }
137}
138
139impl AsyncReconstructionCache for MemoryReconstructionCache {
140    fn ready(&self) -> ReconstructionCacheFuture<'_, ()> {
141        Box::pin(async { Ok(()) })
142    }
143
144    fn get<'operation>(
145        &'operation self,
146        key: &'operation ReconstructionCacheKey,
147    ) -> ReconstructionCacheFuture<'operation, Option<Vec<u8>>> {
148        Box::pin(async move {
149            let now = Instant::now();
150            {
151                let inner = self.inner.read().await;
152                if let Some(entry) = inner.entries.get(key)
153                    && entry.expires_at > now
154                {
155                    return Ok(Some(entry.payload.as_ref().clone()));
156                } else if !inner.loading.contains_key(key) {
157                    return Ok(None);
158                }
159            }
160
161            let mut inner = self.inner.write().await;
162
163            if let Some(entry) = inner.entries.get(key)
164                && entry.expires_at > now
165            {
166                return Ok(Some(entry.payload.as_ref().clone()));
167            }
168
169            if let Some(notify) = inner.loading.get(key) {
170                let notify = Arc::clone(notify);
171                drop(inner);
172                // If the loader fails, the Notify is never fired and
173                // subsequent get() calls for this key would hang forever.
174                // Use a timeout so we can clean up the orphaned loading
175                // entry and let the next caller retry.
176                tokio::select! {
177                    () = notify.notified() => {}
178                    () = tokio::time::sleep(Duration::from_secs(30)) => {
179                        let mut write_guard = self.inner.write().await;
180                        write_guard.loading.remove(key);
181                        return Ok(None);
182                    }
183                }
184
185                let read_inner = self.inner.read().await;
186                if let Some(entry) = read_inner.entries.get(key)
187                    && entry.expires_at > Instant::now()
188                {
189                    return Ok(Some(entry.payload.as_ref().clone()));
190                }
191                return Ok(None);
192            }
193
194            let notify = Arc::new(Notify::new());
195            inner.loading.insert(key.clone(), Arc::clone(&notify));
196
197            let should_remove = inner
198                .entries
199                .get(key)
200                .is_some_and(|entry| entry.expires_at <= now);
201            if should_remove {
202                inner.remove(key);
203            }
204            Ok(None)
205        })
206    }
207
208    fn put<'operation>(
209        &'operation self,
210        key: &'operation ReconstructionCacheKey,
211        payload: &'operation [u8],
212    ) -> ReconstructionCacheFuture<'operation, ()> {
213        Box::pin(async move {
214            let now = Instant::now();
215            let expires_at = now.checked_add(self.ttl).unwrap_or(now);
216            let mut inner = self.inner.write().await;
217            if !inner.entries.contains_key(key) && inner.entries.len() >= self.max_entries.get() {
218                inner.evict_oldest();
219            }
220            let seq = inner.next_seq;
221            inner.next_seq = inner.next_seq.saturating_add(1);
222            inner.insert(
223                key,
224                MemoryEntry {
225                    payload: Arc::new(payload.to_vec()),
226                    expires_at,
227                    inserted_at: now,
228                    seq,
229                },
230            );
231            if let Some(notify) = inner.loading.remove(key) {
232                notify.notify_waiters();
233            }
234            Ok(())
235        })
236    }
237
238    fn delete<'operation>(
239        &'operation self,
240        key: &'operation ReconstructionCacheKey,
241    ) -> ReconstructionCacheFuture<'operation, bool> {
242        Box::pin(async move {
243            let mut inner = self.inner.write().await;
244            Ok(inner.remove(key).is_some())
245        })
246    }
247}