1#![forbid(unsafe_code)]
7
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11use dashmap::DashMap;
12use serde_json::Value;
13
14#[derive(Debug, Clone)]
17struct Entry {
18 value: Value,
19 inserted_at: Instant,
20 ttl: Duration,
21}
22
23impl Entry {
24 fn is_expired(&self) -> bool {
25 self.inserted_at.elapsed() > self.ttl
26 }
27}
28
29#[derive(Debug, Clone)]
33pub struct CacheConfig {
34 pub ttl: Duration,
36 pub max_entries: usize,
38}
39
40impl Default for CacheConfig {
41 fn default() -> Self {
42 Self {
43 ttl: Duration::from_secs(300), max_entries: 1_000,
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
55pub struct MemoryCache {
56 store: Arc<DashMap<String, Entry>>,
57 config: CacheConfig,
58}
59
60impl MemoryCache {
61 pub fn new() -> Self {
63 Self::with_config(CacheConfig::default())
64 }
65
66 pub fn with_config(config: CacheConfig) -> Self {
68 Self {
69 store: Arc::new(DashMap::new()),
70 config,
71 }
72 }
73
74 pub fn get(&self, key: &str) -> Option<Value> {
76 let entry = self.store.get(key)?;
77 if entry.is_expired() {
78 drop(entry);
79 self.store.remove(key);
80 return None;
81 }
82 Some(entry.value.clone())
83 }
84
85 pub fn set(&self, key: String, value: Value) {
87 self.set_with_ttl(key, value, self.config.ttl);
88 }
89
90 pub fn set_with_ttl(&self, key: String, value: Value, ttl: Duration) {
92 if self.store.len() >= self.config.max_entries {
93 self.evict_oldest();
94 }
95
96 self.store.insert(
97 key,
98 Entry {
99 value,
100 inserted_at: Instant::now(),
101 ttl,
102 },
103 );
104 }
105
106 pub fn clear(&self) {
108 self.store.clear();
109 }
110
111 pub fn len(&self) -> usize {
113 self.store.len()
114 }
115
116 pub fn is_empty(&self) -> bool {
118 self.store.is_empty()
119 }
120
121 pub fn evict_expired(&self) {
123 self.store.retain(|_, entry| !entry.is_expired());
124 }
125
126 fn evict_oldest(&self) {
127 let oldest_key = self
128 .store
129 .iter()
130 .min_by_key(|entry| entry.value().inserted_at)
131 .map(|entry| entry.key().clone());
132
133 if let Some(key) = oldest_key {
134 self.store.remove(&key);
135 }
136 }
137}
138
139impl Default for MemoryCache {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145#[cfg(test)]
148mod tests {
149 use super::*;
150 use serde_json::json;
151
152 #[test]
153 fn basic_get_set() {
154 let cache = MemoryCache::new();
155 assert!(cache.get("https://rdap.example.com/domain/foo").is_none());
156
157 cache.set(
158 "https://rdap.example.com/domain/foo".to_string(),
159 json!({ "ldhName": "foo.example" }),
160 );
161
162 assert!(cache.get("https://rdap.example.com/domain/foo").is_some());
163 }
164
165 #[test]
166 fn expired_entry_is_evicted() {
167 let cache = MemoryCache::with_config(CacheConfig {
168 ttl: Duration::from_millis(1),
169 max_entries: 100,
170 });
171
172 cache.set("key".to_string(), json!({}));
173 std::thread::sleep(Duration::from_millis(5));
174 assert!(cache.get("key").is_none());
175 }
176
177 #[test]
178 fn max_entries_evicts_oldest() {
179 let cache = MemoryCache::with_config(CacheConfig {
180 ttl: Duration::from_secs(60),
181 max_entries: 2,
182 });
183
184 cache.set("a".to_string(), json!(1));
185 cache.set("b".to_string(), json!(2));
186 cache.set("c".to_string(), json!(3));
187
188 assert_eq!(cache.len(), 2);
189 assert!(cache.get("a").is_none());
190 }
191
192 #[test]
193 fn clear_empties_cache() {
194 let cache = MemoryCache::new();
195 cache.set("x".to_string(), json!({}));
196 cache.clear();
197 assert!(cache.is_empty());
198 }
199}