1use common::Vector;
6use futures_util::{stream::FuturesUnordered, StreamExt};
7use moka::future::Cache;
8use std::sync::Arc;
9use std::time::Duration;
10
11#[derive(Debug, Clone)]
13pub struct CacheConfig {
14 pub max_capacity: u64,
16 pub ttl: Option<Duration>,
18 pub tti: Option<Duration>,
20}
21
22impl Default for CacheConfig {
23 fn default() -> Self {
24 Self {
25 max_capacity: 100_000,
26 ttl: Some(Duration::from_secs(3600)), tti: Some(Duration::from_secs(600)), }
29 }
30}
31
32#[derive(Debug, Clone, Hash, Eq, PartialEq)]
34pub struct CacheKey {
35 pub namespace: Arc<str>,
36 pub vector_id: Arc<str>,
37}
38
39impl CacheKey {
40 pub fn new(namespace: impl AsRef<str>, vector_id: impl AsRef<str>) -> Self {
41 Self {
42 namespace: Arc::from(namespace.as_ref()),
43 vector_id: Arc::from(vector_id.as_ref()),
44 }
45 }
46}
47
48#[derive(Clone)]
50pub struct VectorCache {
51 cache: Cache<CacheKey, Arc<Vector>>,
52 config: CacheConfig,
53}
54
55impl VectorCache {
56 pub fn new(config: CacheConfig) -> Self {
58 let mut builder = Cache::builder()
59 .max_capacity(config.max_capacity)
60 .support_invalidation_closures();
61
62 if let Some(ttl) = config.ttl {
63 builder = builder.time_to_live(ttl);
64 }
65
66 if let Some(tti) = config.tti {
67 builder = builder.time_to_idle(tti);
68 }
69
70 let cache = builder.build();
71
72 Self { cache, config }
73 }
74
75 pub fn with_defaults() -> Self {
77 Self::new(CacheConfig::default())
78 }
79
80 pub async fn get(&self, namespace: &str, vector_id: &str) -> Option<Arc<Vector>> {
82 let key = CacheKey::new(namespace, vector_id);
83 self.cache.get(&key).await
84 }
85
86 pub async fn insert(&self, namespace: &str, vector: Vector) {
88 let key = CacheKey::new(namespace, &vector.id);
89 self.cache.insert(key, Arc::new(vector)).await;
90 }
91
92 pub async fn insert_batch(&self, namespace: &str, vectors: Vec<Vector>) {
94 let mut futs: FuturesUnordered<_> = vectors
95 .into_iter()
96 .map(|v| self.insert(namespace, v))
97 .collect();
98 while futs.next().await.is_some() {}
99 }
100
101 pub async fn remove(&self, namespace: &str, vector_id: &str) {
103 let key = CacheKey::new(namespace, vector_id);
104 self.cache.remove(&key).await;
105 }
106
107 pub async fn remove_batch(&self, namespace: &str, vector_ids: &[String]) {
109 for id in vector_ids {
110 self.remove(namespace, id).await;
111 }
112 }
113
114 pub async fn invalidate_namespace(&self, namespace: &str) {
116 let ns: Arc<str> = Arc::from(namespace);
117 if let Err(e) = self
118 .cache
119 .invalidate_entries_if(move |k, _v| *k.namespace == *ns)
120 {
121 tracing::error!(
126 namespace = namespace,
127 error = %e,
128 "namespace cache invalidation failed; clearing entire cache as fallback"
129 );
130 self.cache.invalidate_all();
131 return;
132 }
133 tracing::debug!(namespace = namespace, "Cache namespace invalidated");
134 }
135
136 pub fn clear(&self) {
138 self.cache.invalidate_all();
139 }
140
141 pub fn stats(&self) -> CacheStats {
143 CacheStats {
144 entry_count: self.cache.entry_count(),
145 weighted_size: self.cache.weighted_size(),
146 max_capacity: self.config.max_capacity,
147 }
148 }
149
150 pub async fn run_pending_tasks(&self) {
152 self.cache.run_pending_tasks().await;
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct CacheStats {
159 pub entry_count: u64,
161 pub weighted_size: u64,
163 pub max_capacity: u64,
165}
166
167impl CacheStats {
168 pub fn utilization(&self) -> f64 {
170 if self.max_capacity == 0 {
171 return 0.0;
172 }
173 (self.entry_count as f64 / self.max_capacity as f64) * 100.0
174 }
175}
176
177pub struct CachedStorage<S> {
179 inner: S,
180 cache: VectorCache,
181 redis: Option<crate::RedisCache>,
182 disk: Option<Arc<crate::DiskCache>>,
185 delta: Option<Arc<crate::DeltaStoreManager>>,
188}
189
190impl<S> CachedStorage<S> {
191 pub fn new(inner: S, cache: VectorCache, redis: Option<crate::RedisCache>) -> Self {
192 Self {
193 inner,
194 cache,
195 redis,
196 disk: None,
197 delta: None,
198 }
199 }
200
201 pub fn with_disk_cache(mut self, disk: Arc<crate::DiskCache>) -> Self {
203 self.disk = Some(disk);
204 self
205 }
206
207 pub fn with_delta_history(mut self, delta: Arc<crate::DeltaStoreManager>) -> Self {
209 self.delta = Some(delta);
210 self
211 }
212
213 pub fn delta(&self) -> Option<&Arc<crate::DeltaStoreManager>> {
215 self.delta.as_ref()
216 }
217
218 pub fn with_default_cache(inner: S) -> Self {
219 Self::new(inner, VectorCache::with_defaults(), None)
220 }
221
222 pub fn cache(&self) -> &VectorCache {
223 &self.cache
224 }
225
226 pub fn inner(&self) -> &S {
227 &self.inner
228 }
229
230 pub fn redis(&self) -> Option<&crate::RedisCache> {
231 self.redis.as_ref()
232 }
233}
234
235#[async_trait::async_trait]
236impl<S: crate::VectorStorage> crate::VectorStorage for CachedStorage<S> {
237 async fn upsert(
238 &self,
239 namespace: &common::NamespaceId,
240 vectors: Vec<common::Vector>,
241 ) -> common::Result<usize> {
242 let count = self.inner.upsert(namespace, vectors.clone()).await?;
243 self.cache.insert_batch(namespace, vectors.clone()).await;
245 if let Some(ref disk) = self.disk {
247 let _ = disk.put_batch(namespace, &vectors);
248 }
249 if let Some(ref delta) = self.delta {
251 delta.upsert(namespace, &vectors);
252 }
253 if let Some(ref redis) = self.redis {
255 redis.set_batch(namespace, &vectors).await;
256 let ids: Vec<String> = vectors.iter().map(|v| v.id.clone()).collect();
257 redis
258 .publish_invalidation(&crate::CacheInvalidation::Vectors {
259 namespace: namespace.to_string(),
260 ids,
261 })
262 .await;
263 }
264 Ok(count)
265 }
266
267 async fn get(
268 &self,
269 namespace: &common::NamespaceId,
270 ids: &[common::VectorId],
271 ) -> common::Result<Vec<common::Vector>> {
272 let mut found = Vec::new();
273 let mut missing_ids: Vec<String> = Vec::new();
274
275 for id in ids {
277 if let Some(v) = self.cache.get(namespace, id).await {
278 found.push((*v).clone());
279 } else {
280 missing_ids.push(id.clone());
281 }
282 }
283 if missing_ids.is_empty() {
284 return Ok(found);
285 }
286
287 if let Some(ref redis) = self.redis {
289 let from_redis = redis.get_multi(namespace, &missing_ids).await;
290 let redis_found_ids: std::collections::HashSet<String> =
291 from_redis.iter().map(|v| v.id.clone()).collect();
292 for v in &from_redis {
293 self.cache.insert(namespace, v.clone()).await; }
295 found.extend(from_redis);
296 missing_ids.retain(|id| !redis_found_ids.contains(id));
297 }
298 if missing_ids.is_empty() {
299 return Ok(found);
300 }
301
302 if let Some(ref disk) = self.disk {
304 if let Ok(from_disk) = disk.get_batch(namespace, &missing_ids) {
305 let disk_ids: std::collections::HashSet<String> =
306 from_disk.iter().map(|v| v.id.clone()).collect();
307 for v in &from_disk {
308 self.cache.insert(namespace, v.clone()).await;
309 }
310 found.extend(from_disk);
311 missing_ids.retain(|id| !disk_ids.contains(id));
312 }
313 if missing_ids.is_empty() {
314 return Ok(found);
315 }
316 }
317
318 let from_store = self.inner.get(namespace, &missing_ids).await?;
320 for v in &from_store {
321 self.cache.insert(namespace, v.clone()).await; if let Some(ref redis) = self.redis {
323 redis.set(namespace, v).await; }
325 }
326 if let Some(ref disk) = self.disk {
328 let _ = disk.put_batch(namespace, &from_store);
329 }
330 found.extend(from_store);
331 Ok(found)
332 }
333
334 async fn get_all(
335 &self,
336 namespace: &common::NamespaceId,
337 ) -> common::Result<Vec<common::Vector>> {
338 let vectors = self.inner.get_all(namespace).await?;
339 for v in &vectors {
341 self.cache.insert(namespace, v.clone()).await;
342 }
343 if let Some(ref redis) = self.redis {
345 redis.set_batch(namespace, &vectors).await;
346 }
347 Ok(vectors)
348 }
349
350 async fn get_all_meta(
351 &self,
352 namespace: &common::NamespaceId,
353 ) -> common::Result<Vec<common::Vector>> {
354 self.inner.get_all_meta(namespace).await
358 }
359
360 async fn delete(
361 &self,
362 namespace: &common::NamespaceId,
363 ids: &[common::VectorId],
364 ) -> common::Result<usize> {
365 let count = self.inner.delete(namespace, ids).await?;
366 self.cache.remove_batch(namespace, ids).await;
367 if let Some(ref disk) = self.disk {
368 let _ = disk.delete_batch(namespace, ids);
369 }
370 if let Some(ref delta) = self.delta {
371 for id in ids {
372 delta.delete(namespace, id);
373 }
374 }
375 if let Some(ref redis) = self.redis {
376 let id_strings: Vec<String> = ids.iter().map(|s| s.to_string()).collect();
377 redis.delete(namespace, &id_strings).await;
378 redis
379 .publish_invalidation(&crate::CacheInvalidation::Vectors {
380 namespace: namespace.to_string(),
381 ids: id_strings,
382 })
383 .await;
384 }
385 Ok(count)
386 }
387
388 async fn namespace_exists(&self, namespace: &common::NamespaceId) -> common::Result<bool> {
389 self.inner.namespace_exists(namespace).await
390 }
391
392 async fn ensure_namespace(&self, namespace: &common::NamespaceId) -> common::Result<()> {
393 self.inner.ensure_namespace(namespace).await
394 }
395
396 async fn count(&self, namespace: &common::NamespaceId) -> common::Result<usize> {
397 self.inner.count(namespace).await
398 }
399
400 async fn dimension(&self, namespace: &common::NamespaceId) -> common::Result<Option<usize>> {
401 self.inner.dimension(namespace).await
402 }
403
404 async fn list_namespaces(&self) -> common::Result<Vec<common::NamespaceId>> {
405 self.inner.list_namespaces().await
406 }
407
408 async fn delete_namespace(&self, namespace: &common::NamespaceId) -> common::Result<bool> {
409 let result = self.inner.delete_namespace(namespace).await?;
410 self.cache.invalidate_namespace(namespace).await;
411 if let Some(ref disk) = self.disk {
412 let _ = disk.clear_namespace(namespace);
413 }
414 if let Some(ref delta) = self.delta {
415 delta.delete_namespace(namespace);
416 }
417 if let Some(ref redis) = self.redis {
418 redis.invalidate_namespace(namespace).await;
419 redis
420 .publish_invalidation(&crate::CacheInvalidation::Namespace(namespace.to_string()))
421 .await;
422 }
423 Ok(result)
424 }
425
426 async fn cleanup_expired(&self, namespace: &common::NamespaceId) -> common::Result<usize> {
427 self.inner.cleanup_expired(namespace).await
428 }
429
430 async fn cleanup_all_expired(&self) -> common::Result<usize> {
431 self.inner.cleanup_all_expired().await
432 }
433
434 async fn reclaim_derived_caches(&self) {
440 let dropped = self.cache.stats().entry_count;
441 self.cache.clear();
442 self.cache.run_pending_tasks().await;
443 tracing::info!(
444 dropped_entries = dropped,
445 "memory reclaim: Moka L1 vector cache cleared"
446 );
447 self.inner.reclaim_derived_caches().await;
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[tokio::test]
456 async fn test_cache_insert_and_get() {
457 let cache = VectorCache::with_defaults();
458
459 let vector = Vector {
460 id: "v1".to_string(),
461 values: vec![1.0, 2.0, 3.0],
462 metadata: None,
463 ttl_seconds: None,
464 expires_at: None,
465 };
466
467 cache.insert("test_ns", vector.clone()).await;
468
469 let retrieved = cache.get("test_ns", "v1").await;
470 assert!(retrieved.is_some());
471
472 let retrieved = retrieved.unwrap();
473 assert_eq!(retrieved.id, "v1");
474 assert_eq!(retrieved.values, vec![1.0, 2.0, 3.0]);
475 }
476
477 #[tokio::test]
478 async fn test_cache_miss() {
479 let cache = VectorCache::with_defaults();
480
481 let retrieved = cache.get("test_ns", "nonexistent").await;
482 assert!(retrieved.is_none());
483 }
484
485 #[tokio::test]
486 async fn test_cache_remove() {
487 let cache = VectorCache::with_defaults();
488
489 let vector = Vector {
490 id: "v1".to_string(),
491 values: vec![1.0, 2.0, 3.0],
492 metadata: None,
493 ttl_seconds: None,
494 expires_at: None,
495 };
496
497 cache.insert("test_ns", vector).await;
498 assert!(cache.get("test_ns", "v1").await.is_some());
499
500 cache.remove("test_ns", "v1").await;
501 cache.run_pending_tasks().await;
502
503 assert!(cache.get("test_ns", "v1").await.is_none());
504 }
505
506 #[tokio::test]
507 async fn test_cache_batch_operations() {
508 let cache = VectorCache::with_defaults();
509
510 let vectors = vec![
511 Vector {
512 id: "v1".to_string(),
513 values: vec![1.0],
514 metadata: None,
515 ttl_seconds: None,
516 expires_at: None,
517 },
518 Vector {
519 id: "v2".to_string(),
520 values: vec![2.0],
521 metadata: None,
522 ttl_seconds: None,
523 expires_at: None,
524 },
525 Vector {
526 id: "v3".to_string(),
527 values: vec![3.0],
528 metadata: None,
529 ttl_seconds: None,
530 expires_at: None,
531 },
532 ];
533
534 cache.insert_batch("test_ns", vectors).await;
535
536 assert!(cache.get("test_ns", "v1").await.is_some());
537 assert!(cache.get("test_ns", "v2").await.is_some());
538 assert!(cache.get("test_ns", "v3").await.is_some());
539
540 cache
541 .remove_batch("test_ns", &["v1".to_string(), "v2".to_string()])
542 .await;
543 cache.run_pending_tasks().await;
544
545 assert!(cache.get("test_ns", "v1").await.is_none());
546 assert!(cache.get("test_ns", "v2").await.is_none());
547 assert!(cache.get("test_ns", "v3").await.is_some());
548 }
549
550 #[tokio::test]
551 async fn test_cache_stats() {
552 let cache = VectorCache::new(CacheConfig {
553 max_capacity: 1000,
554 ttl: None,
555 tti: None,
556 });
557
558 for i in 0..10 {
559 let vector = Vector {
560 id: format!("v{}", i),
561 values: vec![i as f32],
562 metadata: None,
563 ttl_seconds: None,
564 expires_at: None,
565 };
566 cache.insert("test_ns", vector).await;
567 }
568
569 for i in 0..10 {
571 assert!(cache.get("test_ns", &format!("v{}", i)).await.is_some());
572 }
573
574 let stats = cache.stats();
575 assert_eq!(stats.max_capacity, 1000);
576 }
577
578 #[tokio::test]
579 async fn test_cache_namespace_isolation() {
580 let cache = VectorCache::with_defaults();
581
582 let v1 = Vector {
583 id: "same_id".to_string(),
584 values: vec![1.0],
585 metadata: None,
586 ttl_seconds: None,
587 expires_at: None,
588 };
589
590 let v2 = Vector {
591 id: "same_id".to_string(),
592 values: vec![2.0],
593 metadata: None,
594 ttl_seconds: None,
595 expires_at: None,
596 };
597
598 cache.insert("ns1", v1).await;
599 cache.insert("ns2", v2).await;
600
601 let from_ns1 = cache.get("ns1", "same_id").await.unwrap();
602 let from_ns2 = cache.get("ns2", "same_id").await.unwrap();
603
604 assert_eq!(from_ns1.values, vec![1.0]);
605 assert_eq!(from_ns2.values, vec![2.0]);
606 }
607
608 #[tokio::test]
609 async fn test_cache_clear() {
610 let cache = VectorCache::with_defaults();
611
612 for i in 0..5 {
613 let vector = Vector {
614 id: format!("v{}", i),
615 values: vec![i as f32],
616 metadata: None,
617 ttl_seconds: None,
618 expires_at: None,
619 };
620 cache.insert("test_ns", vector).await;
621 }
622
623 for i in 0..5 {
625 assert!(cache.get("test_ns", &format!("v{}", i)).await.is_some());
626 }
627
628 cache.clear();
629 cache.run_pending_tasks().await;
630
631 for i in 0..5 {
633 assert!(cache.get("test_ns", &format!("v{}", i)).await.is_none());
634 }
635 }
636
637 #[tokio::test]
640 async fn test_reclaim_derived_caches_clears_l1_keeps_durable() {
641 use crate::VectorStorage;
642
643 let inner = crate::InMemoryStorage::new();
644 let storage = CachedStorage::with_default_cache(inner);
645 let namespace = "test_ns".to_string();
646 storage.ensure_namespace(&namespace).await.unwrap();
647 storage
648 .upsert(
649 &namespace,
650 vec![Vector {
651 id: "v1".to_string(),
652 values: vec![1.0, 2.0, 3.0],
653 metadata: None,
654 ttl_seconds: None,
655 expires_at: None,
656 }],
657 )
658 .await
659 .unwrap();
660
661 storage.get(&namespace, &["v1".to_string()]).await.unwrap();
663 storage.reclaim_derived_caches().await;
664 assert_eq!(
665 storage.cache().stats().entry_count,
666 0,
667 "reclaim must empty the Moka L1 synchronously (DAK-7337)"
668 );
669
670 let got = storage.get(&namespace, &["v1".to_string()]).await.unwrap();
672 assert_eq!(got.len(), 1);
673 assert_eq!(got[0].id, "v1");
674 }
675}