1use async_trait::async_trait;
9use common::{DakeraError, NamespaceId, Result, Vector, VectorId};
10use parking_lot::RwLock;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::{Duration, Instant};
15
16use crate::traits::VectorStorage;
17
18#[derive(Debug, Clone)]
20pub struct TieredStorageConfig {
21 pub hot_tier_capacity: usize,
23 pub hot_tier_max_bytes: Option<u64>,
27 pub hot_to_warm_threshold: Duration,
29 pub warm_to_cold_threshold: Duration,
31 pub auto_tier_enabled: bool,
33 pub tier_check_interval: Duration,
35}
36
37impl Default for TieredStorageConfig {
38 fn default() -> Self {
39 Self {
40 hot_tier_capacity: 100_000,
41 hot_tier_max_bytes: None,
42 hot_to_warm_threshold: Duration::from_secs(3600), warm_to_cold_threshold: Duration::from_secs(86400), auto_tier_enabled: true,
45 tier_check_interval: Duration::from_secs(300), }
47 }
48}
49
50fn approx_json_bytes(value: &serde_json::Value) -> usize {
52 match value {
53 serde_json::Value::Null => 4,
54 serde_json::Value::Bool(_) => 5,
55 serde_json::Value::Number(_) => 8,
56 serde_json::Value::String(s) => s.len() + 2,
57 serde_json::Value::Array(a) => a.iter().map(approx_json_bytes).sum::<usize>() + 2 + a.len(),
58 serde_json::Value::Object(o) => {
59 o.iter()
60 .map(|(k, v)| k.len() + 3 + approx_json_bytes(v))
61 .sum::<usize>()
62 + 2
63 }
64 }
65}
66
67const VECTOR_FIXED_OVERHEAD: usize = 96;
69
70fn approx_vector_bytes(v: &Vector) -> usize {
72 v.values.len() * std::mem::size_of::<f32>()
73 + v.id.len()
74 + v.metadata.as_ref().map(approx_json_bytes).unwrap_or(0)
75 + VECTOR_FIXED_OVERHEAD
76}
77
78fn sub_saturating(counter: &AtomicU64, value: u64) {
81 let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |cur| {
82 Some(cur.saturating_sub(value))
83 });
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub enum StorageTier {
89 Hot,
91 Warm,
93 Cold,
95}
96
97impl StorageTier {
98 pub fn as_str(&self) -> &'static str {
99 match self {
100 StorageTier::Hot => "hot",
101 StorageTier::Warm => "warm",
102 StorageTier::Cold => "cold",
103 }
104 }
105}
106
107#[derive(Debug, Clone)]
109struct AccessInfo {
110 last_access: Instant,
112 access_count: u64,
114 tier: StorageTier,
116 approx_bytes: usize,
119}
120
121impl Default for AccessInfo {
122 fn default() -> Self {
123 Self {
124 last_access: Instant::now(),
125 access_count: 0,
126 tier: StorageTier::Hot,
127 approx_bytes: 0,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Default)]
134pub struct TieredStorageStats {
135 pub hot_count: u64,
137 pub hot_bytes: u64,
139 pub warm_count: u64,
141 pub cold_count: u64,
143 pub hot_hits: u64,
145 pub warm_hits: u64,
147 pub cold_hits: u64,
149 pub promotions_to_hot: u64,
151 pub demotions_to_warm: u64,
153 pub demotions_to_cold: u64,
155}
156
157pub struct TieredStorage<H, W, C> {
159 config: TieredStorageConfig,
161 hot_storage: H,
163 warm_storage: W,
165 cold_storage: C,
167 access_info: RwLock<HashMap<(NamespaceId, VectorId), AccessInfo>>,
169 stats: TieredStorageStatsInner,
171}
172
173struct TieredStorageStatsInner {
174 hot_count: AtomicU64,
175 hot_bytes: AtomicU64,
176 warm_count: AtomicU64,
177 cold_count: AtomicU64,
178 hot_hits: AtomicU64,
179 warm_hits: AtomicU64,
180 cold_hits: AtomicU64,
181 promotions_to_hot: AtomicU64,
182 demotions_to_warm: AtomicU64,
183 demotions_to_cold: AtomicU64,
184}
185
186impl Default for TieredStorageStatsInner {
187 fn default() -> Self {
188 Self {
189 hot_count: AtomicU64::new(0),
190 hot_bytes: AtomicU64::new(0),
191 warm_count: AtomicU64::new(0),
192 cold_count: AtomicU64::new(0),
193 hot_hits: AtomicU64::new(0),
194 warm_hits: AtomicU64::new(0),
195 cold_hits: AtomicU64::new(0),
196 promotions_to_hot: AtomicU64::new(0),
197 demotions_to_warm: AtomicU64::new(0),
198 demotions_to_cold: AtomicU64::new(0),
199 }
200 }
201}
202
203impl<H, W, C> TieredStorage<H, W, C>
204where
205 H: VectorStorage,
206 W: VectorStorage,
207 C: VectorStorage,
208{
209 pub fn new(
211 config: TieredStorageConfig,
212 hot_storage: H,
213 warm_storage: W,
214 cold_storage: C,
215 ) -> Self {
216 Self {
217 config,
218 hot_storage,
219 warm_storage,
220 cold_storage,
221 access_info: RwLock::new(HashMap::new()),
222 stats: TieredStorageStatsInner::default(),
223 }
224 }
225
226 pub fn config(&self) -> &TieredStorageConfig {
228 &self.config
229 }
230
231 fn record_access(&self, namespace: &NamespaceId, id: &VectorId, tier: StorageTier) {
233 let key = (namespace.clone(), id.clone());
234 let mut access_map = self.access_info.write();
235
236 let info = access_map.entry(key).or_default();
237 info.last_access = Instant::now();
238 info.access_count += 1;
239 if info.tier == StorageTier::Hot && tier != StorageTier::Hot {
242 sub_saturating(&self.stats.hot_bytes, info.approx_bytes as u64);
243 } else if info.tier != StorageTier::Hot && tier == StorageTier::Hot {
244 self.stats
245 .hot_bytes
246 .fetch_add(info.approx_bytes as u64, Ordering::Relaxed);
247 }
248 info.tier = tier;
249
250 match tier {
252 StorageTier::Hot => self.stats.hot_hits.fetch_add(1, Ordering::Relaxed),
253 StorageTier::Warm => self.stats.warm_hits.fetch_add(1, Ordering::Relaxed),
254 StorageTier::Cold => self.stats.cold_hits.fetch_add(1, Ordering::Relaxed),
255 };
256 }
257
258 fn get_tier(&self, namespace: &NamespaceId, id: &VectorId) -> Option<StorageTier> {
260 let access_map = self.access_info.read();
261 access_map
262 .get(&(namespace.clone(), id.clone()))
263 .map(|info| info.tier)
264 }
265
266 pub async fn promote(&self, namespace: &NamespaceId, id: &VectorId) -> Result<bool> {
268 let current_tier = self.get_tier(namespace, id);
269
270 match current_tier {
271 Some(StorageTier::Warm) => {
272 let vectors = self
274 .warm_storage
275 .get(namespace, std::slice::from_ref(id))
276 .await?;
277 if !vectors.is_empty() {
278 let est = vectors.first().map(approx_vector_bytes).unwrap_or(0);
279 self.hot_storage.upsert(namespace, vectors).await?;
280 self.warm_storage
281 .delete(namespace, std::slice::from_ref(id))
282 .await?;
283
284 self.set_approx_bytes(namespace, id, est);
285 self.update_tier(namespace, id, StorageTier::Hot);
286 self.stats.promotions_to_hot.fetch_add(1, Ordering::Relaxed);
287 self.stats.hot_count.fetch_add(1, Ordering::Relaxed);
288 self.stats.warm_count.fetch_sub(1, Ordering::Relaxed);
289
290 return Ok(true);
291 }
292 }
293 Some(StorageTier::Cold) => {
294 let vectors = self
296 .cold_storage
297 .get(namespace, std::slice::from_ref(id))
298 .await?;
299 if !vectors.is_empty() {
300 let should_be_hot = {
302 let access_map = self.access_info.read();
303 access_map
304 .get(&(namespace.clone(), id.clone()))
305 .map(|info| info.access_count > 10)
306 .unwrap_or(false)
307 };
308
309 if should_be_hot {
310 let est = vectors.first().map(approx_vector_bytes).unwrap_or(0);
311 self.hot_storage.upsert(namespace, vectors).await?;
312 self.set_approx_bytes(namespace, id, est);
313 self.update_tier(namespace, id, StorageTier::Hot);
314 self.stats.promotions_to_hot.fetch_add(1, Ordering::Relaxed);
315 self.stats.hot_count.fetch_add(1, Ordering::Relaxed);
316 } else {
317 self.warm_storage.upsert(namespace, vectors).await?;
318 self.update_tier(namespace, id, StorageTier::Warm);
319 self.stats.warm_count.fetch_add(1, Ordering::Relaxed);
320 }
321 return Ok(true);
326 }
327 }
328 _ => {}
329 }
330
331 Ok(false)
332 }
333
334 pub async fn demote(&self, namespace: &NamespaceId, id: &VectorId) -> Result<bool> {
336 let current_tier = self.get_tier(namespace, id);
337
338 match current_tier {
339 Some(StorageTier::Hot) => {
340 let vectors = self
342 .hot_storage
343 .get(namespace, std::slice::from_ref(id))
344 .await?;
345 if !vectors.is_empty() {
346 self.warm_storage.upsert(namespace, vectors).await?;
347 self.hot_storage
348 .delete(namespace, std::slice::from_ref(id))
349 .await?;
350
351 self.update_tier(namespace, id, StorageTier::Warm);
352 self.stats.demotions_to_warm.fetch_add(1, Ordering::Relaxed);
353 self.stats.hot_count.fetch_sub(1, Ordering::Relaxed);
354 self.stats.warm_count.fetch_add(1, Ordering::Relaxed);
355
356 return Ok(true);
357 }
358 }
359 Some(StorageTier::Warm) => {
360 let vectors = self
362 .warm_storage
363 .get(namespace, std::slice::from_ref(id))
364 .await?;
365 if !vectors.is_empty() {
366 self.cold_storage.upsert(namespace, vectors).await?;
367 self.warm_storage
368 .delete(namespace, std::slice::from_ref(id))
369 .await?;
370
371 self.update_tier(namespace, id, StorageTier::Cold);
372 self.stats.demotions_to_cold.fetch_add(1, Ordering::Relaxed);
373 self.stats.warm_count.fetch_sub(1, Ordering::Relaxed);
374 self.stats.cold_count.fetch_add(1, Ordering::Relaxed);
375
376 return Ok(true);
377 }
378 }
379 _ => {}
380 }
381
382 Ok(false)
383 }
384
385 fn update_tier(&self, namespace: &NamespaceId, id: &VectorId, tier: StorageTier) {
388 let mut access_map = self.access_info.write();
389 let key = (namespace.clone(), id.clone());
390 let info = access_map.entry(key).or_default();
391 let was_hot = info.tier == StorageTier::Hot;
392 let now_hot = tier == StorageTier::Hot;
393 if was_hot && !now_hot {
394 sub_saturating(&self.stats.hot_bytes, info.approx_bytes as u64);
395 } else if !was_hot && now_hot {
396 self.stats
397 .hot_bytes
398 .fetch_add(info.approx_bytes as u64, Ordering::Relaxed);
399 }
400 info.tier = tier;
401 }
402
403 fn set_approx_bytes(&self, namespace: &NamespaceId, id: &VectorId, bytes: usize) {
406 let mut access_map = self.access_info.write();
407 if let Some(info) = access_map.get_mut(&(namespace.clone(), id.clone())) {
408 if info.tier == StorageTier::Hot {
409 sub_saturating(&self.stats.hot_bytes, info.approx_bytes as u64);
410 self.stats
411 .hot_bytes
412 .fetch_add(bytes as u64, Ordering::Relaxed);
413 }
414 info.approx_bytes = bytes;
415 }
416 }
417
418 fn is_permanent_storage_error(error: &DakeraError) -> bool {
419 match error {
420 DakeraError::NamespaceNotFound(_) => true,
421 DakeraError::Storage(msg) => {
422 msg.contains("PermissionDenied") || msg.contains("permission denied")
423 }
424 _ => false,
425 }
426 }
427
428 pub async fn run_auto_tiering(&self) -> Result<TieringResult> {
430 if !self.config.auto_tier_enabled {
431 let demoted_to_warm = self.enforce_hot_capacity().await;
434 return Ok(TieringResult {
435 demoted_to_warm,
436 ..Default::default()
437 });
438 }
439
440 let now = Instant::now();
441 let mut to_demote_to_warm = Vec::new();
442 let mut to_demote_to_cold = Vec::new();
443
444 {
446 let access_map = self.access_info.read();
447 for ((namespace, id), info) in access_map.iter() {
448 let elapsed = now.duration_since(info.last_access);
449
450 match info.tier {
451 StorageTier::Hot if elapsed > self.config.hot_to_warm_threshold => {
452 to_demote_to_warm.push((namespace.clone(), id.clone()));
453 }
454 StorageTier::Warm if elapsed > self.config.warm_to_cold_threshold => {
455 to_demote_to_cold.push((namespace.clone(), id.clone()));
456 }
457 _ => {}
458 }
459 }
460 }
461
462 let mut demoted_to_warm = 0;
465 let mut demoted_to_cold = 0;
466 let mut stale_entries = Vec::new();
467
468 for (namespace, id) in to_demote_to_warm {
469 match self.demote(&namespace, &id).await {
470 Ok(true) => demoted_to_warm += 1,
471 Ok(false) => {}
472 Err(e) => {
473 if Self::is_permanent_storage_error(&e) {
474 stale_entries.push((namespace, id));
475 } else {
476 tracing::debug!(error = %e, "Transient demotion error, will retry next cycle");
477 }
478 }
479 }
480 }
481
482 for (namespace, id) in to_demote_to_cold {
483 match self.demote(&namespace, &id).await {
484 Ok(true) => demoted_to_cold += 1,
485 Ok(false) => {}
486 Err(e) => {
487 if Self::is_permanent_storage_error(&e) {
488 stale_entries.push((namespace, id));
489 } else {
490 tracing::debug!(error = %e, "Transient demotion error, will retry next cycle");
491 }
492 }
493 }
494 }
495
496 if !stale_entries.is_empty() {
497 let removed = stale_entries.len();
498 let mut access_map = self.access_info.write();
499 for (ns, id) in &stale_entries {
500 if let Some(info) = access_map.remove(&(ns.clone(), id.clone())) {
501 match info.tier {
502 StorageTier::Hot => {
503 sub_saturating(&self.stats.hot_bytes, info.approx_bytes as u64);
504 sub_saturating(&self.stats.hot_count, 1);
505 }
506 StorageTier::Warm => sub_saturating(&self.stats.warm_count, 1),
507 StorageTier::Cold => sub_saturating(&self.stats.cold_count, 1),
508 }
509 }
510 }
511 tracing::warn!(
512 removed = removed,
513 "Purged stale entries from tiering queue (deleted namespace or permanent error)"
514 );
515 }
516
517 let demoted_by_capacity = self.enforce_hot_capacity().await;
520
521 Ok(TieringResult {
522 demoted_to_warm: demoted_to_warm + demoted_by_capacity,
523 demoted_to_cold,
524 promoted_to_hot: 0,
525 promoted_to_warm: 0,
526 })
527 }
528
529 pub async fn enforce_hot_capacity(&self) -> u64 {
537 let cap_count = self.config.hot_tier_capacity;
538 let cap_bytes = self.config.hot_tier_max_bytes;
539
540 let mut hot_entries: Vec<((NamespaceId, VectorId), Instant, u64)> = {
541 let access_map = self.access_info.read();
542 access_map
543 .iter()
544 .filter(|(_, info)| info.tier == StorageTier::Hot)
545 .map(|(key, info)| (key.clone(), info.last_access, info.approx_bytes as u64))
546 .collect()
547 };
548
549 let mut cur_count = hot_entries.len();
550 let mut cur_bytes: u64 = hot_entries.iter().map(|(_, _, b)| *b).sum();
551 let over = |count: usize, bytes: u64| {
552 count > cap_count || cap_bytes.map(|cap| bytes > cap).unwrap_or(false)
553 };
554
555 if !over(cur_count, cur_bytes) {
556 return 0;
557 }
558
559 hot_entries.sort_by_key(|(_, last_access, _)| *last_access);
561
562 let mut demoted = 0u64;
563 for ((ns, id), _, bytes) in hot_entries {
564 if !over(cur_count, cur_bytes) {
565 break;
566 }
567 match self.demote(&ns, &id).await {
568 Ok(true) => {
569 demoted += 1;
570 cur_count -= 1;
571 cur_bytes = cur_bytes.saturating_sub(bytes);
572 }
573 Ok(false) => {
574 cur_count = cur_count.saturating_sub(1);
578 cur_bytes = cur_bytes.saturating_sub(bytes);
579 }
580 Err(e) => {
581 tracing::debug!(
582 error = %e,
583 namespace = %ns,
584 "Capacity demotion error, will retry next cycle"
585 );
586 cur_count = cur_count.saturating_sub(1);
587 cur_bytes = cur_bytes.saturating_sub(bytes);
588 }
589 }
590 }
591
592 if demoted > 0 {
593 tracing::info!(
594 demoted,
595 cap_count,
596 cap_bytes = cap_bytes.unwrap_or(0),
597 "Hot tier capacity enforcement demoted LRU vectors to warm"
598 );
599 }
600 demoted
601 }
602
603 pub fn stats(&self) -> TieredStorageStats {
605 TieredStorageStats {
606 hot_count: self.stats.hot_count.load(Ordering::Relaxed),
607 hot_bytes: self.stats.hot_bytes.load(Ordering::Relaxed),
608 warm_count: self.stats.warm_count.load(Ordering::Relaxed),
609 cold_count: self.stats.cold_count.load(Ordering::Relaxed),
610 hot_hits: self.stats.hot_hits.load(Ordering::Relaxed),
611 warm_hits: self.stats.warm_hits.load(Ordering::Relaxed),
612 cold_hits: self.stats.cold_hits.load(Ordering::Relaxed),
613 promotions_to_hot: self.stats.promotions_to_hot.load(Ordering::Relaxed),
614 demotions_to_warm: self.stats.demotions_to_warm.load(Ordering::Relaxed),
615 demotions_to_cold: self.stats.demotions_to_cold.load(Ordering::Relaxed),
616 }
617 }
618
619 pub fn tier_distribution(&self, namespace: &NamespaceId) -> TierDistribution {
621 let access_map = self.access_info.read();
622 let mut hot = 0u64;
623 let mut warm = 0u64;
624 let mut cold = 0u64;
625
626 for ((ns, _), info) in access_map.iter() {
627 if ns == namespace {
628 match info.tier {
629 StorageTier::Hot => hot += 1,
630 StorageTier::Warm => warm += 1,
631 StorageTier::Cold => cold += 1,
632 }
633 }
634 }
635
636 TierDistribution { hot, warm, cold }
637 }
638}
639
640#[derive(Debug, Clone, Default)]
642pub struct TieringResult {
643 pub demoted_to_warm: u64,
645 pub demoted_to_cold: u64,
647 pub promoted_to_hot: u64,
649 pub promoted_to_warm: u64,
651}
652
653#[derive(Debug, Clone)]
655pub struct TierDistribution {
656 pub hot: u64,
657 pub warm: u64,
658 pub cold: u64,
659}
660
661#[async_trait]
662impl<H, W, C> VectorStorage for TieredStorage<H, W, C>
663where
664 H: VectorStorage,
665 W: VectorStorage,
666 C: VectorStorage + Clone + Send + Sync + 'static,
667{
668 async fn upsert(&self, namespace: &NamespaceId, vectors: Vec<Vector>) -> Result<usize> {
669 let cold_vectors = vectors.clone();
671
672 let count = self.hot_storage.upsert(namespace, vectors).await?;
674
675 {
680 let mut access_map = self.access_info.write();
681 let mut newly_hot = 0u64;
682 for v in &cold_vectors {
683 let est = approx_vector_bytes(v);
684 let key = (namespace.clone(), v.id.clone());
685 match access_map.get_mut(&key) {
686 Some(info) => {
687 if info.tier == StorageTier::Hot {
688 sub_saturating(&self.stats.hot_bytes, info.approx_bytes as u64);
689 } else {
690 newly_hot += 1;
691 }
692 info.tier = StorageTier::Hot;
693 info.last_access = Instant::now();
694 info.access_count += 1;
695 info.approx_bytes = est;
696 }
697 None => {
698 newly_hot += 1;
699 access_map.insert(
700 key,
701 AccessInfo {
702 last_access: Instant::now(),
703 access_count: 1,
704 tier: StorageTier::Hot,
705 approx_bytes: est,
706 },
707 );
708 }
709 }
710 self.stats
711 .hot_bytes
712 .fetch_add(est as u64, Ordering::Relaxed);
713 self.stats.hot_hits.fetch_add(1, Ordering::Relaxed);
714 }
715 self.stats.hot_count.fetch_add(newly_hot, Ordering::Relaxed);
716 }
717
718 let cold = self.cold_storage.clone();
721 let cold_ns = namespace.clone();
722 tokio::spawn(async move {
723 if let Err(e) = cold.ensure_namespace(&cold_ns).await {
724 tracing::error!(
725 error = %e,
726 namespace = %cold_ns,
727 "Cold tier namespace ensure failed (S3 flush aborted)"
728 );
729 return;
730 }
731 let retry_vectors = cold_vectors.clone();
732 match cold.upsert(&cold_ns, cold_vectors).await {
733 Ok(_) => {}
734 Err(DakeraError::DimensionMismatch { expected, actual }) => {
735 tracing::warn!(
736 namespace = %cold_ns,
737 cold_dim = expected,
738 hot_dim = actual,
739 "Cold tier dimension mismatch — resetting stale cold namespace"
740 );
741 if let Err(e) = cold.delete_namespace(&cold_ns).await {
742 tracing::error!(error = %e, namespace = %cold_ns,
743 "Failed to delete stale cold namespace");
744 return;
745 }
746 if let Err(e) = cold.ensure_namespace(&cold_ns).await {
747 tracing::error!(error = %e, namespace = %cold_ns,
748 "Failed to recreate cold namespace after dimension fix");
749 return;
750 }
751 if let Err(e) = cold.upsert(&cold_ns, retry_vectors).await {
752 tracing::error!(
753 error = %e,
754 namespace = %cold_ns,
755 "Cold tier S3 flush failed after dimension fix — data is durable in hot tier"
756 );
757 }
758 }
759 Err(e) => {
760 tracing::error!(
761 error = %e,
762 namespace = %cold_ns,
763 "Cold tier S3 flush failed — data is durable in hot tier"
764 );
765 }
766 }
767 });
768
769 Ok(count)
770 }
771
772 async fn get(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<Vec<Vector>> {
773 let mut results = Vec::with_capacity(ids.len());
774 let mut remaining_ids: Vec<VectorId> = ids.to_vec();
775
776 let hot_results = match self.hot_storage.get(namespace, &remaining_ids).await {
778 Ok(v) => v,
779 Err(DakeraError::NamespaceNotFound(_)) => vec![],
780 Err(e) => return Err(e),
781 };
782 for v in &hot_results {
783 self.record_access(namespace, &v.id, StorageTier::Hot);
784 }
785
786 let found_ids: std::collections::HashSet<_> = hot_results.iter().map(|v| &v.id).collect();
788 remaining_ids.retain(|id| !found_ids.contains(id));
789 results.extend(hot_results);
790
791 if remaining_ids.is_empty() {
792 return Ok(results);
793 }
794
795 let warm_results = match self.warm_storage.get(namespace, &remaining_ids).await {
797 Ok(v) => v,
798 Err(common::DakeraError::NamespaceNotFound(_)) => vec![],
799 Err(e) => return Err(e),
800 };
801 for v in &warm_results {
802 self.record_access(namespace, &v.id, StorageTier::Warm);
803 }
804
805 let found_ids: std::collections::HashSet<_> = warm_results.iter().map(|v| &v.id).collect();
806 remaining_ids.retain(|id| !found_ids.contains(id));
807 results.extend(warm_results);
808
809 if remaining_ids.is_empty() {
810 return Ok(results);
811 }
812
813 let cold_results = match self.cold_storage.get(namespace, &remaining_ids).await {
815 Ok(v) => v,
816 Err(DakeraError::NamespaceNotFound(_)) => vec![],
817 Err(e) => return Err(e),
818 };
819 for v in &cold_results {
820 self.record_access(namespace, &v.id, StorageTier::Cold);
821 }
822 results.extend(cold_results);
823
824 Ok(results)
825 }
826
827 async fn get_all(&self, namespace: &NamespaceId) -> Result<Vec<Vector>> {
828 let mut seen = std::collections::HashSet::new();
829 let mut results = Vec::new();
830
831 let tier_get_all = |res: common::Result<Vec<Vector>>| -> common::Result<Vec<Vector>> {
833 match res {
834 Ok(v) => Ok(v),
835 Err(common::DakeraError::NamespaceNotFound(_)) => Ok(vec![]),
836 Err(e) => Err(e),
837 }
838 };
839
840 for v in tier_get_all(self.hot_storage.get_all(namespace).await)? {
844 if seen.insert(v.id.clone()) {
845 results.push(v);
846 }
847 }
848 for v in tier_get_all(self.warm_storage.get_all(namespace).await)? {
849 if seen.insert(v.id.clone()) {
850 results.push(v);
851 }
852 }
853 for v in tier_get_all(self.cold_storage.get_all(namespace).await)? {
854 if seen.insert(v.id.clone()) {
855 results.push(v);
856 }
857 }
858
859 Ok(results)
860 }
861
862 async fn delete(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<usize> {
863 let mut deleted = 0;
864
865 match self.hot_storage.delete(namespace, ids).await {
868 Ok(n) => deleted += n,
869 Err(DakeraError::NamespaceNotFound(_)) => {}
870 Err(e) => return Err(e),
871 }
872 match self.warm_storage.delete(namespace, ids).await {
873 Ok(n) => deleted += n,
874 Err(DakeraError::NamespaceNotFound(_)) => {}
875 Err(e) => return Err(e),
876 }
877 match self.cold_storage.delete(namespace, ids).await {
878 Ok(n) => deleted += n,
879 Err(DakeraError::NamespaceNotFound(_)) => {}
880 Err(e) => return Err(e),
881 }
882
883 {
885 let mut access_map = self.access_info.write();
886 for id in ids {
887 if let Some(info) = access_map.remove(&(namespace.clone(), id.clone())) {
888 match info.tier {
889 StorageTier::Hot => {
890 sub_saturating(&self.stats.hot_bytes, info.approx_bytes as u64);
891 sub_saturating(&self.stats.hot_count, 1);
892 }
893 StorageTier::Warm => sub_saturating(&self.stats.warm_count, 1),
894 StorageTier::Cold => sub_saturating(&self.stats.cold_count, 1),
895 }
896 }
897 }
898 }
899
900 Ok(deleted)
901 }
902
903 async fn namespace_exists(&self, namespace: &NamespaceId) -> Result<bool> {
904 Ok(self.hot_storage.namespace_exists(namespace).await?
906 || self.warm_storage.namespace_exists(namespace).await?
907 || self.cold_storage.namespace_exists(namespace).await?)
908 }
909
910 async fn ensure_namespace(&self, namespace: &NamespaceId) -> Result<()> {
911 self.hot_storage.ensure_namespace(namespace).await?;
913 self.warm_storage.ensure_namespace(namespace).await?;
914 self.cold_storage.ensure_namespace(namespace).await?;
915 Ok(())
916 }
917
918 async fn count(&self, namespace: &NamespaceId) -> Result<usize> {
919 let cold = self.cold_storage.count(namespace).await?;
924 if cold > 0 {
925 return Ok(cold);
926 }
927 let hot = self.hot_storage.count(namespace).await?;
929 let warm = self.warm_storage.count(namespace).await?;
930 Ok(hot + warm)
931 }
932
933 async fn dimension(&self, namespace: &NamespaceId) -> Result<Option<usize>> {
934 if let Some(dim) = self.hot_storage.dimension(namespace).await? {
936 return Ok(Some(dim));
937 }
938 if let Some(dim) = self.warm_storage.dimension(namespace).await? {
939 return Ok(Some(dim));
940 }
941 self.cold_storage.dimension(namespace).await
942 }
943
944 async fn list_namespaces(&self) -> Result<Vec<NamespaceId>> {
945 let mut namespaces = std::collections::HashSet::new();
946
947 namespaces.extend(self.hot_storage.list_namespaces().await?);
948 namespaces.extend(self.warm_storage.list_namespaces().await?);
949 namespaces.extend(self.cold_storage.list_namespaces().await?);
950
951 Ok(namespaces.into_iter().collect())
952 }
953
954 async fn delete_namespace(&self, namespace: &NamespaceId) -> Result<bool> {
955 let hot_deleted = self.hot_storage.delete_namespace(namespace).await?;
957 let warm_deleted = self.warm_storage.delete_namespace(namespace).await?;
958 let cold_deleted = self.cold_storage.delete_namespace(namespace).await?;
959
960 {
962 let mut access_map = self.access_info.write();
963 access_map.retain(|(ns, _), _| ns != namespace);
964 }
965
966 Ok(hot_deleted || warm_deleted || cold_deleted)
967 }
968
969 async fn cleanup_expired(&self, namespace: &NamespaceId) -> Result<usize> {
970 let mut total = 0;
972 total += self.hot_storage.cleanup_expired(namespace).await?;
973 total += self.warm_storage.cleanup_expired(namespace).await?;
974 total += self.cold_storage.cleanup_expired(namespace).await?;
975 Ok(total)
976 }
977
978 async fn cleanup_all_expired(&self) -> Result<usize> {
979 let mut total = 0;
981 total += self.hot_storage.cleanup_all_expired().await?;
982 total += self.warm_storage.cleanup_all_expired().await?;
983 total += self.cold_storage.cleanup_all_expired().await?;
984 Ok(total)
985 }
986}
987
988#[cfg(test)]
989mod tests {
990 use super::*;
991 use crate::memory::InMemoryStorage;
992
993 fn create_test_vector(id: &str, dim: usize) -> Vector {
994 Vector {
995 id: id.to_string(),
996 values: vec![1.0; dim],
997 metadata: None,
998 ttl_seconds: None,
999 expires_at: None,
1000 }
1001 }
1002
1003 #[tokio::test]
1004 async fn test_tiered_storage_basic() {
1005 let config = TieredStorageConfig::default();
1006 let storage = TieredStorage::new(
1007 config,
1008 InMemoryStorage::new(),
1009 InMemoryStorage::new(),
1010 InMemoryStorage::new(),
1011 );
1012
1013 let namespace = "test".to_string();
1014 storage.ensure_namespace(&namespace).await.unwrap();
1015
1016 let vectors = vec![create_test_vector("v1", 4)];
1018 let count = storage.upsert(&namespace, vectors).await.unwrap();
1019 assert_eq!(count, 1);
1020
1021 let results = storage.get(&namespace, &["v1".to_string()]).await.unwrap();
1023 assert_eq!(results.len(), 1);
1024
1025 let stats = storage.stats();
1026 assert_eq!(stats.hot_hits, 2); }
1028
1029 #[tokio::test]
1030 async fn test_tiered_storage_promotion_demotion() {
1031 let config = TieredStorageConfig::default();
1032 let storage = TieredStorage::new(
1033 config,
1034 InMemoryStorage::new(),
1035 InMemoryStorage::new(),
1036 InMemoryStorage::new(),
1037 );
1038
1039 let namespace = "test".to_string();
1040 storage.ensure_namespace(&namespace).await.unwrap();
1041
1042 storage
1044 .upsert(&namespace, vec![create_test_vector("v1", 4)])
1045 .await
1046 .unwrap();
1047
1048 assert_eq!(
1050 storage.get_tier(&namespace, &"v1".to_string()),
1051 Some(StorageTier::Hot)
1052 );
1053
1054 let demoted = storage.demote(&namespace, &"v1".to_string()).await.unwrap();
1056 assert!(demoted);
1057 assert_eq!(
1058 storage.get_tier(&namespace, &"v1".to_string()),
1059 Some(StorageTier::Warm)
1060 );
1061
1062 let demoted = storage.demote(&namespace, &"v1".to_string()).await.unwrap();
1064 assert!(demoted);
1065 assert_eq!(
1066 storage.get_tier(&namespace, &"v1".to_string()),
1067 Some(StorageTier::Cold)
1068 );
1069
1070 let results = storage.get(&namespace, &["v1".to_string()]).await.unwrap();
1072 assert_eq!(results.len(), 1);
1073
1074 let stats = storage.stats();
1075 assert_eq!(stats.demotions_to_warm, 1);
1076 assert_eq!(stats.demotions_to_cold, 1);
1077 }
1078
1079 #[tokio::test]
1080 async fn test_tiered_storage_multi_tier_get() {
1081 let config = TieredStorageConfig::default();
1082 let storage = TieredStorage::new(
1083 config,
1084 InMemoryStorage::new(),
1085 InMemoryStorage::new(),
1086 InMemoryStorage::new(),
1087 );
1088
1089 let namespace = "test".to_string();
1090 storage.ensure_namespace(&namespace).await.unwrap();
1091
1092 for i in 0..3 {
1094 storage
1095 .upsert(&namespace, vec![create_test_vector(&format!("v{}", i), 4)])
1096 .await
1097 .unwrap();
1098 }
1099
1100 storage.demote(&namespace, &"v1".to_string()).await.unwrap();
1102 storage.demote(&namespace, &"v2".to_string()).await.unwrap();
1103 storage.demote(&namespace, &"v2".to_string()).await.unwrap();
1104
1105 let ids: Vec<_> = (0..3).map(|i| format!("v{}", i)).collect();
1107 let results = storage.get(&namespace, &ids).await.unwrap();
1108 assert_eq!(results.len(), 3);
1109 }
1110
1111 #[tokio::test]
1112 async fn test_tier_distribution() {
1113 let config = TieredStorageConfig::default();
1114 let storage = TieredStorage::new(
1115 config,
1116 InMemoryStorage::new(),
1117 InMemoryStorage::new(),
1118 InMemoryStorage::new(),
1119 );
1120
1121 let namespace = "test".to_string();
1122 storage.ensure_namespace(&namespace).await.unwrap();
1123
1124 for i in 0..5 {
1126 storage
1127 .upsert(&namespace, vec![create_test_vector(&format!("v{}", i), 4)])
1128 .await
1129 .unwrap();
1130 }
1131
1132 storage.demote(&namespace, &"v3".to_string()).await.unwrap();
1134 storage.demote(&namespace, &"v4".to_string()).await.unwrap();
1135 storage.demote(&namespace, &"v4".to_string()).await.unwrap();
1136
1137 let dist = storage.tier_distribution(&namespace);
1138 assert_eq!(dist.hot, 3);
1139 assert_eq!(dist.warm, 1);
1140 assert_eq!(dist.cold, 1);
1141 }
1142
1143 #[tokio::test]
1144 async fn test_auto_tiering_purges_stale_namespace_entries() {
1145 let config = TieredStorageConfig {
1146 auto_tier_enabled: true,
1147 hot_to_warm_threshold: Duration::from_millis(0),
1148 warm_to_cold_threshold: Duration::from_millis(0),
1149 ..Default::default()
1150 };
1151 let storage = TieredStorage::new(
1152 config,
1153 InMemoryStorage::new(),
1154 InMemoryStorage::new(),
1155 InMemoryStorage::new(),
1156 );
1157
1158 let namespace = "deleted_bench_ns".to_string();
1159 storage.ensure_namespace(&namespace).await.unwrap();
1160 storage
1161 .upsert(&namespace, vec![create_test_vector("v1", 4)])
1162 .await
1163 .unwrap();
1164
1165 storage.delete_namespace(&namespace).await.unwrap();
1167
1168 {
1170 let mut access_map = storage.access_info.write();
1171 access_map.insert(
1172 (namespace.clone(), "v1".to_string()),
1173 AccessInfo {
1174 last_access: Instant::now() - Duration::from_secs(7200),
1175 access_count: 1,
1176 tier: StorageTier::Hot,
1177 approx_bytes: 0,
1178 },
1179 );
1180 }
1181
1182 let result = storage.run_auto_tiering().await.unwrap();
1184 assert_eq!(result.demoted_to_warm, 0);
1185
1186 let access_map = storage.access_info.read();
1187 assert!(
1188 access_map.is_empty(),
1189 "Stale entries should have been purged from tiering queue"
1190 );
1191 }
1192
1193 #[test]
1194 fn test_is_permanent_storage_error() {
1195 assert!(TieredStorage::<
1196 InMemoryStorage,
1197 InMemoryStorage,
1198 InMemoryStorage,
1199 >::is_permanent_storage_error(
1200 &DakeraError::NamespaceNotFound("test".into())
1201 ));
1202 assert!(TieredStorage::<
1203 InMemoryStorage,
1204 InMemoryStorage,
1205 InMemoryStorage,
1206 >::is_permanent_storage_error(&DakeraError::Storage(
1207 "PermissionDenied (permanent) at write".into()
1208 )));
1209 assert!(!TieredStorage::<
1210 InMemoryStorage,
1211 InMemoryStorage,
1212 InMemoryStorage,
1213 >::is_permanent_storage_error(
1214 &DakeraError::Storage("connection timeout".into())
1215 ));
1216 assert!(!TieredStorage::<
1217 InMemoryStorage,
1218 InMemoryStorage,
1219 InMemoryStorage,
1220 >::is_permanent_storage_error(
1221 &DakeraError::DimensionMismatch {
1222 expected: 384,
1223 actual: 1024,
1224 }
1225 ));
1226 }
1227
1228 #[test]
1229 fn test_approx_vector_bytes_scales_with_payload() {
1230 let small = approx_vector_bytes(&create_test_vector("v", 4));
1231 let big = approx_vector_bytes(&create_test_vector("v", 1024));
1232 assert_eq!(big - small, (1024 - 4) * std::mem::size_of::<f32>());
1234
1235 let mut with_meta = create_test_vector("v", 4);
1236 with_meta.metadata = Some(serde_json::json!({ "content": "x".repeat(100) }));
1237 assert!(approx_vector_bytes(&with_meta) > small + 100);
1238 }
1239
1240 #[tokio::test]
1241 async fn test_hot_bytes_accounting() {
1242 let storage = TieredStorage::new(
1243 TieredStorageConfig::default(),
1244 InMemoryStorage::new(),
1245 InMemoryStorage::new(),
1246 InMemoryStorage::new(),
1247 );
1248 let namespace = "test".to_string();
1249 storage.ensure_namespace(&namespace).await.unwrap();
1250
1251 storage
1252 .upsert(
1253 &namespace,
1254 vec![create_test_vector("v1", 4), create_test_vector("v2", 4)],
1255 )
1256 .await
1257 .unwrap();
1258 let after_insert = storage.stats().hot_bytes;
1259 assert!(after_insert > 0);
1260
1261 storage
1263 .upsert(&namespace, vec![create_test_vector("v1", 4)])
1264 .await
1265 .unwrap();
1266 assert_eq!(storage.stats().hot_bytes, after_insert);
1267 assert_eq!(storage.stats().hot_count, 2);
1268
1269 storage
1271 .delete(&namespace, &["v1".to_string()])
1272 .await
1273 .unwrap();
1274 assert!(storage.stats().hot_bytes < after_insert);
1275 assert_eq!(storage.stats().hot_count, 1);
1276
1277 storage
1278 .delete(&namespace, &["v2".to_string()])
1279 .await
1280 .unwrap();
1281 assert_eq!(storage.stats().hot_bytes, 0);
1282 assert_eq!(storage.stats().hot_count, 0);
1283 }
1284
1285 #[tokio::test]
1286 async fn test_capacity_enforcement_by_count() {
1287 let config = TieredStorageConfig {
1288 hot_tier_capacity: 2,
1289 auto_tier_enabled: false,
1291 ..Default::default()
1292 };
1293 let storage = TieredStorage::new(
1294 config,
1295 InMemoryStorage::new(),
1296 InMemoryStorage::new(),
1297 InMemoryStorage::new(),
1298 );
1299 let namespace = "test".to_string();
1300 storage.ensure_namespace(&namespace).await.unwrap();
1301
1302 for (i, id) in ["v1", "v2", "v3", "v4"].iter().enumerate() {
1303 storage
1304 .upsert(&namespace, vec![create_test_vector(id, 4)])
1305 .await
1306 .unwrap();
1307 if i < 3 {
1309 tokio::time::sleep(Duration::from_millis(5)).await;
1310 }
1311 }
1312
1313 let result = storage.run_auto_tiering().await.unwrap();
1314 assert_eq!(result.demoted_to_warm, 2);
1315 let stats = storage.stats();
1316 assert_eq!(stats.hot_count, 2);
1317 assert_eq!(stats.warm_count, 2);
1318
1319 assert_eq!(
1321 storage.get_tier(&namespace, &"v1".to_string()),
1322 Some(StorageTier::Warm)
1323 );
1324 assert_eq!(
1325 storage.get_tier(&namespace, &"v2".to_string()),
1326 Some(StorageTier::Warm)
1327 );
1328 assert_eq!(
1329 storage.get_tier(&namespace, &"v4".to_string()),
1330 Some(StorageTier::Hot)
1331 );
1332 let all = storage
1333 .get(
1334 &namespace,
1335 &[
1336 "v1".to_string(),
1337 "v2".to_string(),
1338 "v3".to_string(),
1339 "v4".to_string(),
1340 ],
1341 )
1342 .await
1343 .unwrap();
1344 assert_eq!(all.len(), 4);
1345 }
1346
1347 #[tokio::test]
1348 async fn test_capacity_enforcement_by_bytes() {
1349 let per_vec = approx_vector_bytes(&create_test_vector("v1", 128)) as u64;
1350 let budget = per_vec * 2 + 8; let config = TieredStorageConfig {
1352 hot_tier_capacity: usize::MAX,
1353 hot_tier_max_bytes: Some(budget),
1354 auto_tier_enabled: false,
1355 ..Default::default()
1356 };
1357 let storage = TieredStorage::new(
1358 config,
1359 InMemoryStorage::new(),
1360 InMemoryStorage::new(),
1361 InMemoryStorage::new(),
1362 );
1363 let namespace = "test".to_string();
1364 storage.ensure_namespace(&namespace).await.unwrap();
1365
1366 for (i, id) in ["v1", "v2", "v3", "v4"].iter().enumerate() {
1367 storage
1368 .upsert(&namespace, vec![create_test_vector(id, 128)])
1369 .await
1370 .unwrap();
1371 if i < 3 {
1372 tokio::time::sleep(Duration::from_millis(5)).await;
1373 }
1374 }
1375 assert!(storage.stats().hot_bytes > budget);
1376
1377 let result = storage.run_auto_tiering().await.unwrap();
1378 assert_eq!(result.demoted_to_warm, 2);
1379 let stats = storage.stats();
1380 assert!(stats.hot_bytes <= budget);
1381 assert_eq!(stats.hot_count, 2);
1382
1383 let all = storage
1385 .get(
1386 &namespace,
1387 &[
1388 "v1".to_string(),
1389 "v2".to_string(),
1390 "v3".to_string(),
1391 "v4".to_string(),
1392 ],
1393 )
1394 .await
1395 .unwrap();
1396 assert_eq!(all.len(), 4);
1397 }
1398}