1use std::{
2 collections::HashMap,
3 sync::Arc,
4 time::{Duration, Instant},
5};
6
7use super::RoleClient;
8use crate::{
9 model::CacheScope,
10 service::{Peer, ServiceRole},
11};
12
13pub const MAX_CLIENT_CACHE_TTL: Duration = Duration::from_secs(24 * 60 * 60);
15
16#[derive(Debug, Clone, PartialEq, Eq)]
23#[non_exhaustive]
24pub struct ClientCacheConfig {
25 pub enabled: bool,
27 pub default_ttl: Duration,
31 pub max_ttl: Duration,
33 pub private_partition: Option<String>,
40 pub max_entries: usize,
44 pub serve_stale_on_error: bool,
52}
53
54impl Default for ClientCacheConfig {
55 fn default() -> Self {
56 Self {
57 enabled: true,
58 default_ttl: Duration::ZERO,
59 max_ttl: MAX_CLIENT_CACHE_TTL,
60 private_partition: None,
61 max_entries: 512,
62 serve_stale_on_error: true,
63 }
64 }
65}
66
67impl ClientCacheConfig {
68 pub fn disabled() -> Self {
70 Self {
71 enabled: false,
72 ..Self::default()
73 }
74 }
75
76 pub fn with_default_ttl(mut self, default_ttl: Duration) -> Self {
78 self.default_ttl = default_ttl;
79 self
80 }
81
82 pub fn with_max_ttl(mut self, max_ttl: Duration) -> Self {
84 self.max_ttl = max_ttl;
85 self
86 }
87
88 pub fn with_private_partition(mut self, partition: impl Into<String>) -> Self {
90 self.private_partition = Some(partition.into());
91 self
92 }
93
94 pub fn with_max_entries(mut self, max_entries: usize) -> Self {
96 self.max_entries = max_entries;
97 self
98 }
99
100 pub fn with_serve_stale_on_error(mut self, serve_stale_on_error: bool) -> Self {
102 self.serve_stale_on_error = serve_stale_on_error;
103 self
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Hash)]
108enum CachePartition {
109 Public,
110 Private(Arc<str>),
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
114struct CacheKey {
115 logical_key: String,
116 partition: CachePartition,
117}
118
119#[derive(Debug, Clone)]
120struct CachedPeerResponse<T> {
121 value: T,
122 expires_at: Instant,
123 inserted_at: Instant,
124 scope: CacheScope,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub(crate) struct CacheGeneration(u64);
129
130#[derive(Debug)]
131pub(crate) struct PeerResponseCacheState<R: ServiceRole> {
132 entries: HashMap<CacheKey, CachedPeerResponse<R::PeerResp>>,
133 config: ClientCacheConfig,
134 generation: u64,
135}
136
137impl<R: ServiceRole> Default for PeerResponseCacheState<R> {
138 fn default() -> Self {
139 Self {
140 entries: HashMap::new(),
141 config: ClientCacheConfig::default(),
142 generation: 0,
143 }
144 }
145}
146
147impl<R: ServiceRole> PeerResponseCacheState<R> {
148 fn trim_to_limit(&mut self) {
149 while self.config.max_entries > 0 && self.entries.len() > self.config.max_entries {
150 let Some(oldest_key) = self
151 .entries
152 .iter()
153 .min_by_key(|(_, entry)| entry.inserted_at)
154 .map(|(key, _)| key.clone())
155 else {
156 break;
157 };
158 self.entries.remove(&oldest_key);
159 }
160 }
161}
162
163pub(crate) type PeerResponseCache<R> = Arc<tokio::sync::RwLock<PeerResponseCacheState<R>>>;
164
165impl<R: ServiceRole> Peer<R> {
166 fn private_partition(config: &ClientCacheConfig) -> Arc<str> {
167 Arc::from(config.private_partition.as_deref().unwrap_or("connection"))
168 }
169
170 fn cache_key(logical_key: &str, partition: CachePartition) -> CacheKey {
171 CacheKey {
172 logical_key: logical_key.to_owned(),
173 partition,
174 }
175 }
176
177 fn scoped_cache_key(
178 logical_key: &str,
179 scope: CacheScope,
180 config: &ClientCacheConfig,
181 ) -> CacheKey {
182 let partition = match scope {
183 CacheScope::Public => CachePartition::Public,
184 CacheScope::Private => CachePartition::Private(Self::private_partition(config)),
185 };
186 Self::cache_key(logical_key, partition)
187 }
188
189 pub(crate) async fn capture_response_cache_generation(&self) -> CacheGeneration {
196 CacheGeneration(self.response_cache.read().await.generation)
197 }
198
199 pub(crate) async fn cached_response(&self, logical_key: &str) -> Option<R::PeerResp> {
206 let now = Instant::now();
207 let mut cache = self.response_cache.write().await;
208 if !cache.config.enabled {
209 return None;
210 }
211 let keep_stale = cache.config.serve_stale_on_error;
212
213 let private_key = Self::cache_key(
214 logical_key,
215 CachePartition::Private(Self::private_partition(&cache.config)),
216 );
217 let private_fresh = cache.entries.get(&private_key).and_then(|entry| {
218 (entry.expires_at > now && entry.scope == CacheScope::Private)
219 .then(|| entry.value.clone())
220 });
221 if let Some(value) = private_fresh {
222 return Some(value);
223 }
224 if !keep_stale {
225 cache.entries.remove(&private_key);
226 }
227
228 let public_key = Self::cache_key(logical_key, CachePartition::Public);
229 let public_fresh = cache.entries.get(&public_key).and_then(|entry| {
230 (entry.expires_at > now && entry.scope == CacheScope::Public)
231 .then(|| entry.value.clone())
232 });
233 if let Some(value) = public_fresh {
234 return Some(value);
235 }
236 if !keep_stale {
237 cache.entries.remove(&public_key);
238 }
239 None
240 }
241
242 pub(crate) async fn stale_cached_response(&self, logical_key: &str) -> Option<R::PeerResp> {
250 let cache = self.response_cache.read().await;
251 if !cache.config.enabled || !cache.config.serve_stale_on_error {
252 return None;
253 }
254
255 let private_key = Self::cache_key(
256 logical_key,
257 CachePartition::Private(Self::private_partition(&cache.config)),
258 );
259 if let Some(entry) = cache.entries.get(&private_key)
260 && entry.scope == CacheScope::Private
261 {
262 return Some(entry.value.clone());
263 }
264
265 let public_key = Self::cache_key(logical_key, CachePartition::Public);
266 if let Some(entry) = cache.entries.get(&public_key)
267 && entry.scope == CacheScope::Public
268 {
269 return Some(entry.value.clone());
270 }
271 None
272 }
273
274 pub(crate) async fn cache_response_with_generation(
280 &self,
281 logical_key: String,
282 value: R::PeerResp,
283 ttl_ms: Option<u64>,
284 cache_scope: Option<CacheScope>,
285 generation: CacheGeneration,
286 ) {
287 let now = Instant::now();
288 let mut cache = self.response_cache.write().await;
289 if !cache.config.enabled || generation.0 != cache.generation {
290 return;
291 }
292
293 let requested_ttl = ttl_ms
294 .map(Duration::from_millis)
295 .unwrap_or(cache.config.default_ttl);
296 let ttl = requested_ttl.min(cache.config.max_ttl);
297 if ttl.is_zero() {
298 return;
299 }
300 let Some(expires_at) = now.checked_add(ttl) else {
301 return;
302 };
303 let scope = cache_scope.unwrap_or(CacheScope::Private);
304 let target_key = Self::scoped_cache_key(&logical_key, scope, &cache.config);
305 let opposite_key = match scope {
306 CacheScope::Public => Self::cache_key(
307 &logical_key,
308 CachePartition::Private(Self::private_partition(&cache.config)),
309 ),
310 CacheScope::Private => Self::cache_key(&logical_key, CachePartition::Public),
311 };
312
313 if !cache.config.serve_stale_on_error {
314 cache.entries.retain(|_, entry| entry.expires_at > now);
315 }
316 cache.entries.remove(&opposite_key);
317
318 if cache.config.max_entries > 0
319 && !cache.entries.contains_key(&target_key)
320 && cache.entries.len() >= cache.config.max_entries
321 && let Some(oldest_key) = cache
322 .entries
323 .iter()
324 .min_by_key(|(_, entry)| entry.inserted_at)
325 .map(|(key, _)| key.clone())
326 {
327 cache.entries.remove(&oldest_key);
328 }
329
330 cache.entries.insert(
331 target_key,
332 CachedPeerResponse {
333 value,
334 expires_at,
335 inserted_at: now,
336 scope,
337 },
338 );
339 }
340
341 #[cfg(test)]
342 pub(crate) async fn cache_response(
343 &self,
344 logical_key: String,
345 value: R::PeerResp,
346 ttl_ms: Option<u64>,
347 cache_scope: Option<CacheScope>,
348 ) {
349 let generation = self.capture_response_cache_generation().await;
350 self.cache_response_with_generation(logical_key, value, ttl_ms, cache_scope, generation)
351 .await;
352 }
353
354 pub(crate) async fn invalidate_cached_responses(&self, prefix: &str) {
355 let mut cache = self.response_cache.write().await;
356 cache.generation = cache.generation.wrapping_add(1);
357 cache
358 .entries
359 .retain(|key, _| !key.logical_key.starts_with(prefix));
360 }
361}
362
363impl Peer<RoleClient> {
364 pub async fn set_response_cache_config(&self, config: ClientCacheConfig) {
371 let mut cache = self.response_cache.write().await;
372 let config_changed = cache.config != config;
373 let partition_changed = cache.config.private_partition != config.private_partition;
374 let ttl_policy_changed = cache.config.default_ttl != config.default_ttl
375 || cache.config.max_ttl != config.max_ttl;
376 cache.config = config;
377 if config_changed {
378 cache.generation = cache.generation.wrapping_add(1);
379 }
380 if !cache.config.enabled || ttl_policy_changed {
381 cache.entries.clear();
382 } else if partition_changed {
383 cache
384 .entries
385 .retain(|_, entry| entry.scope == CacheScope::Public);
386 }
387 cache.trim_to_limit();
388 }
389
390 pub async fn response_cache_config(&self) -> ClientCacheConfig {
392 self.response_cache.read().await.config.clone()
393 }
394
395 pub async fn clear_response_cache(&self) {
397 let mut cache = self.response_cache.write().await;
398 cache.generation = cache.generation.wrapping_add(1);
399 cache.entries.clear();
400 }
401}