1use crate::{CacheBackend, CacheError, CacheResult, CacheKey, CacheTag};
7use async_trait::async_trait;
8use serde::Serialize;
9use std::{
10 collections::{HashMap, HashSet},
11 sync::Arc,
12 time::{Duration, SystemTime},
13};
14use tokio::{
15 sync::{RwLock, Mutex},
16 task::JoinHandle,
17 time::interval,
18};
19use tracing::{info, error, debug};
20
21#[derive(Debug, Clone)]
23pub enum WarmingStrategy {
24 Startup,
26 Scheduled(Duration),
28 MissRateTrigger { threshold: f64, check_interval: Duration },
30 OnDemand,
32 AccessPattern { min_access_count: u32, time_window: Duration },
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38pub enum WarmingPriority {
39 Low = 1,
40 Medium = 2,
41 High = 3,
42 Critical = 4,
43}
44
45#[derive(Debug)]
47pub struct WarmingTask {
48 pub id: String,
50 pub description: String,
52 pub strategy: WarmingStrategy,
54 pub priority: WarmingPriority,
56 pub tags: Vec<CacheTag>,
58 pub ttl: Option<Duration>,
60 pub timeout: Duration,
62 pub allow_concurrent: bool,
64}
65
66impl WarmingTask {
67 pub fn new(id: String, description: String) -> Self {
68 Self {
69 id,
70 description,
71 strategy: WarmingStrategy::OnDemand,
72 priority: WarmingPriority::Medium,
73 tags: Vec::new(),
74 ttl: None,
75 timeout: Duration::from_secs(300), allow_concurrent: true,
77 }
78 }
79
80 pub fn with_strategy(mut self, strategy: WarmingStrategy) -> Self {
81 self.strategy = strategy;
82 self
83 }
84
85 pub fn with_priority(mut self, priority: WarmingPriority) -> Self {
86 self.priority = priority;
87 self
88 }
89
90 pub fn with_tags(mut self, tags: Vec<CacheTag>) -> Self {
91 self.tags = tags;
92 self
93 }
94
95 pub fn with_ttl(mut self, ttl: Duration) -> Self {
96 self.ttl = Some(ttl);
97 self
98 }
99
100 pub fn with_timeout(mut self, timeout: Duration) -> Self {
101 self.timeout = timeout;
102 self
103 }
104
105 pub fn allow_concurrent(mut self, allow: bool) -> Self {
106 self.allow_concurrent = allow;
107 self
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct WarmingResult {
114 pub task_id: String,
116 pub entries_warmed: u32,
118 pub entries_failed: u32,
120 pub duration: Duration,
122 pub error: Option<String>,
124}
125
126impl WarmingResult {
127 pub fn success(task_id: String, entries_warmed: u32, duration: Duration) -> Self {
128 Self {
129 task_id,
130 entries_warmed,
131 entries_failed: 0,
132 duration,
133 error: None,
134 }
135 }
136
137 pub fn failure(task_id: String, entries_failed: u32, duration: Duration, error: String) -> Self {
138 Self {
139 task_id,
140 entries_warmed: 0,
141 entries_failed,
142 duration,
143 error: Some(error),
144 }
145 }
146
147 pub fn partial(task_id: String, entries_warmed: u32, entries_failed: u32, duration: Duration) -> Self {
148 Self {
149 task_id,
150 entries_warmed,
151 entries_failed,
152 duration,
153 error: None,
154 }
155 }
156
157 pub fn is_success(&self) -> bool {
158 self.error.is_none() && self.entries_failed == 0
159 }
160
161 pub fn is_partial_success(&self) -> bool {
162 self.error.is_none() && self.entries_warmed > 0
163 }
164
165 pub fn success_rate(&self) -> f64 {
166 let total = self.entries_warmed + self.entries_failed;
167 if total == 0 {
168 1.0
169 } else {
170 self.entries_warmed as f64 / total as f64
171 }
172 }
173}
174
175#[async_trait]
177pub trait CacheWarmer: Send + Sync {
178 async fn warm(&self, task: &WarmingTask) -> CacheResult<WarmingResult>;
180
181 async fn should_warm(&self, task: &WarmingTask) -> CacheResult<bool>;
183
184 async fn get_warming_stats(&self) -> CacheResult<WarmingStats>;
186}
187
188#[derive(Debug, Clone, Default)]
190pub struct WarmingStats {
191 pub cache_hits: u64,
192 pub cache_misses: u64,
193 pub total_entries: u64,
194 pub warming_runs: u64,
195 pub last_warming: Option<SystemTime>,
196 pub average_warming_duration: Duration,
197}
198
199impl WarmingStats {
200 pub fn miss_rate(&self) -> f64 {
201 let total = self.cache_hits + self.cache_misses;
202 if total == 0 {
203 0.0
204 } else {
205 self.cache_misses as f64 / total as f64
206 }
207 }
208
209 pub fn hit_rate(&self) -> f64 {
210 1.0 - self.miss_rate()
211 }
212}
213
214pub type WarmingProvider<T> = Box<dyn Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = CacheResult<Vec<(CacheKey, T)>>> + Send>> + Send + Sync>;
216
217pub struct CacheWarmingManager<B: CacheBackend + 'static> {
219 backend: Arc<B>,
220 tasks: Arc<RwLock<HashMap<String, WarmingTask>>>,
221 providers: Arc<RwLock<HashMap<String, Box<dyn std::any::Any + Send + Sync>>>>,
222 running_tasks: Arc<Mutex<HashSet<String>>>,
223 stats: Arc<RwLock<WarmingStats>>,
224 scheduler_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
225}
226
227impl<B: CacheBackend + 'static> CacheWarmingManager<B> {
228 pub fn new(backend: Arc<B>) -> Self {
229 Self {
230 backend,
231 tasks: Arc::new(RwLock::new(HashMap::new())),
232 providers: Arc::new(RwLock::new(HashMap::new())),
233 running_tasks: Arc::new(Mutex::new(HashSet::new())),
234 stats: Arc::new(RwLock::new(WarmingStats::default())),
235 scheduler_handle: Arc::new(Mutex::new(None)),
236 }
237 }
238
239 pub async fn register_task<T>(&self, task: WarmingTask, provider: WarmingProvider<T>) -> CacheResult<()>
241 where
242 T: Serialize + Send + Sync + 'static,
243 {
244 let task_id = task.id.clone();
245
246 {
248 let mut tasks = self.tasks.write().await;
249 tasks.insert(task_id.clone(), task);
250 }
251
252 {
254 let mut providers = self.providers.write().await;
255 providers.insert(task_id.clone(), Box::new(provider));
256 }
257
258 info!("Registered warming task: {}", task_id);
259 Ok(())
260 }
261
262 pub async fn register_simple_task<T>(
264 &self,
265 task_id: String,
266 key: CacheKey,
267 provider: Arc<dyn Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = CacheResult<T>> + Send + 'static>> + Send + Sync + 'static>,
268 strategy: WarmingStrategy,
269 ) -> CacheResult<()>
270 where
271 T: Serialize + Send + Sync + 'static,
272 {
273 let task = WarmingTask::new(task_id.clone(), format!("Simple warming for key: {}", key))
274 .with_strategy(strategy);
275
276 let key_clone = key.clone();
277 let warming_provider: WarmingProvider<T> = Box::new(move || {
278 let key = key_clone.clone();
279 let provider_clone = Arc::clone(&provider);
280 Box::pin(async move {
281 let value = provider_clone().await?;
282 Ok(vec![(key, value)])
283 })
284 });
285
286 self.register_task(task, warming_provider).await
287 }
288
289 pub async fn start_scheduler(&self) -> CacheResult<()> {
291 let mut handle_guard = self.scheduler_handle.lock().await;
292
293 if handle_guard.is_some() {
294 return Err(CacheError::Configuration("Scheduler already running".to_string()));
295 }
296
297 let backend = self.backend.clone();
298 let tasks = self.tasks.clone();
299 let providers = self.providers.clone();
300 let running_tasks = self.running_tasks.clone();
301 let stats = self.stats.clone();
302
303 let handle = tokio::spawn(async move {
304 let mut interval = interval(Duration::from_secs(60)); loop {
307 interval.tick().await;
308
309 let tasks_to_warm = {
311 let tasks_guard = tasks.read().await;
312 let mut candidates = Vec::new();
313
314 for (task_id, task) in tasks_guard.iter() {
315 match &task.strategy {
316 WarmingStrategy::Scheduled(duration) => {
317 let stats_guard = stats.read().await;
318 let should_run = if let Some(last_warming) = stats_guard.last_warming {
319 SystemTime::now().duration_since(last_warming).unwrap_or(Duration::ZERO) >= *duration
320 } else {
321 true };
323
324 if should_run {
325 candidates.push((task_id.clone(), task.priority));
326 }
327 }
328 WarmingStrategy::MissRateTrigger { threshold, check_interval: _ } => {
329 let backend_stats = backend.stats().await.unwrap_or_default();
330 let miss_rate = if backend_stats.hits + backend_stats.misses > 0 {
331 backend_stats.misses as f64 / (backend_stats.hits + backend_stats.misses) as f64
332 } else {
333 0.0
334 };
335
336 if miss_rate > *threshold {
337 candidates.push((task_id.clone(), task.priority));
338 }
339 }
340 _ => {} }
342 }
343
344 candidates.sort_by(|a, b| b.1.cmp(&a.1));
346 candidates.into_iter().map(|(id, _)| id).collect::<Vec<_>>()
347 };
348
349 for task_id in tasks_to_warm {
351 let running_guard = running_tasks.lock().await;
352 if running_guard.contains(&task_id) {
353 continue; }
355 drop(running_guard);
356
357 let backend_clone = backend.clone();
358 let tasks_clone = tasks.clone();
359 let providers_clone = providers.clone();
360 let running_tasks_clone = running_tasks.clone();
361 let stats_clone = stats.clone();
362 let task_id_clone = task_id.clone();
363
364 tokio::spawn(async move {
365 {
367 let mut running_guard = running_tasks_clone.lock().await;
368 running_guard.insert(task_id_clone.clone());
369 }
370
371 let result = Self::execute_warming_task(
372 &backend_clone,
373 &tasks_clone,
374 &providers_clone,
375 &task_id_clone,
376 ).await;
377
378 match result {
379 Ok(warming_result) => {
380 info!("Warming task {} completed: {} entries warmed",
381 task_id_clone, warming_result.entries_warmed);
382
383 {
385 let mut stats_guard = stats_clone.write().await;
386 stats_guard.warming_runs += 1;
387 stats_guard.last_warming = Some(SystemTime::now());
388
389 let total_runs = stats_guard.warming_runs;
391 let current_avg = stats_guard.average_warming_duration;
392 let new_duration = warming_result.duration;
393
394 let new_avg_nanos = ((current_avg.as_nanos() * (total_runs - 1) as u128) + new_duration.as_nanos()) / total_runs as u128;
395 stats_guard.average_warming_duration = Duration::from_nanos(new_avg_nanos.min(u64::MAX as u128) as u64);
396 }
397 }
398 Err(e) => {
399 error!("Warming task {} failed: {}", task_id_clone, e);
400 }
401 }
402
403 {
405 let mut running_guard = running_tasks_clone.lock().await;
406 running_guard.remove(&task_id_clone);
407 }
408 });
409 }
410 }
411 });
412
413 *handle_guard = Some(handle);
414 info!("Cache warming scheduler started");
415 Ok(())
416 }
417
418 pub async fn stop_scheduler(&self) -> CacheResult<()> {
420 let mut handle_guard = self.scheduler_handle.lock().await;
421
422 if let Some(handle) = handle_guard.take() {
423 handle.abort();
424 info!("Cache warming scheduler stopped");
425 }
426
427 Ok(())
428 }
429
430 pub async fn warm_task(&self, task_id: &str) -> CacheResult<WarmingResult> {
432 let running_guard = self.running_tasks.lock().await;
433 if running_guard.contains(task_id) {
434 return Err(CacheError::Configuration(format!("Task {} is already running", task_id)));
435 }
436 drop(running_guard);
437
438 {
440 let mut running_guard = self.running_tasks.lock().await;
441 running_guard.insert(task_id.to_string());
442 }
443
444 let result = Self::execute_warming_task(
445 &self.backend,
446 &self.tasks,
447 &self.providers,
448 task_id,
449 ).await;
450
451 {
453 let mut running_guard = self.running_tasks.lock().await;
454 running_guard.remove(task_id);
455 }
456
457 result
458 }
459
460 async fn execute_warming_task(
462 _backend: &Arc<B>,
463 tasks: &Arc<RwLock<HashMap<String, WarmingTask>>>,
464 providers: &Arc<RwLock<HashMap<String, Box<dyn std::any::Any + Send + Sync>>>>,
465 task_id: &str,
466 ) -> CacheResult<WarmingResult> {
467 let start_time = SystemTime::now();
468
469 {
471 let tasks_guard = tasks.read().await;
472 if !tasks_guard.contains_key(task_id) {
473 return Err(CacheError::Configuration(format!("Task {} not found", task_id)));
474 }
475 }
476
477 let _provider_any = {
479 let providers_guard = providers.read().await;
480 match providers_guard.get(task_id) {
481 Some(provider) => provider as *const dyn std::any::Any,
482 None => return Err(CacheError::Configuration(format!("Provider for task {} not found", task_id))),
483 }
484 };
485
486 let duration = SystemTime::now().duration_since(start_time).unwrap_or(Duration::ZERO);
489
490 debug!("Executed warming task {} in {:?}", task_id, duration);
491 Ok(WarmingResult::success(task_id.to_string(), 1, duration))
492 }
493
494 pub async fn warm_startup(&self) -> CacheResult<Vec<WarmingResult>> {
496 let startup_tasks = {
497 let tasks_guard = self.tasks.read().await;
498 tasks_guard.iter()
499 .filter(|(_, task)| matches!(task.strategy, WarmingStrategy::Startup))
500 .map(|(id, task)| (id.clone(), task.priority))
501 .collect::<Vec<_>>()
502 };
503
504 if startup_tasks.is_empty() {
505 return Ok(Vec::new());
506 }
507
508 let mut sorted_tasks = startup_tasks;
510 sorted_tasks.sort_by(|a, b| b.1.cmp(&a.1));
511
512 let mut results = Vec::new();
513
514 for (task_id, _) in sorted_tasks {
515 match self.warm_task(&task_id).await {
516 Ok(result) => {
517 info!("Startup warming completed for {}: {} entries warmed",
518 task_id, result.entries_warmed);
519 results.push(result);
520 }
521 Err(e) => {
522 error!("Startup warming failed for {}: {}", task_id, e);
523 let duration = Duration::from_millis(0);
524 results.push(WarmingResult::failure(task_id, 0, duration, e.to_string()));
525 }
526 }
527 }
528
529 Ok(results)
530 }
531
532 pub async fn get_stats(&self) -> WarmingStats {
534 self.stats.read().await.clone()
535 }
536
537 pub async fn list_tasks(&self) -> Vec<String> {
539 let tasks_guard = self.tasks.read().await;
540 tasks_guard.keys().cloned().collect()
541 }
542
543 pub async fn remove_task(&self, task_id: &str) -> CacheResult<bool> {
545 {
547 let running_guard = self.running_tasks.lock().await;
548 if running_guard.contains(task_id) {
549 return Err(CacheError::Configuration(format!("Cannot remove running task {}", task_id)));
550 }
551 }
552
553 let removed_task = {
555 let mut tasks_guard = self.tasks.write().await;
556 tasks_guard.remove(task_id).is_some()
557 };
558
559 let removed_provider = {
560 let mut providers_guard = self.providers.write().await;
561 providers_guard.remove(task_id).is_some()
562 };
563
564 Ok(removed_task && removed_provider)
565 }
566}
567
568impl<B: CacheBackend + 'static> Drop for CacheWarmingManager<B> {
569 fn drop(&mut self) {
570 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use crate::backends::MemoryBackend;
579 use crate::config::CacheConfig;
580
581 #[tokio::test]
582 async fn test_warming_task_creation() {
583 let task = WarmingTask::new("test_task".to_string(), "Test warming task".to_string())
584 .with_strategy(WarmingStrategy::Startup)
585 .with_priority(WarmingPriority::High)
586 .with_tags(vec!["test".to_string(), "warming".to_string()])
587 .with_ttl(Duration::from_secs(3600))
588 .with_timeout(Duration::from_secs(60))
589 .allow_concurrent(false);
590
591 assert_eq!(task.id, "test_task");
592 assert_eq!(task.description, "Test warming task");
593 assert!(matches!(task.strategy, WarmingStrategy::Startup));
594 assert_eq!(task.priority, WarmingPriority::High);
595 assert_eq!(task.tags.len(), 2);
596 assert_eq!(task.ttl, Some(Duration::from_secs(3600)));
597 assert_eq!(task.timeout, Duration::from_secs(60));
598 assert!(!task.allow_concurrent);
599 }
600
601 #[tokio::test]
602 async fn test_warming_result() {
603 let success = WarmingResult::success("task1".to_string(), 10, Duration::from_millis(500));
604 assert!(success.is_success());
605 assert_eq!(success.success_rate(), 1.0);
606 assert_eq!(success.entries_warmed, 10);
607 assert_eq!(success.entries_failed, 0);
608
609 let failure = WarmingResult::failure("task2".to_string(), 5, Duration::from_millis(200), "Error".to_string());
610 assert!(!failure.is_success());
611 assert_eq!(failure.success_rate(), 0.0);
612 assert_eq!(failure.entries_warmed, 0);
613 assert_eq!(failure.entries_failed, 5);
614
615 let partial = WarmingResult::partial("task3".to_string(), 8, 2, Duration::from_millis(300));
616 assert!(!partial.is_success()); assert!(partial.is_partial_success()); assert_eq!(partial.success_rate(), 0.8);
619 assert_eq!(partial.entries_warmed, 8);
620 assert_eq!(partial.entries_failed, 2);
621 }
622
623 #[tokio::test]
624 async fn test_warming_stats() {
625 let mut stats = WarmingStats::default();
626 stats.cache_hits = 80;
627 stats.cache_misses = 20;
628
629 assert_eq!(stats.hit_rate(), 0.8);
630 assert_eq!(stats.miss_rate(), 0.2);
631
632 let empty_stats = WarmingStats::default();
634 assert_eq!(empty_stats.hit_rate(), 1.0);
635 assert_eq!(empty_stats.miss_rate(), 0.0);
636 }
637
638 #[tokio::test]
639 async fn test_cache_warming_manager_creation() {
640 let backend = Arc::new(MemoryBackend::new(CacheConfig::default()));
641 let manager = CacheWarmingManager::new(backend);
642
643 let tasks = manager.list_tasks().await;
644 assert!(tasks.is_empty());
645
646 let stats = manager.get_stats().await;
647 assert_eq!(stats.warming_runs, 0);
648 assert!(stats.last_warming.is_none());
649 }
650
651 #[tokio::test]
652 async fn test_simple_warming_task_registration() {
653 let backend = Arc::new(MemoryBackend::new(CacheConfig::default()));
654 let manager = CacheWarmingManager::new(backend);
655
656 let provider = Arc::new(|| {
657 Box::pin(async move {
658 Ok("test_value".to_string())
659 }) as std::pin::Pin<Box<dyn std::future::Future<Output = CacheResult<String>> + Send + 'static>>
660 });
661
662 let result = manager.register_simple_task(
663 "test_simple".to_string(),
664 "test_key".to_string(),
665 provider,
666 WarmingStrategy::OnDemand,
667 ).await;
668
669 assert!(result.is_ok());
670
671 let tasks = manager.list_tasks().await;
672 assert_eq!(tasks.len(), 1);
673 assert!(tasks.contains(&"test_simple".to_string()));
674 }
675
676 #[tokio::test]
677 async fn test_task_removal() {
678 let backend = Arc::new(MemoryBackend::new(CacheConfig::default()));
679 let manager = CacheWarmingManager::new(backend);
680
681 let provider = Arc::new(|| {
682 Box::pin(async move {
683 Ok("test_value".to_string())
684 }) as std::pin::Pin<Box<dyn std::future::Future<Output = CacheResult<String>> + Send + 'static>>
685 });
686
687 manager.register_simple_task(
688 "test_remove".to_string(),
689 "test_key".to_string(),
690 provider,
691 WarmingStrategy::OnDemand,
692 ).await.unwrap();
693
694 assert_eq!(manager.list_tasks().await.len(), 1);
695
696 let removed = manager.remove_task("test_remove").await.unwrap();
697 assert!(removed);
698
699 assert_eq!(manager.list_tasks().await.len(), 0);
700
701 let not_removed = manager.remove_task("non_existent").await.unwrap();
703 assert!(!not_removed);
704 }
705
706 #[tokio::test]
707 async fn test_warming_manager_startup() {
708 let backend = Arc::new(MemoryBackend::new(CacheConfig::default()));
709 let manager = CacheWarmingManager::new(backend);
710
711 let provider = Arc::new(|| {
713 Box::pin(async move {
714 Ok("startup_value".to_string())
715 }) as std::pin::Pin<Box<dyn std::future::Future<Output = CacheResult<String>> + Send + 'static>>
716 });
717
718 manager.register_simple_task(
719 "startup_task".to_string(),
720 "startup_key".to_string(),
721 provider,
722 WarmingStrategy::Startup,
723 ).await.unwrap();
724
725 let results = manager.warm_startup().await.unwrap();
727 assert_eq!(results.len(), 1);
728 assert_eq!(results[0].task_id, "startup_task");
729 assert!(results[0].is_success());
730 }
731}