1use std::collections::{HashMap, VecDeque};
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
60pub enum OstStorageTier {
61 Hot,
63 Warm,
65 Cold,
67}
68
69impl OstStorageTier {
70 pub fn colder(self) -> Option<OstStorageTier> {
72 match self {
73 OstStorageTier::Hot => Some(OstStorageTier::Warm),
74 OstStorageTier::Warm => Some(OstStorageTier::Cold),
75 OstStorageTier::Cold => None,
76 }
77 }
78
79 pub fn hotter(self) -> Option<OstStorageTier> {
81 match self {
82 OstStorageTier::Cold => Some(OstStorageTier::Warm),
83 OstStorageTier::Warm => Some(OstStorageTier::Hot),
84 OstStorageTier::Hot => None,
85 }
86 }
87
88 pub fn name(&self) -> &'static str {
90 match self {
91 OstStorageTier::Hot => "hot",
92 OstStorageTier::Warm => "warm",
93 OstStorageTier::Cold => "cold",
94 }
95 }
96
97 pub fn rank(self) -> u8 {
99 match self {
100 OstStorageTier::Hot => 0,
101 OstStorageTier::Warm => 1,
102 OstStorageTier::Cold => 2,
103 }
104 }
105}
106
107impl std::fmt::Display for OstStorageTier {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.write_str(self.name())
110 }
111}
112
113#[derive(Debug, Clone)]
122pub enum OstTierPolicy {
123 AccessFrequency(u32),
125 RecencyBased(u64),
127 SizeThreshold {
129 hot_max_bytes: u64,
131 warm_max_bytes: u64,
133 },
134 ManualOnly,
136 CostOptimized(f64),
139}
140
141#[derive(Debug, Clone)]
149pub struct OstTierConfig {
150 pub hot_capacity_bytes: u64,
152 pub warm_capacity_bytes: u64,
154 pub cold_capacity_bytes: u64,
156 pub policy: OstTierPolicy,
158 pub promotion_threshold: u32,
160 pub demotion_interval_us: u64,
162}
163
164impl Default for OstTierConfig {
165 fn default() -> Self {
166 Self {
167 hot_capacity_bytes: 512 * 1_024 * 1_024, warm_capacity_bytes: 10 * 1_024 * 1_024 * 1_024, cold_capacity_bytes: u64::MAX,
170 policy: OstTierPolicy::AccessFrequency(10),
171 promotion_threshold: 5,
172 demotion_interval_us: 3_600_000_000, }
174 }
175}
176
177#[derive(Debug, Clone)]
183pub struct TieredObject {
184 pub id: String,
186 pub size_bytes: u64,
188 pub current_tier: OstStorageTier,
190 pub access_count: u64,
192 pub last_accessed: u64,
194 pub created_at: u64,
196 pub tags: Vec<String>,
198 pub cost_per_hour: f64,
200}
201
202#[derive(Debug, Clone)]
210pub struct OstTierTransition {
211 pub object_id: String,
213 pub from_tier: OstStorageTier,
215 pub to_tier: OstStorageTier,
217 pub reason: String,
219 pub transitioned_at: u64,
221 pub bytes_moved: u64,
223}
224
225#[derive(Debug, Clone, Default)]
231pub struct TieringStats {
232 pub hot_objects: usize,
234 pub warm_objects: usize,
236 pub cold_objects: usize,
238 pub hot_bytes: u64,
240 pub warm_bytes: u64,
242 pub cold_bytes: u64,
244 pub promotions: u64,
246 pub demotions: u64,
248 pub total_cost_per_hour: f64,
250}
251
252#[derive(Debug, Clone, PartialEq)]
258pub enum TieringError {
259 ObjectNotFound(String),
261 TierFull {
263 tier: OstStorageTier,
265 needed: u64,
267 available: u64,
269 },
270 PolicyConflict(String),
272 InvalidConfiguration(String),
274}
275
276impl std::fmt::Display for TieringError {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 match self {
279 TieringError::ObjectNotFound(id) => write!(f, "object not found: {id}"),
280 TieringError::TierFull {
281 tier,
282 needed,
283 available,
284 } => {
285 write!(
286 f,
287 "tier {tier} full: needed {needed} bytes, {available} available"
288 )
289 }
290 TieringError::PolicyConflict(msg) => write!(f, "policy conflict: {msg}"),
291 TieringError::InvalidConfiguration(msg) => {
292 write!(f, "invalid configuration: {msg}")
293 }
294 }
295 }
296}
297
298impl std::error::Error for TieringError {}
299
300const MAX_HISTORY: usize = 500;
306
307fn tier_capacity(cfg: &OstTierConfig, tier: OstStorageTier) -> u64 {
309 match tier {
310 OstStorageTier::Hot => cfg.hot_capacity_bytes,
311 OstStorageTier::Warm => cfg.warm_capacity_bytes,
312 OstStorageTier::Cold => cfg.cold_capacity_bytes,
313 }
314}
315
316pub struct ObjectStorageTiering {
326 config: OstTierConfig,
327 objects: HashMap<String, TieredObject>,
329 tier_bytes: [u64; 3],
331 history: VecDeque<OstTierTransition>,
333 promotions: u64,
335 demotions: u64,
336 pending_promotions: Vec<String>,
339}
340
341impl ObjectStorageTiering {
342 pub fn new(config: OstTierConfig) -> Self {
348 Self {
349 config,
350 objects: HashMap::new(),
351 tier_bytes: [0u64; 3],
352 history: VecDeque::with_capacity(MAX_HISTORY + 1),
353 promotions: 0,
354 demotions: 0,
355 pending_promotions: Vec::new(),
356 }
357 }
358
359 fn used_bytes(&self, tier: OstStorageTier) -> u64 {
365 self.tier_bytes[tier.rank() as usize]
366 }
367
368 fn available_bytes(&self, tier: OstStorageTier) -> u64 {
370 let cap = tier_capacity(&self.config, tier);
371 let used = self.used_bytes(tier);
372 cap.saturating_sub(used)
373 }
374
375 fn add_bytes(&mut self, tier: OstStorageTier, delta: u64) {
377 self.tier_bytes[tier.rank() as usize] =
378 self.tier_bytes[tier.rank() as usize].saturating_add(delta);
379 }
380
381 fn sub_bytes(&mut self, tier: OstStorageTier, delta: u64) {
383 self.tier_bytes[tier.rank() as usize] =
384 self.tier_bytes[tier.rank() as usize].saturating_sub(delta);
385 }
386
387 fn push_history(&mut self, t: OstTierTransition) {
389 if self.history.len() >= MAX_HISTORY {
390 self.history.pop_front();
391 }
392 self.history.push_back(t);
393 }
394
395 fn initial_tier_for(&self, size_bytes: u64) -> Result<OstStorageTier, TieringError> {
398 for tier in [
399 OstStorageTier::Hot,
400 OstStorageTier::Warm,
401 OstStorageTier::Cold,
402 ] {
403 if self.available_bytes(tier) >= size_bytes {
404 return Ok(tier);
405 }
406 }
407 Err(TieringError::TierFull {
408 tier: OstStorageTier::Cold,
409 needed: size_bytes,
410 available: self.available_bytes(OstStorageTier::Cold),
411 })
412 }
413
414 fn internal_move(
417 &mut self,
418 id: &str,
419 to_tier: OstStorageTier,
420 reason: &str,
421 current_ts: u64,
422 ) -> OstTierTransition {
423 let (from_tier, size_bytes) = {
425 let obj = self.objects.get(id).expect("caller verified existence");
426 (obj.current_tier, obj.size_bytes)
427 };
428 self.sub_bytes(from_tier, size_bytes);
430 self.add_bytes(to_tier, size_bytes);
431 if let Some(obj) = self.objects.get_mut(id) {
433 obj.current_tier = to_tier;
434 }
435 let t = OstTierTransition {
436 object_id: id.to_string(),
437 from_tier,
438 to_tier,
439 reason: reason.to_string(),
440 transitioned_at: current_ts,
441 bytes_moved: size_bytes,
442 };
443 self.push_history(t.clone());
444 t
445 }
446
447 pub fn store(&mut self, mut object: TieredObject) -> Result<OstStorageTier, TieringError> {
460 let tier = self.initial_tier_for(object.size_bytes)?;
461 object.current_tier = tier;
462 self.add_bytes(tier, object.size_bytes);
463 self.objects.insert(object.id.clone(), object);
464 Ok(tier)
465 }
466
467 pub fn retrieve(&mut self, id: &str, current_ts: u64) -> Result<&TieredObject, TieringError> {
473 let obj = self
474 .objects
475 .get_mut(id)
476 .ok_or_else(|| TieringError::ObjectNotFound(id.to_string()))?;
477 obj.last_accessed = current_ts;
478 obj.access_count = obj.access_count.saturating_add(1);
479 let should_queue = obj.access_count > obj.access_count.saturating_sub(1)
481 && obj.access_count == (self.config.promotion_threshold as u64).saturating_add(1)
482 && obj.current_tier != OstStorageTier::Hot;
483 if should_queue {
484 self.pending_promotions.push(id.to_string());
485 }
486 Ok(self.objects.get(id).expect("just inserted/accessed"))
487 }
488
489 pub fn promote(
496 &mut self,
497 id: &str,
498 to: OstStorageTier,
499 current_ts: u64,
500 ) -> Result<OstTierTransition, TieringError> {
501 let (current_tier, size_bytes) = {
502 let obj = self
503 .objects
504 .get(id)
505 .ok_or_else(|| TieringError::ObjectNotFound(id.to_string()))?;
506 (obj.current_tier, obj.size_bytes)
507 };
508 if to >= current_tier {
509 return Err(TieringError::PolicyConflict(format!(
510 "promote: target tier {to} is not hotter than current tier {current_tier}"
511 )));
512 }
513 let avail = self.available_bytes(to);
514 if avail < size_bytes {
515 return Err(TieringError::TierFull {
516 tier: to,
517 needed: size_bytes,
518 available: avail,
519 });
520 }
521 self.promotions += 1;
522 Ok(self.internal_move(id, to, "explicit promotion", current_ts))
523 }
524
525 pub fn demote(
532 &mut self,
533 id: &str,
534 to: OstStorageTier,
535 current_ts: u64,
536 ) -> Result<OstTierTransition, TieringError> {
537 let (current_tier, size_bytes) = {
538 let obj = self
539 .objects
540 .get(id)
541 .ok_or_else(|| TieringError::ObjectNotFound(id.to_string()))?;
542 (obj.current_tier, obj.size_bytes)
543 };
544 if to <= current_tier {
545 return Err(TieringError::PolicyConflict(format!(
546 "demote: target tier {to} is not colder than current tier {current_tier}"
547 )));
548 }
549 let avail = self.available_bytes(to);
550 if avail < size_bytes {
551 return Err(TieringError::TierFull {
552 tier: to,
553 needed: size_bytes,
554 available: avail,
555 });
556 }
557 self.demotions += 1;
558 Ok(self.internal_move(id, to, "explicit demotion", current_ts))
559 }
560
561 pub fn run_policy(&mut self, current_ts: u64) -> Vec<OstTierTransition> {
572 let mut transitions = Vec::new();
573
574 match self.config.policy.clone() {
575 OstTierPolicy::AccessFrequency(threshold) => {
576 transitions.extend(self.run_access_frequency_policy(threshold, current_ts));
577 }
578 OstTierPolicy::RecencyBased(window_us) => {
579 transitions.extend(self.run_recency_policy(window_us, current_ts));
580 }
581 OstTierPolicy::SizeThreshold {
582 hot_max_bytes,
583 warm_max_bytes,
584 } => {
585 transitions.extend(self.run_size_threshold_policy(
586 hot_max_bytes,
587 warm_max_bytes,
588 current_ts,
589 ));
590 }
591 OstTierPolicy::ManualOnly => {}
592 OstTierPolicy::CostOptimized(multiplier) => {
593 transitions.extend(self.run_cost_optimized_policy(multiplier, current_ts));
594 }
595 }
596
597 let pending: Vec<String> = std::mem::take(&mut self.pending_promotions);
599 for id in pending {
600 if let Some(obj) = self.objects.get(&id) {
601 if obj.current_tier == OstStorageTier::Hot {
602 continue;
603 }
604 let target = OstStorageTier::Hot;
605 let size = obj.size_bytes;
606 if self.available_bytes(target) >= size {
607 self.promotions += 1;
608 let t =
609 self.internal_move(&id, target, "access-threshold promotion", current_ts);
610 transitions.push(t);
611 }
612 }
613 }
614
615 transitions
616 }
617
618 pub fn evict_tier(
623 &mut self,
624 tier: OstStorageTier,
625 needed_bytes: u64,
626 ) -> Vec<OstTierTransition> {
627 let current_ts = 0u64; self.evict_tier_ts(tier, needed_bytes, current_ts)
629 }
630
631 pub fn evict_tier_ts(
633 &mut self,
634 tier: OstStorageTier,
635 needed_bytes: u64,
636 current_ts: u64,
637 ) -> Vec<OstTierTransition> {
638 let target_tier = match tier.colder() {
639 Some(t) => t,
640 None => return vec![], };
642
643 let mut candidates: Vec<(String, u64, u64)> = self
645 .objects
646 .values()
647 .filter(|o| o.current_tier == tier)
648 .map(|o| (o.id.clone(), o.last_accessed, o.size_bytes))
649 .collect();
650 candidates.sort_by_key(|(_, last_accessed, _)| *last_accessed);
651
652 let mut freed = 0u64;
653 let mut transitions = Vec::new();
654
655 for (id, _last_accessed, size) in candidates {
656 if freed >= needed_bytes {
657 break;
658 }
659 let avail = self.available_bytes(target_tier);
660 if avail < size {
661 continue;
663 }
664 self.demotions += 1;
665 let t = self.internal_move(&id, target_tier, "lru eviction", current_ts);
666 freed += size;
667 transitions.push(t);
668 }
669
670 transitions
671 }
672
673 pub fn tier_objects(&self, tier: OstStorageTier) -> Vec<&TieredObject> {
675 self.objects
676 .values()
677 .filter(|o| o.current_tier == tier)
678 .collect()
679 }
680
681 pub fn transition_history(&self) -> Vec<OstTierTransition> {
683 self.history.iter().cloned().collect()
684 }
685
686 pub fn stats(&self) -> TieringStats {
688 let mut stats = TieringStats {
689 promotions: self.promotions,
690 demotions: self.demotions,
691 ..Default::default()
692 };
693 for obj in self.objects.values() {
694 stats.total_cost_per_hour += obj.cost_per_hour;
695 match obj.current_tier {
696 OstStorageTier::Hot => {
697 stats.hot_objects += 1;
698 stats.hot_bytes += obj.size_bytes;
699 }
700 OstStorageTier::Warm => {
701 stats.warm_objects += 1;
702 stats.warm_bytes += obj.size_bytes;
703 }
704 OstStorageTier::Cold => {
705 stats.cold_objects += 1;
706 stats.cold_bytes += obj.size_bytes;
707 }
708 }
709 }
710 stats
711 }
712
713 fn run_access_frequency_policy(
718 &mut self,
719 threshold: u32,
720 current_ts: u64,
721 ) -> Vec<OstTierTransition> {
722 let promote_ids: Vec<String> = self
723 .objects
724 .values()
725 .filter(|o| o.access_count > threshold as u64 && o.current_tier != OstStorageTier::Hot)
726 .map(|o| o.id.clone())
727 .collect();
728
729 let mut transitions = Vec::new();
730 for id in promote_ids {
731 let size = match self.objects.get(&id) {
732 Some(o) => o.size_bytes,
733 None => continue,
734 };
735 if self.available_bytes(OstStorageTier::Hot) >= size {
736 self.promotions += 1;
737 let t = self.internal_move(
738 &id,
739 OstStorageTier::Hot,
740 "access-frequency promotion",
741 current_ts,
742 );
743 transitions.push(t);
744 }
745 }
746 transitions
747 }
748
749 fn run_recency_policy(&mut self, window_us: u64, current_ts: u64) -> Vec<OstTierTransition> {
750 let cutoff = current_ts.saturating_sub(window_us);
751 let demote_ids: Vec<String> = self
752 .objects
753 .values()
754 .filter(|o| o.last_accessed < cutoff && o.current_tier != OstStorageTier::Cold)
755 .map(|o| o.id.clone())
756 .collect();
757
758 let mut transitions = Vec::new();
759 for id in demote_ids {
760 let size = match self.objects.get(&id) {
761 Some(o) => o.size_bytes,
762 None => continue,
763 };
764 if self.available_bytes(OstStorageTier::Cold) >= size {
765 self.demotions += 1;
766 let t =
767 self.internal_move(&id, OstStorageTier::Cold, "recency demotion", current_ts);
768 transitions.push(t);
769 }
770 }
771 transitions
772 }
773
774 fn run_size_threshold_policy(
775 &mut self,
776 hot_max_bytes: u64,
777 warm_max_bytes: u64,
778 current_ts: u64,
779 ) -> Vec<OstTierTransition> {
780 let mut transitions = Vec::new();
781
782 while self.used_bytes(OstStorageTier::Hot) > hot_max_bytes {
784 let victim = self
786 .objects
787 .values()
788 .filter(|o| o.current_tier == OstStorageTier::Hot)
789 .max_by_key(|o| o.size_bytes)
790 .map(|o| (o.id.clone(), o.size_bytes));
791 match victim {
792 None => break,
793 Some((id, size)) => {
794 if self.available_bytes(OstStorageTier::Warm) >= size {
795 self.demotions += 1;
796 let t = self.internal_move(
797 &id,
798 OstStorageTier::Warm,
799 "size-threshold hot→warm",
800 current_ts,
801 );
802 transitions.push(t);
803 } else if self.available_bytes(OstStorageTier::Cold) >= size {
804 self.demotions += 1;
805 let t = self.internal_move(
806 &id,
807 OstStorageTier::Cold,
808 "size-threshold hot→cold",
809 current_ts,
810 );
811 transitions.push(t);
812 } else {
813 break; }
815 }
816 }
817 }
818
819 while self.used_bytes(OstStorageTier::Warm) > warm_max_bytes {
821 let victim = self
822 .objects
823 .values()
824 .filter(|o| o.current_tier == OstStorageTier::Warm)
825 .max_by_key(|o| o.size_bytes)
826 .map(|o| (o.id.clone(), o.size_bytes));
827 match victim {
828 None => break,
829 Some((id, size)) => {
830 if self.available_bytes(OstStorageTier::Cold) >= size {
831 self.demotions += 1;
832 let t = self.internal_move(
833 &id,
834 OstStorageTier::Cold,
835 "size-threshold warm→cold",
836 current_ts,
837 );
838 transitions.push(t);
839 } else {
840 break;
841 }
842 }
843 }
844 }
845
846 transitions
847 }
848
849 fn run_cost_optimized_policy(
850 &mut self,
851 multiplier: f64,
852 current_ts: u64,
853 ) -> Vec<OstTierTransition> {
854 let mut scored: Vec<(String, f64, OstStorageTier, u64)> = self
857 .objects
858 .values()
859 .filter(|o| o.current_tier != OstStorageTier::Cold)
860 .map(|o| {
861 let score = o.access_count as f64 / (o.cost_per_hour * multiplier + 1.0);
862 (o.id.clone(), score, o.current_tier, o.size_bytes)
863 })
864 .collect();
865 scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
867
868 let demote_count = (scored.len() as f64 * 0.25).ceil() as usize;
870 let mut transitions = Vec::new();
871
872 for (id, _score, current_tier, size) in scored.into_iter().take(demote_count) {
873 let target = match current_tier.colder() {
874 Some(t) => t,
875 None => continue,
876 };
877 if self.available_bytes(target) >= size {
878 self.demotions += 1;
879 let t = self.internal_move(&id, target, "cost-optimized demotion", current_ts);
880 transitions.push(t);
881 }
882 }
883
884 transitions
885 }
886}
887
888#[cfg(test)]
893mod tests {
894 use super::*;
895
896 fn xorshift64(state: &mut u64) -> u64 {
901 *state ^= *state << 13;
902 *state ^= *state >> 7;
903 *state ^= *state << 17;
904 *state
905 }
906
907 fn make_object(id: &str, size: u64, access_count: u64, last_accessed: u64) -> TieredObject {
908 TieredObject {
909 id: id.to_string(),
910 size_bytes: size,
911 current_tier: OstStorageTier::Hot, access_count,
913 last_accessed,
914 created_at: 0,
915 tags: vec![],
916 cost_per_hour: 0.001 * size as f64,
917 }
918 }
919
920 fn default_config() -> OstTierConfig {
921 OstTierConfig {
922 hot_capacity_bytes: 10_000,
923 warm_capacity_bytes: 100_000,
924 cold_capacity_bytes: u64::MAX,
925 policy: OstTierPolicy::ManualOnly,
926 promotion_threshold: 5,
927 demotion_interval_us: 1_000_000,
928 }
929 }
930
931 fn tiering() -> ObjectStorageTiering {
932 ObjectStorageTiering::new(default_config())
933 }
934
935 #[test]
940 fn test_store_small_goes_to_hot() {
941 let mut t = tiering();
942 let obj = make_object("a", 100, 0, 0);
943 let tier = t.store(obj).unwrap();
944 assert_eq!(tier, OstStorageTier::Hot);
945 }
946
947 #[test]
948 fn test_store_fills_hot_then_warm() {
949 let mut t = tiering();
950 let obj1 = make_object("big", 10_000, 0, 0);
952 assert_eq!(t.store(obj1).unwrap(), OstStorageTier::Hot);
953 let obj2 = make_object("next", 100, 0, 0);
955 assert_eq!(t.store(obj2).unwrap(), OstStorageTier::Warm);
956 }
957
958 #[test]
959 fn test_store_fills_all_tiers_cold() {
960 let mut cfg = default_config();
961 cfg.cold_capacity_bytes = 1_000_000;
962 let mut t = ObjectStorageTiering::new(cfg);
963 let _ = t.store(make_object("h", 10_000, 0, 0)).unwrap();
964 let _ = t.store(make_object("w", 100_000, 0, 0)).unwrap();
965 let tier = t.store(make_object("c", 500_000, 0, 0)).unwrap();
966 assert_eq!(tier, OstStorageTier::Cold);
967 }
968
969 #[test]
970 fn test_store_all_full_returns_tier_full_error() {
971 let mut cfg = default_config();
972 cfg.cold_capacity_bytes = 5;
973 let mut t = ObjectStorageTiering::new(cfg);
974 let _ = t.store(make_object("h", 10_000, 0, 0)).unwrap();
975 let _ = t.store(make_object("w", 100_000, 0, 0)).unwrap();
976 let err = t.store(make_object("c", 100, 0, 0)).unwrap_err();
977 assert!(matches!(
978 err,
979 TieringError::TierFull {
980 tier: OstStorageTier::Cold,
981 ..
982 }
983 ));
984 }
985
986 #[test]
987 fn test_store_updates_tier_bytes() {
988 let mut t = tiering();
989 t.store(make_object("x", 1_000, 0, 0)).unwrap();
990 assert_eq!(t.used_bytes(OstStorageTier::Hot), 1_000);
991 }
992
993 #[test]
994 fn test_store_multiple_objects() {
995 let mut t = tiering();
996 let mut state = 42u64;
997 for i in 0..20 {
998 let size = (xorshift64(&mut state) % 400) + 10;
999 let id = format!("obj-{i}");
1000 t.store(make_object(&id, size, 0, 0)).unwrap();
1001 }
1002 let s = t.stats();
1003 assert_eq!(s.hot_objects + s.warm_objects + s.cold_objects, 20);
1004 }
1005
1006 #[test]
1011 fn test_retrieve_found() {
1012 let mut t = tiering();
1013 t.store(make_object("r1", 100, 0, 0)).unwrap();
1014 let obj = t.retrieve("r1", 1000).unwrap();
1015 assert_eq!(obj.id, "r1");
1016 }
1017
1018 #[test]
1019 fn test_retrieve_updates_access_count() {
1020 let mut t = tiering();
1021 t.store(make_object("r2", 100, 0, 0)).unwrap();
1022 t.retrieve("r2", 1000).unwrap();
1023 t.retrieve("r2", 2000).unwrap();
1024 let obj = t.retrieve("r2", 3000).unwrap();
1025 assert_eq!(obj.access_count, 3);
1026 }
1027
1028 #[test]
1029 fn test_retrieve_updates_last_accessed() {
1030 let mut t = tiering();
1031 t.store(make_object("r3", 100, 0, 0)).unwrap();
1032 t.retrieve("r3", 9999).unwrap();
1033 let obj = t.retrieve("r3", 9999).unwrap();
1034 assert_eq!(obj.last_accessed, 9999);
1035 }
1036
1037 #[test]
1038 fn test_retrieve_not_found_error() {
1039 let mut t = tiering();
1040 let err = t.retrieve("nope", 0).unwrap_err();
1041 assert!(matches!(err, TieringError::ObjectNotFound(_)));
1042 }
1043
1044 #[test]
1045 fn test_retrieve_queues_pending_promotion() {
1046 let mut cfg = default_config();
1047 cfg.promotion_threshold = 2;
1048 let mut t = ObjectStorageTiering::new(cfg);
1049 t.store(make_object("fill-hot", 10_000, 0, 0)).unwrap();
1051 t.store(make_object("warm-obj", 100, 0, 0)).unwrap();
1052 t.retrieve("warm-obj", 1).unwrap();
1054 t.retrieve("warm-obj", 2).unwrap();
1055 t.retrieve("warm-obj", 3).unwrap(); assert!(!t.pending_promotions.is_empty());
1057 }
1058
1059 #[test]
1064 fn test_promote_warm_to_hot() {
1065 let mut cfg = default_config();
1066 cfg.hot_capacity_bytes = 10_000;
1067 let mut t = ObjectStorageTiering::new(cfg);
1068 t.store(make_object("hot-fill", 10_000, 0, 0)).unwrap();
1070 t.store(make_object("w", 100, 0, 0)).unwrap();
1072 assert_eq!(t.objects["w"].current_tier, OstStorageTier::Warm);
1073 let transitions = t.evict_tier_ts(OstStorageTier::Hot, 10_000, 100);
1075 assert!(!transitions.is_empty());
1076 let r = t.promote("w", OstStorageTier::Hot, 200);
1078 assert!(r.is_ok());
1079 assert_eq!(t.objects["w"].current_tier, OstStorageTier::Hot);
1080 }
1081
1082 #[test]
1083 fn test_promote_cold_to_warm() {
1084 let mut cfg = default_config();
1086 cfg.hot_capacity_bytes = 10_000;
1087 cfg.warm_capacity_bytes = 200_000;
1088 let mut t = ObjectStorageTiering::new(cfg);
1089 t.store(make_object("hot-fill", 10_000, 0, 0)).unwrap();
1091 t.store(make_object("warm-partial", 50_000, 0, 0)).unwrap();
1093 t.store(make_object("warm-fill2", 150_000, 0, 0)).unwrap();
1096 let cold_tier = t.store(make_object("cold-obj", 500, 0, 0)).unwrap();
1098 assert_eq!(cold_tier, OstStorageTier::Cold);
1099 t.evict_tier_ts(OstStorageTier::Warm, 600, 500);
1101 let r = t.promote("cold-obj", OstStorageTier::Warm, 1000);
1102 assert!(r.is_ok());
1103 let tr = r.unwrap();
1104 assert_eq!(tr.from_tier, OstStorageTier::Cold);
1105 assert_eq!(tr.to_tier, OstStorageTier::Warm);
1106 }
1107
1108 #[test]
1109 fn test_promote_increments_promotion_counter() {
1110 let mut t = tiering();
1111 t.store(make_object("hot-fill", 10_000, 0, 0)).unwrap();
1112 t.store(make_object("warm-obj", 100, 0, 0)).unwrap();
1113 t.evict_tier_ts(OstStorageTier::Hot, 200, 50);
1114 t.promote("warm-obj", OstStorageTier::Hot, 100).unwrap();
1115 assert_eq!(t.stats().promotions, 1);
1116 }
1117
1118 #[test]
1119 fn test_promote_rejects_same_or_lower_tier() {
1120 let mut t = tiering();
1121 t.store(make_object("obj", 100, 0, 0)).unwrap();
1122 let err = t.promote("obj", OstStorageTier::Warm, 0).unwrap_err();
1123 assert!(matches!(err, TieringError::PolicyConflict(_)));
1124 }
1125
1126 #[test]
1127 fn test_promote_tier_full_error() {
1128 let mut t = tiering();
1129 t.store(make_object("fill", 10_000, 0, 0)).unwrap();
1131 t.store(make_object("w", 100, 0, 0)).unwrap();
1133 let err = t.promote("w", OstStorageTier::Hot, 0).unwrap_err();
1135 assert!(matches!(
1136 err,
1137 TieringError::TierFull {
1138 tier: OstStorageTier::Hot,
1139 ..
1140 }
1141 ));
1142 }
1143
1144 #[test]
1145 fn test_promote_not_found() {
1146 let mut t = tiering();
1147 let err = t.promote("missing", OstStorageTier::Hot, 0).unwrap_err();
1148 assert!(matches!(err, TieringError::ObjectNotFound(_)));
1149 }
1150
1151 #[test]
1156 fn test_demote_hot_to_warm() {
1157 let mut t = tiering();
1158 t.store(make_object("obj", 100, 0, 0)).unwrap();
1159 let tr = t.demote("obj", OstStorageTier::Warm, 10).unwrap();
1160 assert_eq!(tr.from_tier, OstStorageTier::Hot);
1161 assert_eq!(tr.to_tier, OstStorageTier::Warm);
1162 }
1163
1164 #[test]
1165 fn test_demote_hot_to_cold() {
1166 let mut t = tiering();
1167 t.store(make_object("obj", 100, 0, 0)).unwrap();
1168 let tr = t.demote("obj", OstStorageTier::Cold, 10).unwrap();
1169 assert_eq!(tr.to_tier, OstStorageTier::Cold);
1170 }
1171
1172 #[test]
1173 fn test_demote_increments_demotion_counter() {
1174 let mut t = tiering();
1175 t.store(make_object("obj", 100, 0, 0)).unwrap();
1176 t.demote("obj", OstStorageTier::Cold, 0).unwrap();
1177 assert_eq!(t.stats().demotions, 1);
1178 }
1179
1180 #[test]
1181 fn test_demote_rejects_same_or_higher_tier() {
1182 let mut t = tiering();
1183 t.store(make_object("hot-fill", 10_000, 0, 0)).unwrap();
1184 t.store(make_object("warm-obj", 100, 0, 0)).unwrap();
1185 let err = t.demote("warm-obj", OstStorageTier::Hot, 0).unwrap_err();
1187 assert!(matches!(err, TieringError::PolicyConflict(_)));
1188 }
1189
1190 #[test]
1191 fn test_demote_not_found() {
1192 let mut t = tiering();
1193 let err = t.demote("ghost", OstStorageTier::Cold, 0).unwrap_err();
1194 assert!(matches!(err, TieringError::ObjectNotFound(_)));
1195 }
1196
1197 #[test]
1198 fn test_demote_tier_full() {
1199 let mut cfg = default_config();
1200 cfg.warm_capacity_bytes = 50; let mut t = ObjectStorageTiering::new(cfg);
1202 t.store(make_object("obj", 100, 0, 0)).unwrap();
1203 let err = t.demote("obj", OstStorageTier::Warm, 0).unwrap_err();
1204 assert!(matches!(
1205 err,
1206 TieringError::TierFull {
1207 tier: OstStorageTier::Warm,
1208 ..
1209 }
1210 ));
1211 }
1212
1213 #[test]
1218 fn test_run_policy_access_frequency_promotes() {
1219 let mut cfg = default_config();
1220 cfg.policy = OstTierPolicy::AccessFrequency(3);
1221 let mut t = ObjectStorageTiering::new(cfg);
1222 t.store(make_object("fill", 9_900, 0, 0)).unwrap();
1223 t.store(make_object("warm", 50, 0, 0)).unwrap();
1224 if let Some(obj) = t.objects.get_mut("warm") {
1226 obj.access_count = 10;
1227 }
1228 t.evict_tier_ts(OstStorageTier::Hot, 10_000, 100);
1230 let transitions = t.run_policy(200);
1231 let promoted = transitions
1232 .iter()
1233 .any(|tr| tr.object_id == "warm" && tr.to_tier == OstStorageTier::Hot);
1234 assert!(promoted, "expected warm object to be promoted to hot");
1235 }
1236
1237 #[test]
1238 fn test_run_policy_access_frequency_no_promotion_if_already_hot() {
1239 let mut cfg = default_config();
1240 cfg.policy = OstTierPolicy::AccessFrequency(3);
1241 let mut t = ObjectStorageTiering::new(cfg);
1242 t.store(make_object("obj", 100, 10, 0)).unwrap();
1243 let transitions = t.run_policy(0);
1244 assert!(transitions.iter().all(|tr| tr.object_id != "obj"
1245 || tr.to_tier != OstStorageTier::Hot
1246 || tr.from_tier == OstStorageTier::Hot));
1247 }
1248
1249 #[test]
1254 fn test_run_policy_recency_demotes_stale() {
1255 let mut cfg = default_config();
1256 cfg.policy = OstTierPolicy::RecencyBased(1_000_000); let mut t = ObjectStorageTiering::new(cfg);
1258 t.store(make_object("stale", 100, 0, 0)).unwrap();
1260 let transitions = t.run_policy(2_000_000);
1261 assert!(transitions
1262 .iter()
1263 .any(|tr| tr.object_id == "stale" && tr.to_tier == OstStorageTier::Cold));
1264 }
1265
1266 #[test]
1267 fn test_run_policy_recency_keeps_fresh() {
1268 let mut cfg = default_config();
1269 cfg.policy = OstTierPolicy::RecencyBased(1_000_000);
1270 let mut t = ObjectStorageTiering::new(cfg);
1271 let mut obj = make_object("fresh", 100, 0, 1_500_000);
1273 obj.last_accessed = 1_500_000;
1274 t.store(obj).unwrap();
1275 let transitions = t.run_policy(2_000_000);
1276 assert!(transitions
1277 .iter()
1278 .all(|tr| tr.object_id != "fresh" || tr.to_tier != OstStorageTier::Cold));
1279 }
1280
1281 #[test]
1282 fn test_run_policy_recency_skips_already_cold() {
1283 let mut cfg = default_config();
1284 cfg.policy = OstTierPolicy::RecencyBased(100);
1285 let mut t = ObjectStorageTiering::new(cfg);
1286 t.store(make_object("hot-fill", 10_000, 0, 0)).unwrap();
1287 t.store(make_object("warm-fill", 100_000, 0, 0)).unwrap();
1288 t.store(make_object("cold-obj", 100, 0, 0)).unwrap();
1289 let before_demotions = t.demotions;
1291 let transitions = t.run_policy(999_999_999);
1292 let cold_moved: Vec<_> = transitions
1293 .iter()
1294 .filter(|tr| tr.object_id == "cold-obj")
1295 .collect();
1296 assert!(cold_moved.is_empty() || t.demotions == before_demotions);
1297 }
1298
1299 #[test]
1304 fn test_run_policy_size_threshold_demotes_largest() {
1305 let mut cfg = default_config();
1306 cfg.policy = OstTierPolicy::SizeThreshold {
1307 hot_max_bytes: 5_000,
1308 warm_max_bytes: 50_000,
1309 };
1310 let mut t = ObjectStorageTiering::new(cfg);
1311 t.store(make_object("small", 1_000, 0, 0)).unwrap();
1313 t.store(make_object("medium", 2_000, 0, 0)).unwrap();
1314 t.store(make_object("large", 3_000, 0, 0)).unwrap();
1315 let transitions = t.run_policy(0);
1317 assert!(transitions.iter().any(|tr| tr.object_id == "large"));
1319 }
1320
1321 #[test]
1322 fn test_run_policy_size_threshold_no_action_if_within_limits() {
1323 let mut cfg = default_config();
1324 cfg.policy = OstTierPolicy::SizeThreshold {
1325 hot_max_bytes: 10_000,
1326 warm_max_bytes: 100_000,
1327 };
1328 let mut t = ObjectStorageTiering::new(cfg);
1329 t.store(make_object("a", 100, 0, 0)).unwrap();
1330 let transitions = t.run_policy(0);
1331 assert!(transitions.is_empty());
1332 }
1333
1334 #[test]
1339 fn test_run_policy_cost_optimized_demotes_low_access() {
1340 let mut cfg = default_config();
1341 cfg.policy = OstTierPolicy::CostOptimized(1.0);
1342 let mut t = ObjectStorageTiering::new(cfg);
1343 let mut high = make_object("high", 500, 0, 0);
1345 high.access_count = 1000;
1346 t.store(high).unwrap();
1347 let mut low = make_object("low", 500, 0, 0);
1349 low.access_count = 1;
1350 t.store(low).unwrap();
1351 let transitions = t.run_policy(0);
1352 assert!(!transitions.is_empty());
1354 }
1355
1356 #[test]
1357 fn test_run_policy_manual_only_no_transitions() {
1358 let mut cfg = default_config();
1359 cfg.policy = OstTierPolicy::ManualOnly;
1360 let mut t = ObjectStorageTiering::new(cfg);
1361 t.store(make_object("a", 100, 100, 0)).unwrap();
1362 let transitions = t.run_policy(999_999_999);
1363 assert!(transitions.is_empty());
1364 }
1365
1366 #[test]
1371 fn test_evict_tier_lru_order() {
1372 let mut t = tiering();
1373 let mut o1 = make_object("oldest", 1_000, 0, 100);
1375 o1.last_accessed = 100;
1376 let mut o2 = make_object("middle", 1_000, 0, 200);
1377 o2.last_accessed = 200;
1378 let mut o3 = make_object("newest", 1_000, 0, 300);
1379 o3.last_accessed = 300;
1380 t.store(o1).unwrap();
1381 t.store(o2).unwrap();
1382 t.store(o3).unwrap();
1383 let transitions = t.evict_tier_ts(OstStorageTier::Hot, 1_001, 400);
1384 assert!(!transitions.is_empty());
1386 assert_eq!(transitions[0].object_id, "oldest");
1387 }
1388
1389 #[test]
1390 fn test_evict_tier_stops_when_enough_freed() {
1391 let mut t = tiering();
1392 for i in 0..5 {
1393 t.store(make_object(&format!("o{i}"), 1_000, 0, i as u64))
1394 .unwrap();
1395 }
1396 let transitions = t.evict_tier_ts(OstStorageTier::Hot, 1_500, 100);
1397 assert_eq!(transitions.len(), 2);
1399 }
1400
1401 #[test]
1402 fn test_evict_cold_tier_no_op() {
1403 let mut t = tiering();
1404 t.store(make_object("fill-hot", 10_000, 0, 0)).unwrap();
1405 t.store(make_object("fill-warm", 100_000, 0, 0)).unwrap();
1406 t.store(make_object("c", 100, 0, 0)).unwrap();
1407 let transitions = t.evict_tier(OstStorageTier::Cold, 100);
1408 assert!(transitions.is_empty()); }
1410
1411 #[test]
1412 fn test_evict_tier_increments_demotion_counter() {
1413 let mut t = tiering();
1414 t.store(make_object("a", 1_000, 0, 1)).unwrap();
1415 t.store(make_object("b", 1_000, 0, 2)).unwrap();
1416 t.evict_tier_ts(OstStorageTier::Hot, 1_000, 0);
1417 assert!(t.stats().demotions > 0);
1418 }
1419
1420 #[test]
1425 fn test_tier_objects_returns_correct_set() {
1426 let mut t = tiering(); t.store(make_object("h1", 100, 0, 0)).unwrap();
1428 t.store(make_object("h2", 100, 0, 0)).unwrap();
1429 t.store(make_object("fill", 9_800, 0, 0)).unwrap();
1431 t.store(make_object("w1", 100, 0, 0)).unwrap();
1433 let hot = t.tier_objects(OstStorageTier::Hot);
1434 assert!(hot.len() >= 2);
1435 let warm = t.tier_objects(OstStorageTier::Warm);
1436 assert!(!warm.is_empty());
1437 }
1438
1439 #[test]
1440 fn test_tier_objects_empty_for_unused_tier() {
1441 let t = tiering();
1442 assert!(t.tier_objects(OstStorageTier::Cold).is_empty());
1443 }
1444
1445 #[test]
1450 fn test_transition_history_records_demotions() {
1451 let mut t = tiering();
1452 t.store(make_object("obj", 100, 0, 0)).unwrap();
1453 t.demote("obj", OstStorageTier::Warm, 50).unwrap();
1454 let history = t.transition_history();
1455 assert_eq!(history.len(), 1);
1456 assert_eq!(history[0].from_tier, OstStorageTier::Hot);
1457 assert_eq!(history[0].to_tier, OstStorageTier::Warm);
1458 }
1459
1460 #[test]
1461 fn test_transition_history_capped_at_500() {
1462 let mut t = tiering();
1463 for i in 0u64..600 {
1464 let id = format!("obj-{i}");
1465 t.store(make_object(&id, 50, 0, 0)).unwrap();
1466 t.demote(&id, OstStorageTier::Warm, i).unwrap();
1467 }
1468 assert_eq!(t.transition_history().len(), 500);
1469 }
1470
1471 #[test]
1472 fn test_transition_history_stores_reason() {
1473 let mut t = tiering();
1474 t.store(make_object("x", 100, 0, 0)).unwrap();
1475 t.demote("x", OstStorageTier::Cold, 1).unwrap();
1476 let history = t.transition_history();
1477 assert!(history[0].reason.contains("demotion"));
1478 }
1479
1480 #[test]
1481 fn test_transition_history_stores_timestamp() {
1482 let mut t = tiering();
1483 t.store(make_object("x", 100, 0, 0)).unwrap();
1484 t.demote("x", OstStorageTier::Cold, 12345).unwrap();
1485 let history = t.transition_history();
1486 assert_eq!(history[0].transitioned_at, 12345);
1487 }
1488
1489 #[test]
1490 fn test_transition_history_bytes_moved() {
1491 let mut t = tiering();
1492 t.store(make_object("big", 7_777, 0, 0)).unwrap();
1493 t.demote("big", OstStorageTier::Warm, 0).unwrap();
1494 let history = t.transition_history();
1495 assert_eq!(history[0].bytes_moved, 7_777);
1496 }
1497
1498 #[test]
1503 fn test_stats_counts_per_tier() {
1504 let mut t = tiering();
1505 t.store(make_object("h1", 100, 0, 0)).unwrap();
1506 t.store(make_object("h2", 200, 0, 0)).unwrap();
1507 let s = t.stats();
1508 assert_eq!(s.hot_objects, 2);
1509 assert_eq!(s.hot_bytes, 300);
1510 }
1511
1512 #[test]
1513 fn test_stats_total_cost() {
1514 let mut t = tiering();
1515 let mut obj = make_object("c", 100, 0, 0);
1516 obj.cost_per_hour = 1.5;
1517 t.store(obj).unwrap();
1518 let s = t.stats();
1519 assert!((s.total_cost_per_hour - 1.5).abs() < 1e-9);
1520 }
1521
1522 #[test]
1523 fn test_stats_promotions_and_demotions() {
1524 let mut t = tiering(); t.store(make_object("fill", 10_000, 0, 0)).unwrap();
1527 t.store(make_object("w", 100, 0, 0)).unwrap();
1529 t.evict_tier_ts(OstStorageTier::Hot, 10_000, 0);
1531 t.promote("w", OstStorageTier::Hot, 1).unwrap();
1533 t.demote("w", OstStorageTier::Cold, 2).unwrap();
1535 let s = t.stats();
1536 assert!(s.promotions >= 1);
1537 assert!(s.demotions >= 2);
1538 }
1539
1540 #[test]
1541 fn test_stats_empty_tiering() {
1542 let t = tiering();
1543 let s = t.stats();
1544 assert_eq!(s.hot_objects, 0);
1545 assert_eq!(s.warm_objects, 0);
1546 assert_eq!(s.cold_objects, 0);
1547 assert_eq!(s.hot_bytes, 0);
1548 assert_eq!(s.promotions, 0);
1549 assert_eq!(s.demotions, 0);
1550 }
1551
1552 #[test]
1557 fn test_capacity_hot_never_exceeded() {
1558 let mut t = tiering(); for i in 0..20 {
1560 let id = format!("cap-{i}");
1561 let _ = t.store(make_object(&id, 1_000, 0, 0));
1562 }
1563 assert!(t.used_bytes(OstStorageTier::Hot) <= 10_000);
1564 }
1565
1566 #[test]
1567 fn test_capacity_warm_never_exceeded() {
1568 let mut t = tiering(); for i in 0..200 {
1570 let id = format!("w-{i}");
1571 let _ = t.store(make_object(&id, 1_000, 0, 0));
1572 }
1573 assert!(t.used_bytes(OstStorageTier::Warm) <= 100_000);
1574 }
1575
1576 #[test]
1577 fn test_available_bytes_decreases_after_store() {
1578 let mut t = tiering();
1579 let before = t.available_bytes(OstStorageTier::Hot);
1580 t.store(make_object("x", 500, 0, 0)).unwrap();
1581 assert_eq!(t.available_bytes(OstStorageTier::Hot), before - 500);
1582 }
1583
1584 #[test]
1585 fn test_available_bytes_increases_after_demote() {
1586 let mut t = tiering();
1587 t.store(make_object("x", 500, 0, 0)).unwrap();
1588 let before_hot = t.available_bytes(OstStorageTier::Hot);
1589 t.demote("x", OstStorageTier::Warm, 0).unwrap();
1590 assert_eq!(t.available_bytes(OstStorageTier::Hot), before_hot + 500);
1591 }
1592
1593 #[test]
1598 fn test_tiering_error_display_object_not_found() {
1599 let e = TieringError::ObjectNotFound("abc".to_string());
1600 assert!(e.to_string().contains("abc"));
1601 }
1602
1603 #[test]
1604 fn test_tiering_error_display_tier_full() {
1605 let e = TieringError::TierFull {
1606 tier: OstStorageTier::Hot,
1607 needed: 100,
1608 available: 50,
1609 };
1610 let s = e.to_string();
1611 assert!(s.contains("hot"));
1612 assert!(s.contains("100"));
1613 }
1614
1615 #[test]
1616 fn test_tiering_error_display_policy_conflict() {
1617 let e = TieringError::PolicyConflict("test msg".to_string());
1618 assert!(e.to_string().contains("test msg"));
1619 }
1620
1621 #[test]
1622 fn test_tiering_error_display_invalid_config() {
1623 let e = TieringError::InvalidConfiguration("bad".to_string());
1624 assert!(e.to_string().contains("bad"));
1625 }
1626
1627 #[test]
1632 fn test_tier_colder() {
1633 assert_eq!(OstStorageTier::Hot.colder(), Some(OstStorageTier::Warm));
1634 assert_eq!(OstStorageTier::Warm.colder(), Some(OstStorageTier::Cold));
1635 assert_eq!(OstStorageTier::Cold.colder(), None);
1636 }
1637
1638 #[test]
1639 fn test_tier_hotter() {
1640 assert_eq!(OstStorageTier::Cold.hotter(), Some(OstStorageTier::Warm));
1641 assert_eq!(OstStorageTier::Warm.hotter(), Some(OstStorageTier::Hot));
1642 assert_eq!(OstStorageTier::Hot.hotter(), None);
1643 }
1644
1645 #[test]
1646 fn test_tier_rank_ordering() {
1647 assert!(OstStorageTier::Hot.rank() < OstStorageTier::Warm.rank());
1648 assert!(OstStorageTier::Warm.rank() < OstStorageTier::Cold.rank());
1649 }
1650
1651 #[test]
1652 fn test_tier_name() {
1653 assert_eq!(OstStorageTier::Hot.name(), "hot");
1654 assert_eq!(OstStorageTier::Warm.name(), "warm");
1655 assert_eq!(OstStorageTier::Cold.name(), "cold");
1656 }
1657
1658 #[test]
1659 fn test_tier_display() {
1660 assert_eq!(OstStorageTier::Hot.to_string(), "hot");
1661 }
1662
1663 #[test]
1668 fn test_round_trip_store_retrieve_demote_promote() {
1669 let mut cfg = default_config();
1670 cfg.hot_capacity_bytes = 100_000;
1671 cfg.warm_capacity_bytes = 1_000_000;
1672 let mut t = ObjectStorageTiering::new(cfg);
1673 let obj = make_object("rt", 1_000, 0, 0);
1674 let tier = t.store(obj).unwrap();
1675 assert_eq!(tier, OstStorageTier::Hot);
1676 t.retrieve("rt", 500).unwrap();
1677 t.demote("rt", OstStorageTier::Warm, 1000).unwrap();
1678 let obj_ref = t.retrieve("rt", 1500).unwrap();
1679 assert_eq!(obj_ref.current_tier, OstStorageTier::Warm);
1680 t.evict_tier_ts(OstStorageTier::Hot, 1, 2000); t.promote("rt", OstStorageTier::Hot, 2500).unwrap();
1682 let obj_ref2 = t.retrieve("rt", 3000).unwrap();
1683 assert_eq!(obj_ref2.current_tier, OstStorageTier::Hot);
1684 }
1685
1686 #[test]
1687 fn test_stress_many_objects_stats_consistent() {
1688 let mut cfg = default_config();
1689 cfg.hot_capacity_bytes = 50_000;
1690 cfg.warm_capacity_bytes = 500_000;
1691 let mut t = ObjectStorageTiering::new(cfg);
1692 let mut state = 12345u64;
1693 for i in 0..100 {
1694 let size = xorshift64(&mut state) % 1_000 + 100;
1695 let _ = t.store(make_object(&format!("s{i}"), size, 0, 0));
1696 }
1697 let s = t.stats();
1698 let total_objects = s.hot_objects + s.warm_objects + s.cold_objects;
1699 let total_bytes = s.hot_bytes + s.warm_bytes + s.cold_bytes;
1700 assert_eq!(
1702 t.used_bytes(OstStorageTier::Hot)
1703 + t.used_bytes(OstStorageTier::Warm)
1704 + t.used_bytes(OstStorageTier::Cold),
1705 total_bytes
1706 );
1707 assert_eq!(total_objects, t.objects.len());
1708 }
1709
1710 #[test]
1711 fn test_default_config_sensible_capacities() {
1712 let cfg = OstTierConfig::default();
1713 assert!(cfg.hot_capacity_bytes > 0);
1714 assert!(cfg.warm_capacity_bytes > cfg.hot_capacity_bytes);
1715 assert_eq!(cfg.cold_capacity_bytes, u64::MAX);
1716 }
1717}