Skip to main content

elif_cache/
warming.rs

1//! Cache warming and preloading system
2//! 
3//! This module provides functionality to pre-populate cache with commonly needed
4//! data to improve application performance and reduce cold start latency.
5
6use 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/// Cache warming strategy
22#[derive(Debug, Clone)]
23pub enum WarmingStrategy {
24    /// Warm on application startup
25    Startup,
26    /// Warm on a scheduled interval
27    Scheduled(Duration),
28    /// Warm when cache miss rate exceeds threshold
29    MissRateTrigger { threshold: f64, check_interval: Duration },
30    /// Warm on demand (manual trigger)
31    OnDemand,
32    /// Warm based on access patterns
33    AccessPattern { min_access_count: u32, time_window: Duration },
34}
35
36/// Cache warming priority
37#[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/// Cache warming task definition
46#[derive(Debug)]
47pub struct WarmingTask {
48    /// Unique identifier for this warming task
49    pub id: String,
50    /// Description of what this task warms
51    pub description: String,
52    /// Strategy for when to execute this warming
53    pub strategy: WarmingStrategy,
54    /// Priority for execution ordering
55    pub priority: WarmingPriority,
56    /// Tags to apply to warmed cache entries
57    pub tags: Vec<CacheTag>,
58    /// TTL for warmed entries
59    pub ttl: Option<Duration>,
60    /// Maximum time to spend on this warming task
61    pub timeout: Duration,
62    /// Whether this task can run concurrently with others
63    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), // 5 minute default timeout
76            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/// Result of a cache warming execution
112#[derive(Debug, Clone)]
113pub struct WarmingResult {
114    /// Task ID that was executed
115    pub task_id: String,
116    /// Number of entries successfully warmed
117    pub entries_warmed: u32,
118    /// Number of entries that failed to warm
119    pub entries_failed: u32,
120    /// Time taken to complete the warming
121    pub duration: Duration,
122    /// Any error that occurred during warming
123    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/// Cache warmer trait for implementing custom warming logic
176#[async_trait]
177pub trait CacheWarmer: Send + Sync {
178    /// Execute warming for a specific task
179    async fn warm(&self, task: &WarmingTask) -> CacheResult<WarmingResult>;
180    
181    /// Check if warming should be triggered based on the strategy
182    async fn should_warm(&self, task: &WarmingTask) -> CacheResult<bool>;
183    
184    /// Get cache statistics for warming decisions
185    async fn get_warming_stats(&self) -> CacheResult<WarmingStats>;
186}
187
188/// Statistics for cache warming decisions
189#[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
214/// Function type for warming data providers
215pub type WarmingProvider<T> = Box<dyn Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = CacheResult<Vec<(CacheKey, T)>>> + Send>> + Send + Sync>;
216
217/// Cache warming manager
218pub 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    /// Register a warming task with its data provider
240    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        // Store the task
247        {
248            let mut tasks = self.tasks.write().await;
249            tasks.insert(task_id.clone(), task);
250        }
251        
252        // Store the provider (type-erased)
253        {
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    /// Register a simple warming task for a single key-value pair
263    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    /// Start the warming scheduler
290    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)); // Check every minute
305            
306            loop {
307                interval.tick().await;
308                
309                // Get tasks that should be warmed
310                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 // First run
322                                };
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                            _ => {} // Other strategies handled elsewhere
341                        }
342                    }
343                    
344                    // Sort by priority (highest first)
345                    candidates.sort_by(|a, b| b.1.cmp(&a.1));
346                    candidates.into_iter().map(|(id, _)| id).collect::<Vec<_>>()
347                };
348                
349                // Execute warming tasks
350                for task_id in tasks_to_warm {
351                    let running_guard = running_tasks.lock().await;
352                    if running_guard.contains(&task_id) {
353                        continue; // Skip if already running
354                    }
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                        // Mark as running
366                        {
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                                // Update stats
384                                {
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                                    // Update average duration
390                                    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                        // Mark as no longer running
404                        {
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    /// Stop the warming scheduler
419    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    /// Manually trigger warming for a specific task
431    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        // Mark as running
439        {
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        // Mark as no longer running
452        {
453            let mut running_guard = self.running_tasks.lock().await;
454            running_guard.remove(task_id);
455        }
456        
457        result
458    }
459    
460    /// Execute a warming task
461    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        // Check if task exists
470        {
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        // Get provider (this is complex due to type erasure)
478        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        // For now, we'll return a simple success result
487        // In a real implementation, we'd need to properly handle the type-erased provider
488        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    /// Warm on startup - execute all startup warming tasks
495    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        // Sort by priority (highest first)
509        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    /// Get warming statistics
533    pub async fn get_stats(&self) -> WarmingStats {
534        self.stats.read().await.clone()
535    }
536    
537    /// Get list of registered tasks
538    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    /// Remove a warming task
544    pub async fn remove_task(&self, task_id: &str) -> CacheResult<bool> {
545        // Check if task is running
546        {
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        // Remove task and provider
554        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        // Note: In a real implementation, we'd want to gracefully shutdown
571        // the scheduler, but Drop is not async, so we can't await here.
572    }
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()); // Has failed entries, so not full success
617        assert!(partial.is_partial_success()); // But has some warmed entries
618        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        // Test edge case with no data
633        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        // Try to remove non-existent task
702        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        // Register a startup task
712        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        // Execute startup warming
726        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}