Skip to main content

sigstore_cache/
memory.rs

1//! In-memory cache implementation with TTL support
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use tokio::sync::RwLock;
8
9use crate::{CacheAdapter, CacheKey};
10
11/// A cached entry with expiration time
12#[derive(Debug, Clone)]
13struct CacheEntry {
14    /// The cached data
15    data: Vec<u8>,
16    /// When this entry expires
17    expires_at: Instant,
18}
19
20impl CacheEntry {
21    fn is_expired(&self) -> bool {
22        Instant::now() >= self.expires_at
23    }
24}
25
26/// In-memory cache with TTL support
27///
28/// This cache stores values in memory with automatic expiration.
29/// It's fast but not persistent across process restarts.
30///
31/// Thread-safe and suitable for use across async tasks.
32///
33/// # Example
34///
35/// ```
36/// use sigstore_cache::{InMemoryCache, CacheAdapter, CacheKey};
37/// use std::time::Duration;
38///
39/// # async fn example() -> Result<(), sigstore_cache::Error> {
40/// let cache = InMemoryCache::new();
41///
42/// // Cache a value for 1 hour
43/// cache.set(
44///     CacheKey::RekorPublicKey,
45///     b"public-key-data",
46///     Duration::from_secs(3600)
47/// ).await?;
48///
49/// // Retrieve it
50/// if let Some(data) = cache.get(CacheKey::RekorPublicKey).await? {
51///     println!("Got {} bytes", data.len());
52/// }
53/// # Ok(())
54/// # }
55/// ```
56#[derive(Debug, Clone)]
57pub struct InMemoryCache {
58    /// The actual cache storage
59    entries: Arc<RwLock<HashMap<CacheKey, CacheEntry>>>,
60}
61
62impl Default for InMemoryCache {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl InMemoryCache {
69    /// Create a new empty in-memory cache
70    pub fn new() -> Self {
71        Self {
72            entries: Arc::new(RwLock::new(HashMap::new())),
73        }
74    }
75
76    /// Remove expired entries from the cache
77    ///
78    /// This is called automatically on `get` operations, but can be
79    /// called manually to proactively clean up memory.
80    pub async fn cleanup_expired(&self) {
81        let mut entries = self.entries.write().await;
82        entries.retain(|_, entry| !entry.is_expired());
83    }
84
85    /// Get the number of entries in the cache (including expired ones)
86    pub async fn len(&self) -> usize {
87        self.entries.read().await.len()
88    }
89
90    /// Check if the cache is empty
91    pub async fn is_empty(&self) -> bool {
92        self.entries.read().await.is_empty()
93    }
94}
95
96impl CacheAdapter for InMemoryCache {
97    fn get(&self, key: CacheKey) -> crate::CacheGetFuture<'_> {
98        Box::pin(async move {
99            let entries = self.entries.read().await;
100
101            match entries.get(&key) {
102                Some(entry) if !entry.is_expired() => Ok(Some(entry.data.clone())),
103                Some(_) => {
104                    // Entry exists but is expired - clean it up
105                    drop(entries);
106                    let mut entries = self.entries.write().await;
107                    entries.remove(&key);
108                    Ok(None)
109                }
110                None => Ok(None),
111            }
112        })
113    }
114
115    fn set(&self, key: CacheKey, value: &[u8], ttl: Duration) -> crate::CacheOpFuture<'_> {
116        let value = value.to_vec();
117        Box::pin(async move {
118            let entry = CacheEntry {
119                data: value,
120                expires_at: Instant::now() + ttl,
121            };
122
123            let mut entries = self.entries.write().await;
124            entries.insert(key, entry);
125
126            Ok(())
127        })
128    }
129
130    fn remove(&self, key: CacheKey) -> crate::CacheOpFuture<'_> {
131        Box::pin(async move {
132            let mut entries = self.entries.write().await;
133            entries.remove(&key);
134            Ok(())
135        })
136    }
137
138    fn clear(&self) -> crate::CacheOpFuture<'_> {
139        Box::pin(async move {
140            let mut entries = self.entries.write().await;
141            entries.clear();
142            Ok(())
143        })
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[tokio::test]
152    async fn test_memory_cache_roundtrip() {
153        let cache = InMemoryCache::new();
154        let key = CacheKey::RekorPublicKey;
155        let value = b"test-data";
156
157        // Initially empty
158        assert!(cache.get(key).await.unwrap().is_none());
159
160        // Set and get
161        cache
162            .set(key, value, Duration::from_secs(3600))
163            .await
164            .unwrap();
165        let retrieved = cache.get(key).await.unwrap().unwrap();
166        assert_eq!(retrieved, value);
167
168        // Remove
169        cache.remove(key).await.unwrap();
170        assert!(cache.get(key).await.unwrap().is_none());
171    }
172
173    #[tokio::test]
174    async fn test_memory_cache_expiration() {
175        let cache = InMemoryCache::new();
176        let key = CacheKey::FulcioConfiguration;
177        let value = b"test-config";
178
179        // Set with very short TTL
180        cache
181            .set(key, value, Duration::from_millis(10))
182            .await
183            .unwrap();
184
185        // Should exist immediately
186        assert!(cache.get(key).await.unwrap().is_some());
187
188        // Wait for expiration
189        tokio::time::sleep(Duration::from_millis(20)).await;
190
191        // Should be expired now
192        assert!(cache.get(key).await.unwrap().is_none());
193    }
194
195    #[tokio::test]
196    async fn test_memory_cache_clear() {
197        let cache = InMemoryCache::new();
198
199        cache
200            .set(CacheKey::RekorPublicKey, b"a", Duration::from_secs(3600))
201            .await
202            .unwrap();
203        cache
204            .set(CacheKey::FulcioTrustBundle, b"b", Duration::from_secs(3600))
205            .await
206            .unwrap();
207
208        assert_eq!(cache.len().await, 2);
209
210        cache.clear().await.unwrap();
211
212        assert!(cache.is_empty().await);
213    }
214
215    #[tokio::test]
216    async fn test_memory_cache_cleanup_expired() {
217        let cache = InMemoryCache::new();
218
219        // Add some entries with different TTLs
220        cache
221            .set(
222                CacheKey::RekorPublicKey,
223                b"long-lived",
224                Duration::from_secs(3600),
225            )
226            .await
227            .unwrap();
228        cache
229            .set(
230                CacheKey::FulcioTrustBundle,
231                b"short-lived",
232                Duration::from_millis(10),
233            )
234            .await
235            .unwrap();
236
237        assert_eq!(cache.len().await, 2);
238
239        // Wait for short-lived to expire
240        tokio::time::sleep(Duration::from_millis(20)).await;
241
242        // Cleanup
243        cache.cleanup_expired().await;
244
245        // Only long-lived should remain
246        assert_eq!(cache.len().await, 1);
247        assert!(cache.get(CacheKey::RekorPublicKey).await.unwrap().is_some());
248        assert!(cache
249            .get(CacheKey::FulcioTrustBundle)
250            .await
251            .unwrap()
252            .is_none());
253    }
254}