1use super::types::{AccessStatistics, IndexMetadata, StorageTier, TierStatistics};
4use serde::{Deserialize, Serialize};
5use std::time::{Duration, SystemTime};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
9pub enum TieringPolicy {
10 Lru,
12 Lfu,
14 CostBased,
16 SizeBased,
18 LatencyOptimized,
20 #[default]
22 Adaptive,
23 Custom,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub enum TierTransitionReason {
30 HighAccessFrequency,
32 LowAccessFrequency,
34 CapacityExceeded,
36 CostOptimization,
38 LatencyOptimization,
40 Manual,
42 Predictive,
44 SlaRequirement,
46 Emergency,
48}
49
50pub struct PolicyEvaluator {
52 policy: TieringPolicy,
53}
54
55impl PolicyEvaluator {
56 pub fn new(policy: TieringPolicy) -> Self {
58 Self { policy }
59 }
60
61 pub fn evaluate_optimal_tier(
63 &self,
64 metadata: &IndexMetadata,
65 tier_stats: &[TierStatistics; 3],
66 current_time: SystemTime,
67 ) -> (StorageTier, TierTransitionReason) {
68 match self.policy {
69 TieringPolicy::Lru => self.evaluate_lru(metadata, tier_stats, current_time),
70 TieringPolicy::Lfu => self.evaluate_lfu(metadata, tier_stats),
71 TieringPolicy::CostBased => self.evaluate_cost_based(metadata, tier_stats),
72 TieringPolicy::SizeBased => self.evaluate_size_based(metadata, tier_stats),
73 TieringPolicy::LatencyOptimized => {
74 self.evaluate_latency_optimized(metadata, tier_stats)
75 }
76 TieringPolicy::Adaptive => self.evaluate_adaptive(metadata, tier_stats, current_time),
77 TieringPolicy::Custom => {
78 self.evaluate_adaptive(metadata, tier_stats, current_time)
80 }
81 }
82 }
83
84 fn evaluate_lru(
86 &self,
87 metadata: &IndexMetadata,
88 tier_stats: &[TierStatistics; 3],
89 current_time: SystemTime,
90 ) -> (StorageTier, TierTransitionReason) {
91 let time_since_access = metadata
92 .access_stats
93 .last_access_time
94 .and_then(|t| current_time.duration_since(t).ok())
95 .unwrap_or(Duration::from_secs(u64::MAX));
96
97 if time_since_access < Duration::from_secs(3600) {
101 if self.has_capacity(&tier_stats[0], metadata.size_bytes) {
102 (StorageTier::Hot, TierTransitionReason::HighAccessFrequency)
103 } else {
104 (StorageTier::Warm, TierTransitionReason::CapacityExceeded)
105 }
106 } else if time_since_access < Duration::from_secs(86400) {
107 if self.has_capacity(&tier_stats[1], metadata.size_bytes) {
108 (StorageTier::Warm, TierTransitionReason::LowAccessFrequency)
109 } else {
110 (StorageTier::Cold, TierTransitionReason::CapacityExceeded)
111 }
112 } else {
113 (StorageTier::Cold, TierTransitionReason::LowAccessFrequency)
114 }
115 }
116
117 fn evaluate_lfu(
119 &self,
120 metadata: &IndexMetadata,
121 tier_stats: &[TierStatistics; 3],
122 ) -> (StorageTier, TierTransitionReason) {
123 let qps = metadata.access_stats.avg_qps;
124
125 if qps > 10.0 {
129 if self.has_capacity(&tier_stats[0], metadata.size_bytes) {
130 (StorageTier::Hot, TierTransitionReason::HighAccessFrequency)
131 } else {
132 (StorageTier::Warm, TierTransitionReason::CapacityExceeded)
133 }
134 } else if qps > 1.0 {
135 if self.has_capacity(&tier_stats[1], metadata.size_bytes) {
136 (StorageTier::Warm, TierTransitionReason::HighAccessFrequency)
137 } else {
138 (StorageTier::Cold, TierTransitionReason::CapacityExceeded)
139 }
140 } else {
141 (StorageTier::Cold, TierTransitionReason::LowAccessFrequency)
142 }
143 }
144
145 fn evaluate_cost_based(
147 &self,
148 metadata: &IndexMetadata,
149 tier_stats: &[TierStatistics; 3],
150 ) -> (StorageTier, TierTransitionReason) {
151 let hot_cost = self.calculate_tier_cost(metadata, StorageTier::Hot);
153 let warm_cost = self.calculate_tier_cost(metadata, StorageTier::Warm);
154 let cold_cost = self.calculate_tier_cost(metadata, StorageTier::Cold);
155
156 if cold_cost <= warm_cost
158 && cold_cost <= hot_cost
159 && self.has_capacity(&tier_stats[2], metadata.size_bytes)
160 {
161 (StorageTier::Cold, TierTransitionReason::CostOptimization)
162 } else if warm_cost <= hot_cost && self.has_capacity(&tier_stats[1], metadata.size_bytes) {
163 (StorageTier::Warm, TierTransitionReason::CostOptimization)
164 } else if self.has_capacity(&tier_stats[0], metadata.size_bytes) {
165 (StorageTier::Hot, TierTransitionReason::CostOptimization)
166 } else {
167 (StorageTier::Cold, TierTransitionReason::CapacityExceeded)
169 }
170 }
171
172 fn evaluate_size_based(
174 &self,
175 metadata: &IndexMetadata,
176 tier_stats: &[TierStatistics; 3],
177 ) -> (StorageTier, TierTransitionReason) {
178 let size_gb = metadata.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
179
180 if size_gb < 1.0 && self.has_capacity(&tier_stats[0], metadata.size_bytes) {
184 (StorageTier::Hot, TierTransitionReason::LatencyOptimization)
185 } else if size_gb < 10.0 && self.has_capacity(&tier_stats[1], metadata.size_bytes) {
186 (StorageTier::Warm, TierTransitionReason::CostOptimization)
187 } else {
188 (StorageTier::Cold, TierTransitionReason::CostOptimization)
189 }
190 }
191
192 fn evaluate_latency_optimized(
194 &self,
195 metadata: &IndexMetadata,
196 tier_stats: &[TierStatistics; 3],
197 ) -> (StorageTier, TierTransitionReason) {
198 if metadata.access_stats.avg_qps > 0.1 {
200 if self.has_capacity(&tier_stats[0], metadata.size_bytes) {
201 (StorageTier::Hot, TierTransitionReason::LatencyOptimization)
202 } else if self.has_capacity(&tier_stats[1], metadata.size_bytes) {
203 (StorageTier::Warm, TierTransitionReason::LatencyOptimization)
204 } else {
205 (StorageTier::Cold, TierTransitionReason::CapacityExceeded)
206 }
207 } else {
208 (StorageTier::Cold, TierTransitionReason::LowAccessFrequency)
209 }
210 }
211
212 fn evaluate_adaptive(
214 &self,
215 metadata: &IndexMetadata,
216 tier_stats: &[TierStatistics; 3],
217 current_time: SystemTime,
218 ) -> (StorageTier, TierTransitionReason) {
219 let hot_score = self.calculate_adaptive_score(metadata, StorageTier::Hot, current_time);
221 let warm_score = self.calculate_adaptive_score(metadata, StorageTier::Warm, current_time);
222 let cold_score = self.calculate_adaptive_score(metadata, StorageTier::Cold, current_time);
223
224 if hot_score >= warm_score
226 && hot_score >= cold_score
227 && self.has_capacity(&tier_stats[0], metadata.size_bytes)
228 {
229 (StorageTier::Hot, TierTransitionReason::HighAccessFrequency)
230 } else if warm_score >= cold_score && self.has_capacity(&tier_stats[1], metadata.size_bytes)
231 {
232 (StorageTier::Warm, TierTransitionReason::CostOptimization)
233 } else {
234 (StorageTier::Cold, TierTransitionReason::LowAccessFrequency)
235 }
236 }
237
238 fn calculate_adaptive_score(
240 &self,
241 metadata: &IndexMetadata,
242 tier: StorageTier,
243 current_time: SystemTime,
244 ) -> f64 {
245 let mut score = 0.0;
246
247 let qps_factor = metadata.access_stats.avg_qps.min(100.0) / 100.0;
249 score += qps_factor * 0.4;
250
251 let recency_factor = metadata
253 .access_stats
254 .last_access_time
255 .and_then(|t| current_time.duration_since(t).ok())
256 .map(|d| {
257 let hours = d.as_secs() as f64 / 3600.0;
258 1.0 / (1.0 + hours)
259 })
260 .unwrap_or(0.0);
261 score += recency_factor * 0.3;
262
263 let cost = self.calculate_tier_cost(metadata, tier);
265 let cost_factor = 1.0 / (1.0 + cost);
266 score += cost_factor * 0.2;
267
268 let latency_factor = match tier {
270 StorageTier::Hot => 1.0,
271 StorageTier::Warm => 0.5,
272 StorageTier::Cold => 0.1,
273 };
274 score += latency_factor * 0.1;
275
276 score
277 }
278
279 fn calculate_tier_cost(&self, metadata: &IndexMetadata, tier: StorageTier) -> f64 {
281 let size_gb = metadata.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
282 let storage_cost = size_gb * tier.cost_factor();
283 let query_cost = metadata.access_stats.avg_qps * tier.typical_latency().as_secs_f64();
284 storage_cost + query_cost
285 }
286
287 fn has_capacity(&self, tier_stats: &TierStatistics, size_bytes: u64) -> bool {
289 tier_stats.available_bytes() >= size_bytes
290 }
291}
292
293pub fn calculate_access_score(stats: &AccessStatistics) -> f64 {
295 let mut score = 0.0;
296
297 score += (stats.avg_qps.min(100.0) / 100.0) * 0.5;
299
300 score += (stats.peak_qps.min(1000.0) / 1000.0) * 0.2;
302
303 let total_queries_normalized = stats.total_queries.min(1_000_000) as f64 / 1_000_000.0;
305 score += total_queries_normalized * 0.15;
306
307 let recent_activity =
309 (stats.queries_last_hour as f64).max((stats.queries_last_day as f64) / 24.0);
310 score += (recent_activity.min(1000.0) / 1000.0) * 0.15;
311
312 score.clamp(0.0, 1.0)
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::tiering::types::{AccessPattern, IndexType, LatencyPercentiles, PerformanceMetrics};
319 use std::collections::HashMap;
320
321 fn create_test_metadata(qps: f64, size_gb: f64) -> IndexMetadata {
322 IndexMetadata {
323 index_id: "test_index".to_string(),
324 current_tier: StorageTier::Warm,
325 size_bytes: (size_gb * 1024.0 * 1024.0 * 1024.0) as u64,
326 compressed_size_bytes: (size_gb * 512.0 * 1024.0 * 1024.0) as u64,
327 vector_count: 1_000_000,
328 dimension: 768,
329 index_type: IndexType::Hnsw,
330 created_at: SystemTime::now(),
331 last_accessed: SystemTime::now(),
332 last_modified: SystemTime::now(),
333 access_stats: AccessStatistics {
334 total_queries: 100_000,
335 queries_last_hour: (qps * 3600.0) as u64,
336 queries_last_day: (qps * 86400.0) as u64,
337 queries_last_week: (qps * 604800.0) as u64,
338 avg_qps: qps,
339 peak_qps: qps * 2.0,
340 last_access_time: Some(SystemTime::now()),
341 access_pattern: AccessPattern::Hot,
342 query_latencies: LatencyPercentiles::default(),
343 },
344 performance_metrics: PerformanceMetrics::default(),
345 storage_path: None,
346 custom_metadata: HashMap::new(),
347 }
348 }
349
350 fn create_test_tier_stats() -> [TierStatistics; 3] {
351 [
352 TierStatistics {
353 capacity_bytes: 16 * 1024 * 1024 * 1024, used_bytes: 8 * 1024 * 1024 * 1024, ..Default::default()
356 },
357 TierStatistics {
358 capacity_bytes: 128 * 1024 * 1024 * 1024, used_bytes: 64 * 1024 * 1024 * 1024, ..Default::default()
361 },
362 TierStatistics {
363 capacity_bytes: 1024 * 1024 * 1024 * 1024, used_bytes: 256 * 1024 * 1024 * 1024, ..Default::default()
366 },
367 ]
368 }
369
370 #[test]
371 fn test_lfu_policy_high_qps() {
372 let evaluator = PolicyEvaluator::new(TieringPolicy::Lfu);
373 let metadata = create_test_metadata(20.0, 1.0); let tier_stats = create_test_tier_stats();
375
376 let (tier, _reason) = evaluator.evaluate_lfu(&metadata, &tier_stats);
377 assert_eq!(tier, StorageTier::Hot);
378 }
379
380 #[test]
381 fn test_lfu_policy_medium_qps() {
382 let evaluator = PolicyEvaluator::new(TieringPolicy::Lfu);
383 let metadata = create_test_metadata(5.0, 1.0); let tier_stats = create_test_tier_stats();
385
386 let (tier, _reason) = evaluator.evaluate_lfu(&metadata, &tier_stats);
387 assert_eq!(tier, StorageTier::Warm);
388 }
389
390 #[test]
391 fn test_lfu_policy_low_qps() {
392 let evaluator = PolicyEvaluator::new(TieringPolicy::Lfu);
393 let metadata = create_test_metadata(0.5, 1.0); let tier_stats = create_test_tier_stats();
395
396 let (tier, _reason) = evaluator.evaluate_lfu(&metadata, &tier_stats);
397 assert_eq!(tier, StorageTier::Cold);
398 }
399
400 #[test]
401 fn test_size_based_policy() {
402 let evaluator = PolicyEvaluator::new(TieringPolicy::SizeBased);
403 let tier_stats = create_test_tier_stats();
404
405 let small_metadata = create_test_metadata(1.0, 0.5);
407 let (tier, _) = evaluator.evaluate_size_based(&small_metadata, &tier_stats);
408 assert_eq!(tier, StorageTier::Hot);
409
410 let medium_metadata = create_test_metadata(1.0, 5.0);
412 let (tier, _) = evaluator.evaluate_size_based(&medium_metadata, &tier_stats);
413 assert_eq!(tier, StorageTier::Warm);
414
415 let large_metadata = create_test_metadata(1.0, 20.0);
417 let (tier, _) = evaluator.evaluate_size_based(&large_metadata, &tier_stats);
418 assert_eq!(tier, StorageTier::Cold);
419 }
420
421 #[test]
422 fn test_access_score_calculation() {
423 let stats = AccessStatistics {
424 total_queries: 500_000,
425 queries_last_hour: 3600,
426 queries_last_day: 86400,
427 queries_last_week: 604800,
428 avg_qps: 10.0,
429 peak_qps: 20.0,
430 last_access_time: Some(SystemTime::now()),
431 access_pattern: AccessPattern::Hot,
432 query_latencies: LatencyPercentiles::default(),
433 };
434
435 let score = calculate_access_score(&stats);
436 assert!(score > 0.0 && score <= 1.0);
437 }
438}