pas_external/epoch/
ttl_cache.rs1use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::Duration;
6
7use async_trait::async_trait;
8use ppoppo_clock::ArcClock;
9use ppoppo_clock::native::WallClock;
10use tokio::sync::RwLock;
11
12use super::Cache;
13
14pub struct InProcessTtlCache {
45 inner: RwLock<HashMap<String, (i64, i64)>>,
46 ttl: Duration,
47 clock: ArcClock,
48}
49
50impl InProcessTtlCache {
51 #[must_use]
55 pub fn new(ttl: Duration) -> Self {
56 Self {
57 inner: RwLock::new(HashMap::new()),
58 ttl,
59 clock: Arc::new(WallClock),
60 }
61 }
62
63 #[must_use]
64 pub fn with_clock(mut self, clock: ArcClock) -> Self {
65 self.clock = clock;
66 self
67 }
68}
69
70impl std::fmt::Debug for InProcessTtlCache {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("InProcessTtlCache")
73 .field("ttl", &self.ttl)
74 .finish_non_exhaustive()
75 }
76}
77
78#[async_trait]
79impl Cache for InProcessTtlCache {
80 async fn get(&self, key: &str) -> Option<i64> {
81 let now_ms = self.clock.now_unix_millis();
82 let ttl_ms = self.ttl.as_millis() as i64;
83 {
87 let read = self.inner.read().await;
88 if let Some((sv, inserted_at_ms)) = read.get(key) {
89 if now_ms - inserted_at_ms < ttl_ms {
90 return Some(*sv);
91 }
92 } else {
93 return None;
94 }
95 }
96
97 let mut write = self.inner.write().await;
99 if let Some((_, inserted_at_ms)) = write.get(key)
100 && now_ms - inserted_at_ms >= ttl_ms {
101 write.remove(key);
102 }
103 None
104 }
105
106 async fn set(&self, key: &str, sv: i64, _ttl: Duration) {
107 let mut write = self.inner.write().await;
112 write.insert(key.to_string(), (sv, self.clock.now_unix_millis()));
113 }
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used)]
118mod tests {
119 use super::*;
120
121 #[tokio::test]
122 async fn miss_returns_none() {
123 let cache = InProcessTtlCache::new(Duration::from_secs(60));
124 assert_eq!(cache.get("sv:nonexistent").await, None);
125 }
126
127 #[tokio::test]
128 async fn hit_after_set() {
129 let cache = InProcessTtlCache::new(Duration::from_secs(60));
130 cache.set("sv:abc", 7, Duration::from_secs(60)).await;
131 assert_eq!(cache.get("sv:abc").await, Some(7));
132 }
133
134 #[tokio::test]
135 async fn expired_entry_evicts_on_access() {
136 let cache = InProcessTtlCache::new(Duration::from_millis(10));
137 cache.set("sv:abc", 5, Duration::from_millis(10)).await;
138 assert_eq!(cache.get("sv:abc").await, Some(5));
139 tokio::time::sleep(Duration::from_millis(25)).await;
140 assert_eq!(cache.get("sv:abc").await, None);
141
142 cache.set("sv:abc", 11, Duration::from_secs(60)).await;
145 assert_eq!(cache.get("sv:abc").await, Some(11));
146 }
147
148 #[tokio::test]
149 async fn set_overwrites_existing_entry() {
150 let cache = InProcessTtlCache::new(Duration::from_secs(60));
151 cache.set("sv:abc", 7, Duration::from_secs(60)).await;
152 cache.set("sv:abc", 8, Duration::from_secs(60)).await;
153 assert_eq!(cache.get("sv:abc").await, Some(8));
154 }
155
156 #[tokio::test]
157 async fn distinct_keys_dont_collide() {
158 let cache = InProcessTtlCache::new(Duration::from_secs(60));
159 cache.set("sv:alpha", 1, Duration::from_secs(60)).await;
160 cache.set("sv:beta", 2, Duration::from_secs(60)).await;
161 assert_eq!(cache.get("sv:alpha").await, Some(1));
162 assert_eq!(cache.get("sv:beta").await, Some(2));
163 }
164}