1use std::path::Path;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10use rocksdb::{BlockBasedOptions, Cache, IteratorMode, Options, WriteBatch, DB};
11use serde::{Deserialize, Serialize};
12use tracing::{debug, warn};
13
14use common::{DakeraError, Result, Vector, VectorId};
15
16#[derive(Debug, Clone)]
18pub struct DiskCacheConfig {
19 pub path: String,
21 pub max_size_bytes: u64,
23 pub compression: bool,
25 pub write_buffer_size: usize,
27 pub max_write_buffer_number: i32,
29}
30
31impl Default for DiskCacheConfig {
32 fn default() -> Self {
33 Self {
34 path: "./cache".to_string(),
35 max_size_bytes: 10 * 1024 * 1024 * 1024, compression: true,
37 write_buffer_size: 64 * 1024 * 1024, max_write_buffer_number: 3,
39 }
40 }
41}
42
43#[derive(Debug, Serialize, Deserialize)]
45struct CacheEntry {
46 vector: Vector,
47 access_count: u64,
48 created_at: u64,
49}
50
51pub struct DiskCache {
53 db: Arc<DB>,
54 config: DiskCacheConfig,
55 writes_since_eviction: AtomicU64,
59}
60
61impl DiskCache {
62 pub fn new(config: DiskCacheConfig) -> Result<Self> {
64 let mut opts = Options::default();
65 opts.create_if_missing(true);
66 opts.set_write_buffer_size(config.write_buffer_size);
67 opts.set_max_write_buffer_number(config.max_write_buffer_number);
68
69 if config.compression {
70 opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
71 }
72
73 let block_cache_mb: usize = std::env::var("DAKERA_ROCKSDB_BLOCK_CACHE_MB")
75 .ok()
76 .and_then(|v| v.parse().ok())
77 .unwrap_or(64);
78 let cache = Cache::new_lru_cache(block_cache_mb * 1024 * 1024);
79 let mut block_opts = BlockBasedOptions::default();
80 block_opts.set_block_cache(&cache);
81 block_opts.set_optimize_filters_for_memory(true);
82 opts.set_block_based_table_factory(&block_opts);
83
84 opts.set_level_compaction_dynamic_level_bytes(true);
86 opts.set_max_background_jobs(4);
87
88 let db = DB::open(&opts, &config.path)
89 .map_err(|e| DakeraError::Storage(format!("Failed to open RocksDB: {}", e)))?;
90
91 debug!(path = %config.path, "Disk cache initialized");
92
93 Ok(Self {
94 db: Arc::new(db),
95 config,
96 writes_since_eviction: AtomicU64::new(0),
97 })
98 }
99
100 const EVICTION_CHECK_INTERVAL: u64 = 1024;
103
104 pub fn enforce_size_limit(&self) -> Result<usize> {
112 let max = self.config.max_size_bytes;
113 if max == 0 {
114 return Ok(0);
115 }
116
117 let mut entries: Vec<(Box<[u8]>, u64, u64)> = Vec::new();
118 let mut total: u64 = 0;
119 for item in self.db.iterator(IteratorMode::Start) {
120 let (key, value) =
121 item.map_err(|e| DakeraError::Storage(format!("disk cache scan failed: {}", e)))?;
122 let bytes = (key.len() + value.len()) as u64;
123 total += bytes;
124 let created_at = serde_json::from_slice::<CacheEntry>(&value)
125 .map(|e| e.created_at)
126 .unwrap_or(0);
127 entries.push((key, created_at, bytes));
128 }
129
130 if total <= max {
131 return Ok(0);
132 }
133
134 let target = max - max / 10;
136 entries.sort_by_key(|(_, created_at, _)| *created_at);
137
138 let mut batch = WriteBatch::default();
139 let mut evicted = 0usize;
140 for (key, _, bytes) in entries {
141 if total <= target {
142 break;
143 }
144 batch.delete(key);
145 total = total.saturating_sub(bytes);
146 evicted += 1;
147 }
148
149 if evicted > 0 {
150 self.db.write(batch).map_err(|e| {
151 DakeraError::Storage(format!("disk cache eviction write failed: {}", e))
152 })?;
153 debug!(
154 evicted,
155 remaining_bytes = total,
156 "disk cache evicted oldest entries to honor max_size_bytes"
157 );
158 }
159 Ok(evicted)
160 }
161
162 fn note_writes(&self, n: u64) {
166 if self.config.max_size_bytes == 0 {
167 return;
168 }
169 let prev = self.writes_since_eviction.fetch_add(n, Ordering::Relaxed);
170 if prev + n >= Self::EVICTION_CHECK_INTERVAL {
171 self.writes_since_eviction.store(0, Ordering::Relaxed);
172 if let Err(e) = self.enforce_size_limit() {
173 warn!(error = %e, "disk cache size-limit enforcement failed");
174 }
175 }
176 }
177
178 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
180 let config = DiskCacheConfig {
181 path: path.as_ref().to_string_lossy().to_string(),
182 ..Default::default()
183 };
184 Self::new(config)
185 }
186
187 fn make_key(namespace: &str, id: &VectorId) -> Vec<u8> {
189 format!("{}:{}", namespace, id).into_bytes()
190 }
191
192 fn namespace_prefix(namespace: &str) -> Vec<u8> {
194 format!("{}:", namespace).into_bytes()
195 }
196
197 pub fn put(&self, namespace: &str, vector: &Vector) -> Result<()> {
199 let key = Self::make_key(namespace, &vector.id);
200 let entry = CacheEntry {
201 vector: vector.clone(),
202 access_count: 1,
203 created_at: std::time::SystemTime::now()
204 .duration_since(std::time::UNIX_EPOCH)
205 .unwrap_or_default()
206 .as_secs(),
207 };
208
209 let value = serde_json::to_vec(&entry)
210 .map_err(|e| DakeraError::Storage(format!("Failed to serialize cache entry: {}", e)))?;
211
212 self.db
213 .put(&key, &value)
214 .map_err(|e| DakeraError::Storage(format!("Failed to write to disk cache: {}", e)))?;
215
216 self.note_writes(1);
217 debug!(namespace = %namespace, id = %vector.id, "Cached vector to disk");
218 Ok(())
219 }
220
221 pub fn put_batch(&self, namespace: &str, vectors: &[Vector]) -> Result<usize> {
223 let mut batch = rocksdb::WriteBatch::default();
224 let now = std::time::SystemTime::now()
225 .duration_since(std::time::UNIX_EPOCH)
226 .unwrap_or_default()
227 .as_secs();
228
229 for vector in vectors {
230 let key = Self::make_key(namespace, &vector.id);
231 let entry = CacheEntry {
232 vector: vector.clone(),
233 access_count: 1,
234 created_at: now,
235 };
236
237 let value = serde_json::to_vec(&entry).map_err(|e| {
238 DakeraError::Storage(format!("Failed to serialize cache entry: {}", e))
239 })?;
240
241 batch.put(&key, &value);
242 }
243
244 let count = vectors.len();
245 self.db.write(batch).map_err(|e| {
246 DakeraError::Storage(format!("Failed to write batch to disk cache: {}", e))
247 })?;
248
249 self.note_writes(count as u64);
250 debug!(namespace = %namespace, count = count, "Batch cached vectors to disk");
251 Ok(count)
252 }
253
254 pub fn get(&self, namespace: &str, id: &VectorId) -> Result<Option<Vector>> {
256 let key = Self::make_key(namespace, id);
257
258 match self.db.get(&key) {
259 Ok(Some(value)) => {
260 let entry: CacheEntry = serde_json::from_slice(&value).map_err(|e| {
261 DakeraError::Storage(format!("Failed to deserialize cache entry: {}", e))
262 })?;
263 Ok(Some(entry.vector))
264 }
265 Ok(None) => Ok(None),
266 Err(e) => {
267 warn!(error = %e, "Failed to read from disk cache");
268 Ok(None)
269 }
270 }
271 }
272
273 pub fn get_batch(&self, namespace: &str, ids: &[VectorId]) -> Result<Vec<Vector>> {
275 let keys: Vec<Vec<u8>> = ids.iter().map(|id| Self::make_key(namespace, id)).collect();
276
277 let results = self.db.multi_get(&keys);
278 let mut vectors = Vec::with_capacity(ids.len());
279
280 for result in results {
281 if let Ok(Some(value)) = result {
282 if let Ok(entry) = serde_json::from_slice::<CacheEntry>(&value) {
283 vectors.push(entry.vector);
284 }
285 }
286 }
287
288 Ok(vectors)
289 }
290
291 pub fn get_all(&self, namespace: &str) -> Result<Vec<Vector>> {
293 let prefix = Self::namespace_prefix(namespace);
294 let mut vectors = Vec::new();
295
296 let iter = self.db.prefix_iterator(&prefix);
297 for item in iter {
298 match item {
299 Ok((key, value)) => {
300 if !key.starts_with(&prefix) {
302 break;
303 }
304
305 if let Ok(entry) = serde_json::from_slice::<CacheEntry>(&value) {
306 vectors.push(entry.vector);
307 }
308 }
309 Err(e) => {
310 warn!(error = %e, "Error iterating disk cache");
311 break;
312 }
313 }
314 }
315
316 Ok(vectors)
317 }
318
319 pub fn delete(&self, namespace: &str, id: &VectorId) -> Result<bool> {
321 let key = Self::make_key(namespace, id);
322
323 let existed = self
325 .db
326 .get(&key)
327 .map_err(|e| DakeraError::Storage(format!("Failed to check disk cache: {}", e)))?
328 .is_some();
329
330 if existed {
331 self.db.delete(&key).map_err(|e| {
332 DakeraError::Storage(format!("Failed to delete from disk cache: {}", e))
333 })?;
334 }
335
336 Ok(existed)
337 }
338
339 pub fn delete_batch(&self, namespace: &str, ids: &[VectorId]) -> Result<usize> {
341 let mut batch = rocksdb::WriteBatch::default();
342 let mut count = 0;
343
344 for id in ids {
345 let key = Self::make_key(namespace, id);
346 if self.db.get(&key).ok().flatten().is_some() {
347 batch.delete(&key);
348 count += 1;
349 }
350 }
351
352 self.db.write(batch).map_err(|e| {
353 DakeraError::Storage(format!("Failed to delete batch from disk cache: {}", e))
354 })?;
355
356 Ok(count)
357 }
358
359 pub fn clear_namespace(&self, namespace: &str) -> Result<usize> {
361 let prefix = Self::namespace_prefix(namespace);
362 let mut batch = rocksdb::WriteBatch::default();
363 let mut count = 0;
364
365 let iter = self.db.prefix_iterator(&prefix);
366 for item in iter {
367 match item {
368 Ok((key, _)) => {
369 if !key.starts_with(&prefix) {
370 break;
371 }
372 batch.delete(&key);
373 count += 1;
374 }
375 Err(_) => break,
376 }
377 }
378
379 if count > 0 {
380 self.db.write(batch).map_err(|e| {
381 DakeraError::Storage(format!("Failed to clear namespace from disk cache: {}", e))
382 })?;
383 }
384
385 debug!(namespace = %namespace, count = count, "Cleared namespace from disk cache");
386 Ok(count)
387 }
388
389 pub fn approximate_size(&self) -> u64 {
391 self.db
392 .property_int_value("rocksdb.estimate-live-data-size")
393 .ok()
394 .flatten()
395 .unwrap_or(0)
396 }
397
398 pub fn stats(&self) -> DiskCacheStats {
400 DiskCacheStats {
401 approximate_size_bytes: self.approximate_size(),
402 approximate_num_keys: self
403 .db
404 .property_int_value("rocksdb.estimate-num-keys")
405 .ok()
406 .flatten()
407 .unwrap_or(0),
408 }
409 }
410
411 pub fn flush(&self) -> Result<()> {
413 self.db
414 .flush()
415 .map_err(|e| DakeraError::Storage(format!("Failed to flush disk cache: {}", e)))
416 }
417}
418
419#[derive(Debug, Clone)]
421pub struct DiskCacheStats {
422 pub approximate_size_bytes: u64,
423 pub approximate_num_keys: u64,
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use tempfile::TempDir;
430
431 fn create_test_cache() -> (DiskCache, TempDir) {
432 let temp_dir = TempDir::new().unwrap();
433 let config = DiskCacheConfig {
434 path: temp_dir.path().to_string_lossy().to_string(),
435 ..Default::default()
436 };
437 let cache = DiskCache::new(config).unwrap();
438 (cache, temp_dir)
439 }
440
441 fn test_vector(id: &str) -> Vector {
442 Vector {
443 id: id.to_string(),
444 values: vec![1.0, 2.0, 3.0],
445 metadata: None,
446 ttl_seconds: None,
447 expires_at: None,
448 }
449 }
450
451 #[test]
452 fn test_put_and_get() {
453 let (cache, _dir) = create_test_cache();
454 let namespace = "test";
455 let vector = test_vector("v1");
456
457 cache.put(namespace, &vector).unwrap();
458 let result = cache.get(namespace, &"v1".to_string()).unwrap();
459
460 assert!(result.is_some());
461 let retrieved = result.unwrap();
462 assert_eq!(retrieved.id, "v1");
463 assert_eq!(retrieved.values, vec![1.0, 2.0, 3.0]);
464 }
465
466 #[test]
467 fn test_get_nonexistent() {
468 let (cache, _dir) = create_test_cache();
469 let result = cache.get("test", &"nonexistent".to_string()).unwrap();
470 assert!(result.is_none());
471 }
472
473 #[test]
476 fn enforce_size_limit_evicts_when_over_cap() {
477 let temp_dir = TempDir::new().unwrap();
478 let config = DiskCacheConfig {
479 path: temp_dir.path().to_string_lossy().to_string(),
480 max_size_bytes: 2048, ..Default::default()
482 };
483 let cache = DiskCache::new(config).unwrap();
484 let ns = "test";
485 let vectors: Vec<Vector> = (0..200).map(|i| test_vector(&format!("v{i}"))).collect();
486 cache.put_batch(ns, &vectors).unwrap();
487
488 let before = cache.get_all(ns).unwrap().len();
489 let evicted = cache.enforce_size_limit().unwrap();
490 let after = cache.get_all(ns).unwrap().len();
491
492 assert!(evicted > 0, "should evict when over the cap");
493 assert!(after < before, "eviction should reduce entry count");
494 }
495
496 #[test]
497 fn enforce_size_limit_noop_when_unlimited() {
498 let temp_dir = TempDir::new().unwrap();
499 let config = DiskCacheConfig {
500 path: temp_dir.path().to_string_lossy().to_string(),
501 max_size_bytes: 0, ..Default::default()
503 };
504 let cache = DiskCache::new(config).unwrap();
505 cache
506 .put_batch("test", &[test_vector("a"), test_vector("b")])
507 .unwrap();
508 assert_eq!(cache.enforce_size_limit().unwrap(), 0);
509 assert_eq!(cache.get_all("test").unwrap().len(), 2);
510 }
511
512 #[test]
513 fn test_batch_operations() {
514 let (cache, _dir) = create_test_cache();
515 let namespace = "test";
516 let vectors = vec![test_vector("v1"), test_vector("v2"), test_vector("v3")];
517
518 let count = cache.put_batch(namespace, &vectors).unwrap();
519 assert_eq!(count, 3);
520
521 let ids: Vec<String> = vec!["v1".to_string(), "v2".to_string(), "v3".to_string()];
522 let retrieved = cache.get_batch(namespace, &ids).unwrap();
523 assert_eq!(retrieved.len(), 3);
524 }
525
526 #[test]
527 fn test_get_all() {
528 let (cache, _dir) = create_test_cache();
529 let namespace = "test";
530 let vectors = vec![test_vector("v1"), test_vector("v2")];
531
532 cache.put_batch(namespace, &vectors).unwrap();
533 let all = cache.get_all(namespace).unwrap();
534 assert_eq!(all.len(), 2);
535 }
536
537 #[test]
538 fn test_delete() {
539 let (cache, _dir) = create_test_cache();
540 let namespace = "test";
541 let vector = test_vector("v1");
542
543 cache.put(namespace, &vector).unwrap();
544 assert!(cache.get(namespace, &"v1".to_string()).unwrap().is_some());
545
546 let deleted = cache.delete(namespace, &"v1".to_string()).unwrap();
547 assert!(deleted);
548
549 assert!(cache.get(namespace, &"v1".to_string()).unwrap().is_none());
550 }
551
552 #[test]
553 fn test_delete_batch() {
554 let (cache, _dir) = create_test_cache();
555 let namespace = "test";
556 let vectors = vec![test_vector("v1"), test_vector("v2"), test_vector("v3")];
557
558 cache.put_batch(namespace, &vectors).unwrap();
559
560 let ids = vec!["v1".to_string(), "v2".to_string()];
561 let deleted = cache.delete_batch(namespace, &ids).unwrap();
562 assert_eq!(deleted, 2);
563
564 assert!(cache.get(namespace, &"v1".to_string()).unwrap().is_none());
565 assert!(cache.get(namespace, &"v2".to_string()).unwrap().is_none());
566 assert!(cache.get(namespace, &"v3".to_string()).unwrap().is_some());
567 }
568
569 #[test]
570 fn test_clear_namespace() {
571 let (cache, _dir) = create_test_cache();
572 let vectors = vec![test_vector("v1"), test_vector("v2")];
573
574 cache.put_batch("ns1", &vectors).unwrap();
575 cache.put_batch("ns2", &vectors).unwrap();
576
577 let cleared = cache.clear_namespace("ns1").unwrap();
578 assert_eq!(cleared, 2);
579
580 assert!(cache.get_all("ns1").unwrap().is_empty());
581 assert_eq!(cache.get_all("ns2").unwrap().len(), 2);
582 }
583
584 #[test]
585 fn test_namespace_isolation() {
586 let (cache, _dir) = create_test_cache();
587 let vector = test_vector("v1");
588
589 cache.put("ns1", &vector).unwrap();
590 cache.put("ns2", &vector).unwrap();
591
592 assert!(cache.get("ns1", &"v1".to_string()).unwrap().is_some());
593 assert!(cache.get("ns2", &"v1".to_string()).unwrap().is_some());
594
595 cache.delete("ns1", &"v1".to_string()).unwrap();
596
597 assert!(cache.get("ns1", &"v1".to_string()).unwrap().is_none());
598 assert!(cache.get("ns2", &"v1".to_string()).unwrap().is_some());
599 }
600
601 #[test]
602 fn test_stats() {
603 let (cache, _dir) = create_test_cache();
604 let vectors = vec![test_vector("v1"), test_vector("v2")];
605
606 cache.put_batch("test", &vectors).unwrap();
607 cache.flush().unwrap();
608
609 let stats = cache.stats();
610 let _ = stats.approximate_num_keys;
612 }
613}