1use std::{
24 sync::{
25 Arc,
26 atomic::{AtomicU64, Ordering},
27 },
28 time::Duration,
29};
30
31use dashmap::{DashMap, DashSet};
32use moka::sync::Cache as MokaCache;
33use serde_json::Value;
34
35use crate::{error::Result, security::SecurityContext};
36
37#[derive(Debug, Clone, Copy)]
39pub struct ResponseCacheConfig {
40 pub enabled: bool,
42
43 pub max_entries: usize,
45
46 pub ttl_seconds: u64,
48}
49
50impl Default for ResponseCacheConfig {
51 fn default() -> Self {
52 Self {
53 enabled: false, max_entries: 10_000,
55 ttl_seconds: 300,
56 }
57 }
58}
59
60struct ResponseEntry {
65 response: Arc<Value>,
67
68 accessed_views: Box<[String]>,
70}
71
72pub struct ResponseCache {
83 store: MokaCache<(u64, u64), Arc<ResponseEntry>>,
84
85 view_index: Arc<DashMap<String, DashSet<(u64, u64)>>>,
90
91 enabled: bool,
92 hits: AtomicU64,
93 misses: AtomicU64,
94}
95
96impl ResponseCache {
97 #[must_use]
99 pub fn new(config: ResponseCacheConfig) -> Self {
100 let view_index: Arc<DashMap<String, DashSet<(u64, u64)>>> = Arc::new(DashMap::new());
101 let vi = Arc::clone(&view_index);
102
103 let mut builder = MokaCache::builder()
104 .max_capacity(config.max_entries as u64)
105 .eviction_listener(move |key: Arc<(u64, u64)>, value: Arc<ResponseEntry>, _cause| {
106 for view in &value.accessed_views {
107 if let Some(keys) = vi.get(view) {
108 keys.remove(&*key);
109 }
110 }
111 });
112
113 if config.ttl_seconds > 0 {
114 builder = builder.time_to_live(Duration::from_secs(config.ttl_seconds));
115 }
116
117 let store = builder.build();
118
119 Self {
120 store,
121 view_index,
122 enabled: config.enabled,
123 hits: AtomicU64::new(0),
124 misses: AtomicU64::new(0),
125 }
126 }
127
128 #[must_use]
130 pub const fn is_enabled(&self) -> bool {
131 self.enabled
132 }
133
134 pub fn get(&self, query_key: u64, security_hash: u64) -> Result<Option<Arc<Value>>> {
140 if !self.enabled {
141 return Ok(None);
142 }
143
144 let key = (query_key, security_hash);
145 if let Some(entry) = self.store.get(&key) {
146 self.hits.fetch_add(1, Ordering::Relaxed);
147 Ok(Some(Arc::clone(&entry.response)))
148 } else {
149 self.misses.fetch_add(1, Ordering::Relaxed);
150 Ok(None)
151 }
152 }
153
154 pub fn put(
160 &self,
161 query_key: u64,
162 security_hash: u64,
163 response: Arc<Value>,
164 accessed_views: Vec<String>,
165 ) -> Result<()> {
166 if !self.enabled {
167 return Ok(());
168 }
169
170 let key = (query_key, security_hash);
171
172 for view in &accessed_views {
175 self.view_index.entry(view.clone()).or_default().insert(key);
176 }
177
178 let entry = Arc::new(ResponseEntry {
179 response,
180 accessed_views: accessed_views.into_boxed_slice(),
181 });
182
183 self.store.insert(key, entry);
184 Ok(())
185 }
186
187 pub fn invalidate_views(&self, views: &[String]) -> Result<u64> {
195 let mut total = 0_u64;
196
197 for view in views {
198 if let Some(keys) = self.view_index.get(view) {
199 let to_remove: Vec<(u64, u64)> = keys.iter().map(|k| *k).collect();
200 drop(keys);
201 for key in to_remove {
202 self.store.invalidate(&key);
203 total += 1;
205 }
206 }
207 }
208
209 Ok(total)
210 }
211
212 #[must_use]
214 pub fn metrics(&self) -> (u64, u64) {
215 (self.hits.load(Ordering::Relaxed), self.misses.load(Ordering::Relaxed))
216 }
217}
218
219#[must_use]
233pub fn hash_security_context(ctx: Option<&SecurityContext>) -> u64 {
234 use std::hash::{Hash, Hasher};
235
236 let Some(ctx) = ctx else {
237 return 0;
238 };
239
240 let mut hasher = ahash::AHasher::default();
241 ctx.user_id.hash(&mut hasher);
242
243 let mut sorted_roles = ctx.roles.clone();
245 sorted_roles.sort();
246 for role in &sorted_roles {
247 role.hash(&mut hasher);
248 }
249
250 ctx.tenant_id.hash(&mut hasher);
251
252 let mut sorted_scopes = ctx.scopes.clone();
253 sorted_scopes.sort();
254 for scope in &sorted_scopes {
255 scope.hash(&mut hasher);
256 }
257
258 if !ctx.attributes.is_empty() {
260 let mut attr_keys: Vec<&String> = ctx.attributes.keys().collect();
261 attr_keys.sort();
262 for key in attr_keys {
263 key.hash(&mut hasher);
264 serde_json::to_string(&ctx.attributes[key])
266 .unwrap_or_default()
267 .hash(&mut hasher);
268 }
269 }
270
271 hasher.finish()
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 fn enabled_config() -> ResponseCacheConfig {
279 ResponseCacheConfig {
280 enabled: true,
281 max_entries: 100,
282 ttl_seconds: 3600,
283 }
284 }
285
286 #[test]
287 fn test_put_and_get() {
288 let cache = ResponseCache::new(enabled_config());
289 let response = Arc::new(serde_json::json!({"data": {"users": []}}));
290
291 cache
292 .put(1, 0, response.clone(), vec!["v_user".to_string()])
293 .expect("put should succeed");
294 let result = cache.get(1, 0).expect("get should succeed");
295 assert!(result.is_some());
296 assert_eq!(*result.expect("should be Some"), *response);
297 }
298
299 #[test]
300 fn test_different_security_contexts_different_entries() {
301 let cache = ResponseCache::new(enabled_config());
302
303 let admin_response =
304 Arc::new(serde_json::json!({"data": {"users": [{"id": "1", "role": "admin"}]}}));
305 let user_response = Arc::new(serde_json::json!({"data": {"users": [{"id": "1"}]}}));
306
307 cache
309 .put(1, 100, admin_response.clone(), vec!["v_user".to_string()])
310 .expect("put admin");
311 cache
312 .put(1, 200, user_response.clone(), vec!["v_user".to_string()])
313 .expect("put user");
314
315 let admin_result = cache.get(1, 100).expect("get admin").expect("admin hit");
316 let user_result = cache.get(1, 200).expect("get user").expect("user hit");
317
318 assert_ne!(*admin_result, *user_result);
319 assert_eq!(*admin_result, *admin_response);
320 assert_eq!(*user_result, *user_response);
321 }
322
323 #[test]
324 fn test_invalidate_views() {
325 let cache = ResponseCache::new(enabled_config());
326
327 cache
328 .put(1, 0, Arc::new(serde_json::json!("r1")), vec!["v_user".to_string()])
329 .expect("put 1");
330 cache
331 .put(2, 0, Arc::new(serde_json::json!("r2")), vec!["v_post".to_string()])
332 .expect("put 2");
333
334 cache.store.run_pending_tasks();
336
337 let invalidated = cache.invalidate_views(&["v_user".to_string()]).expect("invalidate");
338 assert_eq!(invalidated, 1);
339
340 cache.store.run_pending_tasks();
342
343 assert!(cache.get(1, 0).expect("get 1").is_none());
344 assert!(cache.get(2, 0).expect("get 2").is_some());
345 }
346
347 #[test]
348 fn test_disabled_cache_returns_none() {
349 let cache = ResponseCache::new(ResponseCacheConfig::default());
350 assert!(!cache.is_enabled());
351
352 cache.put(1, 0, Arc::new(serde_json::json!("r")), vec![]).expect("put disabled");
353 assert!(cache.get(1, 0).expect("get disabled").is_none());
354 }
355
356 #[test]
357 fn test_metrics() {
358 let cache = ResponseCache::new(enabled_config());
359
360 cache.put(1, 0, Arc::new(serde_json::json!("r")), vec![]).expect("put");
361 cache.store.run_pending_tasks();
362 let _ = cache.get(1, 0); let _ = cache.get(2, 0); let (hits, misses) = cache.metrics();
366 assert_eq!(hits, 1);
367 assert_eq!(misses, 1);
368 }
369
370 #[test]
375 fn test_hash_security_context_none_returns_zero() {
376 assert_eq!(hash_security_context(None), 0);
377 }
378
379 #[test]
380 fn test_hash_security_context_same_context_same_hash() {
381 let ctx = make_security_context("alice", &["admin"], Some("tenant-1"), &["read:user"]);
382 let hash1 = hash_security_context(Some(&ctx));
383 let hash2 = hash_security_context(Some(&ctx));
384 assert_eq!(hash1, hash2, "Same context must produce same hash");
385 }
386
387 #[test]
388 fn test_hash_security_context_different_user_different_hash() {
389 let alice = make_security_context("alice", &["admin"], Some("tenant-1"), &[]);
390 let bob = make_security_context("bob", &["admin"], Some("tenant-1"), &[]);
391
392 assert_ne!(
393 hash_security_context(Some(&alice)),
394 hash_security_context(Some(&bob)),
395 "Different user_id must produce different hash"
396 );
397 }
398
399 #[test]
400 fn test_hash_security_context_different_roles_different_hash() {
401 let admin = make_security_context("alice", &["admin"], None, &[]);
402 let viewer = make_security_context("alice", &["viewer"], None, &[]);
403
404 assert_ne!(
405 hash_security_context(Some(&admin)),
406 hash_security_context(Some(&viewer)),
407 "Different roles must produce different hash"
408 );
409 }
410
411 #[test]
412 fn test_hash_security_context_role_order_independent() {
413 let ctx1 = make_security_context("alice", &["admin", "viewer"], None, &[]);
414 let ctx2 = make_security_context("alice", &["viewer", "admin"], None, &[]);
415
416 assert_eq!(
417 hash_security_context(Some(&ctx1)),
418 hash_security_context(Some(&ctx2)),
419 "Role order must not affect hash (sorted internally)"
420 );
421 }
422
423 #[test]
424 fn test_hash_security_context_different_tenant_different_hash() {
425 let t1 = make_security_context("alice", &[], Some("tenant-1"), &[]);
426 let t2 = make_security_context("alice", &[], Some("tenant-2"), &[]);
427 let none = make_security_context("alice", &[], None, &[]);
428
429 assert_ne!(hash_security_context(Some(&t1)), hash_security_context(Some(&t2)),);
430 assert_ne!(hash_security_context(Some(&t1)), hash_security_context(Some(&none)),);
431 }
432
433 #[test]
434 fn test_hash_security_context_different_scopes_different_hash() {
435 let read = make_security_context("alice", &[], None, &["read:user"]);
436 let write = make_security_context("alice", &[], None, &["write:user"]);
437 let both = make_security_context("alice", &[], None, &["read:user", "write:user"]);
438
439 assert_ne!(hash_security_context(Some(&read)), hash_security_context(Some(&write)),);
440 assert_ne!(hash_security_context(Some(&read)), hash_security_context(Some(&both)),);
441 }
442
443 #[test]
444 fn test_hash_security_context_scope_order_independent() {
445 let ctx1 = make_security_context("alice", &[], None, &["read:user", "write:post"]);
446 let ctx2 = make_security_context("alice", &[], None, &["write:post", "read:user"]);
447
448 assert_eq!(
449 hash_security_context(Some(&ctx1)),
450 hash_security_context(Some(&ctx2)),
451 "Scope order must not affect hash (sorted internally)"
452 );
453 }
454
455 #[test]
456 fn test_hash_security_context_different_attributes_different_hash() {
457 let mut ctx1 = make_security_context("alice", &["admin"], None, &[]);
458 ctx1.attributes
459 .insert("department".to_string(), serde_json::json!("engineering"));
460
461 let mut ctx2 = make_security_context("alice", &["admin"], None, &[]);
462 ctx2.attributes.insert("department".to_string(), serde_json::json!("sales"));
463
464 let ctx_no_attrs = make_security_context("alice", &["admin"], None, &[]);
465
466 assert_ne!(
467 hash_security_context(Some(&ctx1)),
468 hash_security_context(Some(&ctx2)),
469 "Different attribute values must produce different hashes"
470 );
471 assert_ne!(
472 hash_security_context(Some(&ctx1)),
473 hash_security_context(Some(&ctx_no_attrs)),
474 "Attributes vs no attributes must produce different hashes"
475 );
476 }
477
478 #[test]
483 fn test_invalidate_empty_views_is_noop() {
484 let cache = ResponseCache::new(enabled_config());
485 cache
486 .put(1, 0, Arc::new(serde_json::json!("r")), vec!["v_user".to_string()])
487 .expect("put");
488 cache.store.run_pending_tasks();
489
490 let invalidated = cache.invalidate_views(&[]).expect("invalidate empty");
491 assert_eq!(invalidated, 0);
492 assert!(cache.get(1, 0).expect("still cached").is_some());
493 }
494
495 #[test]
496 fn test_invalidate_nonexistent_view_is_noop() {
497 let cache = ResponseCache::new(enabled_config());
498 cache
499 .put(1, 0, Arc::new(serde_json::json!("r")), vec!["v_user".to_string()])
500 .expect("put");
501 cache.store.run_pending_tasks();
502
503 let invalidated = cache
504 .invalidate_views(&["v_nonexistent".to_string()])
505 .expect("invalidate nonexistent");
506 assert_eq!(invalidated, 0);
507 assert!(cache.get(1, 0).expect("still cached").is_some());
508 }
509
510 #[test]
511 fn test_invalidate_clears_all_security_contexts_for_view() {
512 let cache = ResponseCache::new(enabled_config());
513
514 cache
516 .put(1, 100, Arc::new(serde_json::json!("admin")), vec!["v_user".to_string()])
517 .expect("put admin");
518 cache
519 .put(1, 200, Arc::new(serde_json::json!("user")), vec!["v_user".to_string()])
520 .expect("put user");
521 cache
522 .put(1, 0, Arc::new(serde_json::json!("anon")), vec!["v_user".to_string()])
523 .expect("put anon");
524 cache.store.run_pending_tasks();
525
526 let invalidated = cache.invalidate_views(&["v_user".to_string()]).expect("invalidate");
527 assert_eq!(invalidated, 3, "All entries for the view must be invalidated");
528
529 cache.store.run_pending_tasks();
530
531 assert!(cache.get(1, 100).expect("admin gone").is_none());
532 assert!(cache.get(1, 200).expect("user gone").is_none());
533 assert!(cache.get(1, 0).expect("anon gone").is_none());
534 }
535
536 #[test]
537 fn test_invalidate_multiple_views_at_once() {
538 let cache = ResponseCache::new(enabled_config());
539
540 cache
541 .put(1, 0, Arc::new(serde_json::json!("users")), vec!["v_user".to_string()])
542 .expect("put users");
543 cache
544 .put(2, 0, Arc::new(serde_json::json!("posts")), vec!["v_post".to_string()])
545 .expect("put posts");
546 cache
547 .put(3, 0, Arc::new(serde_json::json!("tags")), vec!["v_tag".to_string()])
548 .expect("put tags");
549 cache.store.run_pending_tasks();
550
551 let invalidated = cache
552 .invalidate_views(&["v_user".to_string(), "v_post".to_string()])
553 .expect("invalidate");
554 assert_eq!(invalidated, 2);
555
556 cache.store.run_pending_tasks();
557
558 assert!(cache.get(1, 0).expect("users gone").is_none());
559 assert!(cache.get(2, 0).expect("posts gone").is_none());
560 assert!(cache.get(3, 0).expect("tags alive").is_some());
561 }
562
563 #[test]
564 fn test_entry_with_multiple_views_invalidated_by_any() {
565 let cache = ResponseCache::new(enabled_config());
566
567 cache
569 .put(
570 1,
571 0,
572 Arc::new(serde_json::json!("joined")),
573 vec!["v_user".to_string(), "v_post".to_string()],
574 )
575 .expect("put");
576 cache.store.run_pending_tasks();
577
578 let invalidated = cache.invalidate_views(&["v_post".to_string()]).expect("invalidate");
580 assert_eq!(invalidated, 1);
581
582 cache.store.run_pending_tasks();
583 assert!(cache.get(1, 0).expect("gone").is_none());
584 }
585
586 #[test]
591 fn test_different_query_keys_no_collision() {
592 let cache = ResponseCache::new(enabled_config());
593
594 cache
595 .put(1, 0, Arc::new(serde_json::json!("response_1")), vec![])
596 .expect("put q1");
597 cache
598 .put(2, 0, Arc::new(serde_json::json!("response_2")), vec![])
599 .expect("put q2");
600 cache.store.run_pending_tasks();
601
602 let r1 = cache.get(1, 0).expect("get q1").expect("q1 hit");
603 let r2 = cache.get(2, 0).expect("get q2").expect("q2 hit");
604
605 assert_eq!(*r1, serde_json::json!("response_1"));
606 assert_eq!(*r2, serde_json::json!("response_2"));
607 }
608
609 #[test]
610 fn test_same_query_key_different_security_no_collision() {
611 let cache = ResponseCache::new(enabled_config());
612
613 for sec_hash in 0_u64..10 {
614 cache
615 .put(
616 42,
617 sec_hash,
618 Arc::new(serde_json::json!(format!("response_for_user_{sec_hash}"))),
619 vec![],
620 )
621 .expect("put");
622 }
623 cache.store.run_pending_tasks();
624
625 for sec_hash in 0_u64..10 {
626 let r = cache.get(42, sec_hash).expect("get").expect("should be cached");
627 assert_eq!(*r, serde_json::json!(format!("response_for_user_{sec_hash}")));
628 }
629 }
630
631 fn make_security_context(
636 user_id: &str,
637 roles: &[&str],
638 tenant_id: Option<&str>,
639 scopes: &[&str],
640 ) -> SecurityContext {
641 use chrono::Utc;
642 SecurityContext {
643 user_id: user_id.to_string(),
644 roles: roles.iter().map(|s| (*s).to_string()).collect(),
645 tenant_id: tenant_id.map(str::to_string),
646 scopes: scopes.iter().map(|s| (*s).to_string()).collect(),
647 attributes: std::collections::HashMap::new(),
648 request_id: "test-request".to_string(),
649 ip_address: None,
650 authenticated_at: Utc::now(),
651 expires_at: Utc::now() + chrono::Duration::hours(1),
652 issuer: None,
653 audience: None,
654 }
655 }
656}