oxirs_vec/tiering/
policies.rs

1//! Tiering policies for index placement and transition decisions
2
3use super::types::{AccessStatistics, IndexMetadata, StorageTier, TierStatistics};
4use serde::{Deserialize, Serialize};
5use std::time::{Duration, SystemTime};
6
7/// Tiering policy for determining index placement
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
9pub enum TieringPolicy {
10    /// Least Recently Used (LRU)
11    Lru,
12    /// Least Frequently Used (LFU)
13    Lfu,
14    /// Cost-based optimization
15    CostBased,
16    /// Size-aware placement
17    SizeBased,
18    /// Latency-sensitive optimization
19    LatencyOptimized,
20    /// Adaptive policy (ML-driven)
21    #[default]
22    Adaptive,
23    /// Custom policy with user-defined rules
24    Custom,
25}
26
27/// Reason for tier transition
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub enum TierTransitionReason {
30    /// High access frequency (promotion)
31    HighAccessFrequency,
32    /// Low access frequency (demotion)
33    LowAccessFrequency,
34    /// Tier capacity exceeded
35    CapacityExceeded,
36    /// Cost optimization
37    CostOptimization,
38    /// Latency optimization
39    LatencyOptimization,
40    /// Manual intervention
41    Manual,
42    /// Predictive (ML-driven)
43    Predictive,
44    /// SLA requirements
45    SlaRequirement,
46    /// Emergency (e.g., out of memory)
47    Emergency,
48}
49
50/// Policy evaluator for tier placement decisions
51pub struct PolicyEvaluator {
52    policy: TieringPolicy,
53}
54
55impl PolicyEvaluator {
56    /// Create a new policy evaluator
57    pub fn new(policy: TieringPolicy) -> Self {
58        Self { policy }
59    }
60
61    /// Evaluate optimal tier for an index
62    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                // Default to adaptive for custom policy
79                self.evaluate_adaptive(metadata, tier_stats, current_time)
80            }
81        }
82    }
83
84    /// LRU policy: Place in tier based on recency of access
85    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        // Hot: accessed in last 1 hour
98        // Warm: accessed in last 24 hours
99        // Cold: accessed more than 24 hours ago
100        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    /// LFU policy: Place in tier based on access frequency
118    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        // Hot: > 10 QPS
126        // Warm: 1-10 QPS
127        // Cold: < 1 QPS
128        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    /// Cost-based policy: Minimize cost while meeting performance requirements
146    fn evaluate_cost_based(
147        &self,
148        metadata: &IndexMetadata,
149        tier_stats: &[TierStatistics; 3],
150    ) -> (StorageTier, TierTransitionReason) {
151        // Calculate cost for each tier
152        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        // Choose tier with minimum cost that has capacity
157        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            // Fall back to cold if no capacity
168            (StorageTier::Cold, TierTransitionReason::CapacityExceeded)
169        }
170    }
171
172    /// Size-based policy: Large indices in cold tier, small in hot tier
173    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        // Hot: < 1 GB
181        // Warm: 1-10 GB
182        // Cold: > 10 GB
183        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    /// Latency-optimized policy: Prioritize query latency
193    fn evaluate_latency_optimized(
194        &self,
195        metadata: &IndexMetadata,
196        tier_stats: &[TierStatistics; 3],
197    ) -> (StorageTier, TierTransitionReason) {
198        // If actively queried, keep in fastest tier available
199        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    /// Adaptive policy: Combine multiple factors
213    fn evaluate_adaptive(
214        &self,
215        metadata: &IndexMetadata,
216        tier_stats: &[TierStatistics; 3],
217        current_time: SystemTime,
218    ) -> (StorageTier, TierTransitionReason) {
219        // Calculate scores for each tier (higher is better)
220        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        // Choose tier with highest score that has capacity
225        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    /// Calculate adaptive score for tier placement
239    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        // Factor 1: Access frequency (40% weight)
248        let qps_factor = metadata.access_stats.avg_qps.min(100.0) / 100.0;
249        score += qps_factor * 0.4;
250
251        // Factor 2: Recency (30% weight)
252        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        // Factor 3: Cost efficiency (20% weight)
264        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        // Factor 4: Latency sensitivity (10% weight)
269        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    /// Calculate cost for placing index in a tier
280    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    /// Check if tier has capacity for index
288    fn has_capacity(&self, tier_stats: &TierStatistics, size_bytes: u64) -> bool {
289        tier_stats.available_bytes() >= size_bytes
290    }
291}
292
293/// Calculate access score for tier promotion/demotion decisions
294pub fn calculate_access_score(stats: &AccessStatistics) -> f64 {
295    let mut score = 0.0;
296
297    // Recent access frequency (50% weight)
298    score += (stats.avg_qps.min(100.0) / 100.0) * 0.5;
299
300    // Peak QPS (20% weight)
301    score += (stats.peak_qps.min(1000.0) / 1000.0) * 0.2;
302
303    // Total queries (15% weight)
304    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    // Recent activity (15% weight)
308    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, // 16 GB
354                used_bytes: 8 * 1024 * 1024 * 1024,      // 8 GB used
355                ..Default::default()
356            },
357            TierStatistics {
358                capacity_bytes: 128 * 1024 * 1024 * 1024, // 128 GB
359                used_bytes: 64 * 1024 * 1024 * 1024,      // 64 GB used
360                ..Default::default()
361            },
362            TierStatistics {
363                capacity_bytes: 1024 * 1024 * 1024 * 1024, // 1 TB
364                used_bytes: 256 * 1024 * 1024 * 1024,      // 256 GB used
365                ..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); // 20 QPS, 1 GB
374        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); // 5 QPS, 1 GB
384        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); // 0.5 QPS, 1 GB
394        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        // Small index -> Hot
406        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        // Medium index -> Warm
411        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        // Large index -> Cold
416        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}