Skip to main content

sz_rust_core/
cache_warmer.rs

1//! 缓存预热机制(Cache Warmer)
2//!
3//! 部署时或应用启动时预热缓存,避免冷启动高延迟。对齐 PHP `think console`
4//! 的 `cache:warmup` 命令设计:
5//!
6//! ```text
7//! php think cache:warmup         # 执行所有预热器
8//! php think cache:warmup config  # 只执行 config 预热器
9//! php think cache:clear --warm   # 清空后立即预热
10//! ```
11//!
12//! ## 设计原则
13//!
14//! - **异步执行**:所有预热器实现 `async fn warm()`,不阻塞调度线程
15//! - **失败隔离**:单个预热器失败不影响其他预热器,错误记录到 `WarmupReport`
16//! - **超时控制**:每个预热器有独立超时(默认 30s),避免无限等待
17//! - **可观测**:返回 `WarmupReport` 记录每个预热器的耗时、状态、错误信息
18//! - **可组合**:通过 `WarmupPipeline` 串行或并行执行多个预热器
19//!
20//! ## 使用示例
21//!
22//! ```ignore
23//! use sz_rust_core::cache_warmer::{WarmupPipeline, WarmupReport, Warmer};
24//!
25//! // 1. 注册预热器
26//! let mut pipeline = WarmupPipeline::new();
27//! pipeline.register(Box::new(ConfigWarmer::new()));
28//! pipeline.register(Box::new(RouteWarmer::new()));
29//!
30//! // 2. 执行预热
31//! let report = pipeline.warm_all().await;
32//!
33//! // 3. 检查结果
34//! for item in &report.items {
35//!     println!("{}: {:?} ({}ms)", item.name, item.status, item.duration_ms);
36//! }
37//! ```
38
39use std::sync::Arc;
40use std::time::{Duration, Instant};
41
42use async_trait::async_trait;
43use tokio::sync::Mutex;
44
45// ============================================================================
46// 预热器 trait
47// ============================================================================
48
49/// 预热器 trait — 定义缓存预热接口
50///
51/// 每个预热器负责预热一类缓存(如配置、路由、字典数据等)。
52/// 实现方需提供 `name` / `warm` 方法,可选 `timeout` / `description`。
53///
54/// 使用 `async_trait` 宏确保 trait 是 dyn-compatible(可用 `Box<dyn Warmer>`)。
55#[async_trait]
56pub trait Warmer: Send + Sync {
57    /// 预热器名称(唯一标识,用于日志和报告)
58    fn name(&self) -> &str;
59
60    /// 预热器描述(人类可读)
61    fn description(&self) -> &str {
62        "Cache warmer"
63    }
64
65    /// 单个预热器超时时间(默认 30 秒)
66    ///
67    /// 超时后该预热器被中止,记录为 `WarmupStatus::Timeout`。
68    fn timeout(&self) -> Duration {
69        Duration::from_secs(30)
70    }
71
72    /// 执行预热
73    ///
74    /// 返回 `Result<(), WarmupError>`:
75    /// - `Ok(())`:预热成功
76    /// - `Err(e)`:预热失败(错误信息记录到报告中)
77    async fn warm(&self) -> Result<(), WarmupError>;
78}
79
80// ============================================================================
81// 错误类型
82// ============================================================================
83
84/// 预热错误
85#[derive(Debug, Clone)]
86pub enum WarmupError {
87    /// IO 错误(如配置文件读取失败)
88    Io(String),
89    /// 序列化/反序列化错误
90    Serialize(String),
91    /// 缓存写入错误
92    Cache(String),
93    /// 数据库查询错误
94    Database(String),
95    /// 自定义错误
96    Custom(String),
97}
98
99impl std::fmt::Display for WarmupError {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            Self::Io(msg) => write!(f, "IO error: {}", msg),
103            Self::Serialize(msg) => write!(f, "Serialize error: {}", msg),
104            Self::Cache(msg) => write!(f, "Cache error: {}", msg),
105            Self::Database(msg) => write!(f, "Database error: {}", msg),
106            Self::Custom(msg) => write!(f, "{}", msg),
107        }
108    }
109}
110
111impl std::error::Error for WarmupError {}
112
113impl From<std::io::Error> for WarmupError {
114    fn from(e: std::io::Error) -> Self {
115        Self::Io(e.to_string())
116    }
117}
118
119// ============================================================================
120// 预热状态与报告
121// ============================================================================
122
123/// 单个预热器的执行状态
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum WarmupStatus {
126    /// 成功
127    Success,
128    /// 失败
129    Failed,
130    /// 超时
131    Timeout,
132    /// 跳过(如配置禁用)
133    Skipped,
134}
135
136impl std::fmt::Display for WarmupStatus {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        match self {
139            Self::Success => write!(f, "success"),
140            Self::Failed => write!(f, "failed"),
141            Self::Timeout => write!(f, "timeout"),
142            Self::Skipped => write!(f, "skipped"),
143        }
144    }
145}
146
147/// 单个预热器的执行结果
148#[derive(Debug, Clone)]
149pub struct WarmupItem {
150    /// 预热器名称
151    pub name: String,
152    /// 预热器描述
153    pub description: String,
154    /// 执行状态
155    pub status: WarmupStatus,
156    /// 执行耗时(毫秒)
157    pub duration_ms: u64,
158    /// 错误信息(仅在失败/超时时有值)
159    pub error: Option<String>,
160}
161
162impl WarmupItem {
163    /// 创建成功结果
164    pub fn success(
165        name: impl Into<String>,
166        description: impl Into<String>,
167        duration_ms: u64,
168    ) -> Self {
169        Self {
170            name: name.into(),
171            description: description.into(),
172            status: WarmupStatus::Success,
173            duration_ms,
174            error: None,
175        }
176    }
177
178    /// 创建失败结果
179    pub fn failed(
180        name: impl Into<String>,
181        description: impl Into<String>,
182        duration_ms: u64,
183        error: impl Into<String>,
184    ) -> Self {
185        Self {
186            name: name.into(),
187            description: description.into(),
188            status: WarmupStatus::Failed,
189            duration_ms,
190            error: Some(error.into()),
191        }
192    }
193
194    /// 创建超时结果
195    pub fn timeout(
196        name: impl Into<String>,
197        description: impl Into<String>,
198        duration_ms: u64,
199    ) -> Self {
200        Self {
201            name: name.into(),
202            description: description.into(),
203            status: WarmupStatus::Timeout,
204            duration_ms,
205            error: Some(format!("Timed out after {}ms", duration_ms)),
206        }
207    }
208
209    /// 是否成功
210    pub fn is_success(&self) -> bool {
211        self.status == WarmupStatus::Success
212    }
213}
214
215/// 预热报告(汇总所有预热器的执行结果)
216#[derive(Debug, Clone, Default)]
217pub struct WarmupReport {
218    /// 各预热器的执行结果
219    pub items: Vec<WarmupItem>,
220    /// 总耗时(毫秒)
221    pub total_duration_ms: u64,
222}
223
224impl WarmupReport {
225    /// 创建空报告
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    /// 添加一个执行结果
231    pub fn add_item(&mut self, item: WarmupItem) {
232        self.items.push(item);
233    }
234
235    /// 成功数量
236    pub fn success_count(&self) -> usize {
237        self.items.iter().filter(|i| i.is_success()).count()
238    }
239
240    /// 失败数量
241    pub fn failed_count(&self) -> usize {
242        self.items
243            .iter()
244            .filter(|i| i.status == WarmupStatus::Failed)
245            .count()
246    }
247
248    /// 超时数量
249    pub fn timeout_count(&self) -> usize {
250        self.items
251            .iter()
252            .filter(|i| i.status == WarmupStatus::Timeout)
253            .count()
254    }
255
256    /// 总数量
257    pub fn total_count(&self) -> usize {
258        self.items.len()
259    }
260
261    /// 是否全部成功
262    pub fn all_success(&self) -> bool {
263        !self.items.is_empty() && self.failed_count() == 0 && self.timeout_count() == 0
264    }
265
266    /// 生成可读的汇总报告
267    pub fn summary(&self) -> String {
268        let mut s = String::new();
269        s.push_str(&format!(
270            "Cache warmup: {}/{} succeeded, {} failed, {} timeout (total {}ms)\n",
271            self.success_count(),
272            self.total_count(),
273            self.failed_count(),
274            self.timeout_count(),
275            self.total_duration_ms
276        ));
277        for item in &self.items {
278            s.push_str(&format!(
279                "  - {:<20} {:<10} {}ms",
280                item.name, item.status, item.duration_ms
281            ));
282            if let Some(err) = &item.error {
283                s.push_str(&format!(" ({})", err));
284            }
285            s.push('\n');
286        }
287        s
288    }
289}
290
291// ============================================================================
292// 预热管道
293// ============================================================================
294
295/// 预热管道 — 管理并执行多个预热器
296///
297/// 默认串行执行(避免缓存并发写入冲突)。如需并行执行,
298/// 可使用 `warm_all_parallel`。
299pub struct WarmupPipeline {
300    warmers: Vec<Box<dyn Warmer>>,
301    /// 全局超时(默认 5 分钟,覆盖所有预热器的总耗时)
302    global_timeout: Duration,
303}
304
305impl WarmupPipeline {
306    /// 创建空的预热管道
307    pub fn new() -> Self {
308        Self {
309            warmers: Vec::new(),
310            global_timeout: Duration::from_secs(300),
311        }
312    }
313
314    /// 注册预热器
315    pub fn register(&mut self, warmer: Box<dyn Warmer>) -> &mut Self {
316        self.warmers.push(warmer);
317        self
318    }
319
320    /// 设置全局超时
321    pub fn with_global_timeout(mut self, timeout: Duration) -> Self {
322        self.global_timeout = timeout;
323        self
324    }
325
326    /// 获取已注册的预热器数量
327    pub fn count(&self) -> usize {
328        self.warmers.len()
329    }
330
331    /// 获取所有预热器名称
332    pub fn names(&self) -> Vec<&str> {
333        self.warmers.iter().map(|w| w.name()).collect()
334    }
335
336    /// 串行执行所有预热器
337    ///
338    /// 逐个执行预热器,每个预热器有自己的超时(`Warmer::timeout`)。
339    /// 单个失败不影响后续预热器。
340    pub async fn warm_all(&self) -> WarmupReport {
341        let mut report = WarmupReport::new();
342        let total_start = Instant::now();
343
344        for warmer in &self.warmers {
345            let item = self.warm_one(warmer.as_ref()).await;
346            report.add_item(item);
347        }
348
349        report.total_duration_ms = total_start.elapsed().as_millis() as u64;
350        report
351    }
352
353    /// 并行执行所有预热器
354    ///
355    /// 使用 `tokio::join_all` 并发执行,适合预热器之间无依赖的场景。
356    /// 注意:并行执行可能增加缓存写入冲突。
357    pub async fn warm_all_parallel(&self) -> WarmupReport {
358        let total_start = Instant::now();
359
360        let futures: Vec<_> = self
361            .warmers
362            .iter()
363            .map(|w| self.warm_one_async(w.as_ref()))
364            .collect();
365
366        let items = futures::future::join_all(futures).await;
367
368        let mut report = WarmupReport::new();
369        for item in items {
370            report.add_item(item);
371        }
372        report.total_duration_ms = total_start.elapsed().as_millis() as u64;
373        report
374    }
375
376    /// 执行单个预热器(带超时)
377    async fn warm_one(&self, warmer: &dyn Warmer) -> WarmupItem {
378        self.warm_one_async(warmer).await
379    }
380
381    /// 异步执行单个预热器(内部辅助方法,便于并行化)
382    async fn warm_one_async(&self, warmer: &dyn Warmer) -> WarmupItem {
383        let name = warmer.name().to_string();
384        let description = warmer.description().to_string();
385        let timeout = warmer.timeout();
386        let start = Instant::now();
387
388        let result = tokio::time::timeout(timeout, warmer.warm()).await;
389
390        let duration_ms = start.elapsed().as_millis() as u64;
391
392        match result {
393            Ok(Ok(())) => WarmupItem::success(name, description, duration_ms),
394            Ok(Err(e)) => WarmupItem::failed(name, description, duration_ms, e.to_string()),
395            Err(_) => WarmupItem::timeout(name, description, duration_ms),
396        }
397    }
398
399    /// 执行指定名称的预热器
400    ///
401    /// 找不到时返回 `WarmupReport` 仅包含一条 `Skipped` 记录。
402    pub async fn warm_one_by_name(&self, name: &str) -> WarmupReport {
403        let mut report = WarmupReport::new();
404        let total_start = Instant::now();
405
406        if let Some(warmer) = self.warmers.iter().find(|w| w.name() == name) {
407            let item = self.warm_one(warmer.as_ref()).await;
408            report.add_item(item);
409        } else {
410            report.add_item(WarmupItem {
411                name: name.to_string(),
412                description: "Not found".to_string(),
413                status: WarmupStatus::Skipped,
414                duration_ms: 0,
415                error: Some(format!("Warmer '{}' not registered", name)),
416            });
417        }
418
419        report.total_duration_ms = total_start.elapsed().as_millis() as u64;
420        report
421    }
422}
423
424impl Default for WarmupPipeline {
425    fn default() -> Self {
426        Self::new()
427    }
428}
429
430// ============================================================================
431// 部署钩子(Deployment Hook)
432// ============================================================================
433
434/// 部署钩子 — 在部署生命周期中触发预热
435///
436/// 对齐 PHP `think console` 的部署钩子:
437/// - `pre_warmup`:部署前预热(旧版本仍在服务)
438/// - `post_deploy`:部署后预热(新版本已切换)
439/// - `rollback`:回滚后清理预热缓存
440pub struct DeploymentHook {
441    pipeline: Arc<Mutex<WarmupPipeline>>,
442}
443
444impl DeploymentHook {
445    /// 创建部署钩子
446    pub fn new(pipeline: WarmupPipeline) -> Self {
447        Self {
448            pipeline: Arc::new(Mutex::new(pipeline)),
449        }
450    }
451
452    /// 部署前预热(在旧版本服务期间预热新版本缓存)
453    ///
454    /// 适用于蓝绿部署:在切换流量前先预热新版本的缓存。
455    pub async fn pre_warmup(&self) -> WarmupReport {
456        let pipeline = self.pipeline.lock().await;
457        pipeline.warm_all().await
458    }
459
460    /// 部署后预热(新版本已切换流量后立即预热)
461    ///
462    /// 适用于滚动更新:新实例启动后立即预热缓存。
463    pub async fn post_deploy(&self) -> WarmupReport {
464        let pipeline = self.pipeline.lock().await;
465        pipeline.warm_all().await
466    }
467
468    /// 回滚后清理(清空所有预热缓存)
469    ///
470    /// 注意:本方法只触发预热器,实际的缓存清理应由调用方在预热器实现中处理。
471    pub async fn rollback(&self) -> WarmupReport {
472        let pipeline = self.pipeline.lock().await;
473        pipeline.warm_all().await
474    }
475
476    /// 获取管道的共享句柄
477    pub fn pipeline(&self) -> Arc<Mutex<WarmupPipeline>> {
478        self.pipeline.clone()
479    }
480}
481
482// ============================================================================
483// 内置预热器
484// ============================================================================
485
486/// 无操作预热器(用于测试)
487pub struct NoopWarmer {
488    name: String,
489    delay_ms: u64,
490    should_fail: bool,
491}
492
493impl NoopWarmer {
494    /// 创建无操作预热器
495    ///
496    /// # 参数
497    ///
498    /// - `name`:预热器名称
499    /// - `delay_ms`:模拟耗时(毫秒)
500    /// - `should_fail`:是否模拟失败
501    pub fn new(name: impl Into<String>, delay_ms: u64, should_fail: bool) -> Self {
502        Self {
503            name: name.into(),
504            delay_ms,
505            should_fail,
506        }
507    }
508
509    /// 创建立即成功的预热器
510    pub fn success(name: impl Into<String>) -> Self {
511        Self::new(name, 0, false)
512    }
513
514    /// 创建立即失败的预热器
515    pub fn failing(name: impl Into<String>) -> Self {
516        Self::new(name, 0, true)
517    }
518
519    /// 创建耗时指定毫秒的预热器
520    pub fn delayed(name: impl Into<String>, delay_ms: u64) -> Self {
521        Self::new(name, delay_ms, false)
522    }
523}
524
525#[async_trait]
526impl Warmer for NoopWarmer {
527    fn name(&self) -> &str {
528        &self.name
529    }
530
531    fn description(&self) -> &str {
532        "Noop warmer for testing"
533    }
534
535    async fn warm(&self) -> Result<(), WarmupError> {
536        if self.delay_ms > 0 {
537            tokio::time::sleep(Duration::from_millis(self.delay_ms)).await;
538        }
539        if self.should_fail {
540            return Err(WarmupError::Custom("Simulated failure".to_string()));
541        }
542        Ok(())
543    }
544}
545
546// ============================================================================
547// 单元测试
548// ============================================================================
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553
554    // ========================================================================
555    // WarmupError
556    // ========================================================================
557
558    #[test]
559    fn test_warmup_error_display() {
560        assert_eq!(
561            WarmupError::Io("file not found".to_string()).to_string(),
562            "IO error: file not found"
563        );
564        assert_eq!(
565            WarmupError::Serialize("invalid json".to_string()).to_string(),
566            "Serialize error: invalid json"
567        );
568        assert_eq!(
569            WarmupError::Cache("write failed".to_string()).to_string(),
570            "Cache error: write failed"
571        );
572        assert_eq!(
573            WarmupError::Database("connection refused".to_string()).to_string(),
574            "Database error: connection refused"
575        );
576        assert_eq!(
577            WarmupError::Custom("custom".to_string()).to_string(),
578            "custom"
579        );
580    }
581
582    #[test]
583    fn test_warmup_error_from_io() {
584        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
585        let warmup_err: WarmupError = io_err.into();
586        assert!(matches!(warmup_err, WarmupError::Io(_)));
587    }
588
589    // ========================================================================
590    // WarmupStatus
591    // ========================================================================
592
593    #[test]
594    fn test_warmup_status_display() {
595        assert_eq!(WarmupStatus::Success.to_string(), "success");
596        assert_eq!(WarmupStatus::Failed.to_string(), "failed");
597        assert_eq!(WarmupStatus::Timeout.to_string(), "timeout");
598        assert_eq!(WarmupStatus::Skipped.to_string(), "skipped");
599    }
600
601    // ========================================================================
602    // WarmupItem
603    // ========================================================================
604
605    #[test]
606    fn test_warmup_item_success() {
607        let item = WarmupItem::success("config", "Config warmer", 100);
608        assert_eq!(item.name, "config");
609        assert_eq!(item.status, WarmupStatus::Success);
610        assert_eq!(item.duration_ms, 100);
611        assert!(item.error.is_none());
612        assert!(item.is_success());
613    }
614
615    #[test]
616    fn test_warmup_item_failed() {
617        let item = WarmupItem::failed("route", "Route warmer", 50, "missing file");
618        assert_eq!(item.status, WarmupStatus::Failed);
619        assert_eq!(item.error, Some("missing file".to_string()));
620        assert!(!item.is_success());
621    }
622
623    #[test]
624    fn test_warmup_item_timeout() {
625        let item = WarmupItem::timeout("db", "DB warmer", 30000);
626        assert_eq!(item.status, WarmupStatus::Timeout);
627        assert!(item.error.unwrap().contains("Timed out"));
628    }
629
630    // ========================================================================
631    // WarmupReport
632    // ========================================================================
633
634    #[test]
635    fn test_warmup_report_empty() {
636        let report = WarmupReport::new();
637        assert_eq!(report.total_count(), 0);
638        assert_eq!(report.success_count(), 0);
639        assert_eq!(report.failed_count(), 0);
640        assert_eq!(report.timeout_count(), 0);
641        assert!(!report.all_success());
642    }
643
644    #[test]
645    fn test_warmup_report_all_success() {
646        let mut report = WarmupReport::new();
647        report.add_item(WarmupItem::success("a", "A", 10));
648        report.add_item(WarmupItem::success("b", "B", 20));
649        report.total_duration_ms = 30;
650
651        assert_eq!(report.total_count(), 2);
652        assert_eq!(report.success_count(), 2);
653        assert_eq!(report.failed_count(), 0);
654        assert!(report.all_success());
655    }
656
657    #[test]
658    fn test_warmup_report_mixed() {
659        let mut report = WarmupReport::new();
660        report.add_item(WarmupItem::success("a", "A", 10));
661        report.add_item(WarmupItem::failed("b", "B", 20, "err"));
662        report.add_item(WarmupItem::timeout("c", "C", 30000));
663
664        assert_eq!(report.total_count(), 3);
665        assert_eq!(report.success_count(), 1);
666        assert_eq!(report.failed_count(), 1);
667        assert_eq!(report.timeout_count(), 1);
668        assert!(!report.all_success());
669    }
670
671    #[test]
672    fn test_warmup_report_summary_contains_status() {
673        let mut report = WarmupReport::new();
674        report.add_item(WarmupItem::success("config", "Config", 100));
675        report.add_item(WarmupItem::failed("route", "Route", 50, "missing"));
676        report.total_duration_ms = 150;
677
678        let summary = report.summary();
679        assert!(summary.contains("1/2 succeeded"));
680        assert!(summary.contains("1 failed"));
681        assert!(summary.contains("config"));
682        assert!(summary.contains("success"));
683        assert!(summary.contains("route"));
684        assert!(summary.contains("failed"));
685        assert!(summary.contains("missing"));
686    }
687
688    // ========================================================================
689    // WarmupPipeline
690    // ========================================================================
691
692    #[tokio::test]
693    async fn test_pipeline_empty() {
694        let pipeline = WarmupPipeline::new();
695        assert_eq!(pipeline.count(), 0);
696
697        let report = pipeline.warm_all().await;
698        assert_eq!(report.total_count(), 0);
699        assert!(report.items.is_empty());
700    }
701
702    #[tokio::test]
703    async fn test_pipeline_single_success() {
704        let mut pipeline = WarmupPipeline::new();
705        pipeline.register(Box::new(NoopWarmer::success("config")));
706
707        let report = pipeline.warm_all().await;
708        assert_eq!(report.total_count(), 1);
709        assert_eq!(report.success_count(), 1);
710        assert!(report.all_success());
711        assert_eq!(report.items[0].name, "config");
712    }
713
714    #[tokio::test]
715    async fn test_pipeline_mixed_results() {
716        let mut pipeline = WarmupPipeline::new();
717        pipeline.register(Box::new(NoopWarmer::success("success_warmer")));
718        pipeline.register(Box::new(NoopWarmer::failing("failing_warmer")));
719
720        let report = pipeline.warm_all().await;
721        assert_eq!(report.total_count(), 2);
722        assert_eq!(report.success_count(), 1);
723        assert_eq!(report.failed_count(), 1);
724        assert!(!report.all_success());
725
726        assert_eq!(report.items[0].name, "success_warmer");
727        assert_eq!(report.items[0].status, WarmupStatus::Success);
728        assert_eq!(report.items[1].name, "failing_warmer");
729        assert_eq!(report.items[1].status, WarmupStatus::Failed);
730        assert!(report.items[1]
731            .error
732            .as_ref()
733            .unwrap()
734            .contains("Simulated failure"));
735    }
736
737    #[tokio::test]
738    async fn test_pipeline_failure_does_not_stop_others() {
739        let mut pipeline = WarmupPipeline::new();
740        pipeline.register(Box::new(NoopWarmer::failing("first_fails")));
741        pipeline.register(Box::new(NoopWarmer::success("second_success")));
742
743        let report = pipeline.warm_all().await;
744        assert_eq!(report.total_count(), 2);
745        assert_eq!(report.success_count(), 1);
746        assert_eq!(report.failed_count(), 1);
747    }
748
749    #[tokio::test]
750    async fn test_pipeline_timeout() {
751        let mut pipeline = WarmupPipeline::new();
752
753        // 自定义超时 100ms 的预热器,但实际耗时 500ms
754        struct SlowWarmer;
755        #[async_trait]
756        impl Warmer for SlowWarmer {
757            fn name(&self) -> &str {
758                "slow"
759            }
760            fn timeout(&self) -> Duration {
761                Duration::from_millis(100)
762            }
763            async fn warm(&self) -> Result<(), WarmupError> {
764                tokio::time::sleep(Duration::from_millis(500)).await;
765                Ok(())
766            }
767        }
768
769        pipeline.register(Box::new(SlowWarmer));
770
771        let report = pipeline.warm_all().await;
772        assert_eq!(report.total_count(), 1);
773        assert_eq!(report.timeout_count(), 1);
774        assert_eq!(report.items[0].status, WarmupStatus::Timeout);
775    }
776
777    #[tokio::test]
778    async fn test_pipeline_parallel() {
779        let mut pipeline = WarmupPipeline::new();
780        pipeline.register(Box::new(NoopWarmer::delayed("a", 100)));
781        pipeline.register(Box::new(NoopWarmer::delayed("b", 100)));
782        pipeline.register(Box::new(NoopWarmer::delayed("c", 100)));
783
784        // 串行:约 300ms
785        let serial_report = pipeline.warm_all().await;
786        assert!(serial_report.total_duration_ms >= 250);
787
788        // 并行:约 100ms
789        let parallel_report = pipeline.warm_all_parallel().await;
790        assert!(parallel_report.total_duration_ms < 200);
791        assert_eq!(parallel_report.success_count(), 3);
792    }
793
794    #[tokio::test]
795    async fn test_pipeline_warm_one_by_name_found() {
796        let mut pipeline = WarmupPipeline::new();
797        pipeline.register(Box::new(NoopWarmer::success("config")));
798        pipeline.register(Box::new(NoopWarmer::success("route")));
799
800        let report = pipeline.warm_one_by_name("config").await;
801        assert_eq!(report.total_count(), 1);
802        assert_eq!(report.items[0].name, "config");
803        assert_eq!(report.items[0].status, WarmupStatus::Success);
804    }
805
806    #[tokio::test]
807    async fn test_pipeline_warm_one_by_name_not_found() {
808        let pipeline = WarmupPipeline::new();
809
810        let report = pipeline.warm_one_by_name("nonexistent").await;
811        assert_eq!(report.total_count(), 1);
812        assert_eq!(report.items[0].status, WarmupStatus::Skipped);
813        assert!(report.items[0]
814            .error
815            .as_ref()
816            .unwrap()
817            .contains("not registered"));
818    }
819
820    #[tokio::test]
821    async fn test_pipeline_names() {
822        let mut pipeline = WarmupPipeline::new();
823        pipeline.register(Box::new(NoopWarmer::success("a")));
824        pipeline.register(Box::new(NoopWarmer::success("b")));
825
826        let names = pipeline.names();
827        assert_eq!(names, vec!["a", "b"]);
828    }
829
830    #[tokio::test]
831    async fn test_pipeline_with_global_timeout() {
832        let pipeline = WarmupPipeline::new().with_global_timeout(Duration::from_secs(60));
833        assert_eq!(pipeline.count(), 0);
834    }
835
836    // ========================================================================
837    // DeploymentHook
838    // ========================================================================
839
840    #[tokio::test]
841    async fn test_deployment_hook_pre_warmup() {
842        let mut pipeline = WarmupPipeline::new();
843        pipeline.register(Box::new(NoopWarmer::success("config")));
844
845        let hook = DeploymentHook::new(pipeline);
846        let report = hook.pre_warmup().await;
847
848        assert_eq!(report.success_count(), 1);
849    }
850
851    #[tokio::test]
852    async fn test_deployment_hook_post_deploy() {
853        let mut pipeline = WarmupPipeline::new();
854        pipeline.register(Box::new(NoopWarmer::success("config")));
855        pipeline.register(Box::new(NoopWarmer::success("route")));
856
857        let hook = DeploymentHook::new(pipeline);
858        let report = hook.post_deploy().await;
859
860        assert_eq!(report.success_count(), 2);
861    }
862
863    #[tokio::test]
864    async fn test_deployment_hook_rollback() {
865        let mut pipeline = WarmupPipeline::new();
866        pipeline.register(Box::new(NoopWarmer::success("cleanup")));
867
868        let hook = DeploymentHook::new(pipeline);
869        let report = hook.rollback().await;
870
871        assert_eq!(report.success_count(), 1);
872    }
873
874    #[tokio::test]
875    async fn test_deployment_hook_pipeline_handle() {
876        let pipeline = WarmupPipeline::new();
877        let hook = DeploymentHook::new(pipeline);
878
879        let handle = hook.pipeline();
880        let p = handle.lock().await;
881        assert_eq!(p.count(), 0);
882    }
883
884    // ========================================================================
885    // NoopWarmer
886    // ========================================================================
887
888    #[tokio::test]
889    async fn test_noop_warmer_success() {
890        let warmer = NoopWarmer::success("test");
891        assert_eq!(warmer.name(), "test");
892        assert_eq!(warmer.description(), "Noop warmer for testing");
893        let result = warmer.warm().await;
894        assert!(result.is_ok());
895    }
896
897    #[tokio::test]
898    async fn test_noop_warmer_failing() {
899        let warmer = NoopWarmer::failing("test");
900        let result = warmer.warm().await;
901        assert!(result.is_err());
902        assert!(result
903            .unwrap_err()
904            .to_string()
905            .contains("Simulated failure"));
906    }
907
908    #[tokio::test]
909    async fn test_noop_warmer_delayed() {
910        let warmer = NoopWarmer::delayed("test", 50);
911        let start = Instant::now();
912        warmer.warm().await.unwrap();
913        assert!(start.elapsed().as_millis() >= 40);
914    }
915
916    #[test]
917    fn test_noop_warmer_default_timeout() {
918        let warmer = NoopWarmer::success("test");
919        assert_eq!(warmer.timeout(), Duration::from_secs(30));
920    }
921
922    // ========================================================================
923    // 端到端
924    // ========================================================================
925
926    #[tokio::test]
927    async fn test_end_to_end_deployment_scenario() {
928        // 模拟完整部署流程:3 个预热器,其中 1 个失败
929        let mut pipeline = WarmupPipeline::new();
930        pipeline.register(Box::new(NoopWarmer::success("config")));
931        pipeline.register(Box::new(NoopWarmer::success("route")));
932        pipeline.register(Box::new(NoopWarmer::failing("dict")));
933
934        let hook = DeploymentHook::new(pipeline);
935
936        // 执行预热
937        let report = hook.pre_warmup().await;
938
939        // 验证结果
940        assert_eq!(report.total_count(), 3);
941        assert_eq!(report.success_count(), 2);
942        assert_eq!(report.failed_count(), 1);
943
944        // 验证 summary
945        let summary = report.summary();
946        assert!(summary.contains("2/3 succeeded"));
947        assert!(summary.contains("1 failed"));
948
949        // 验证失败的预热器被正确记录
950        let failed_item = report
951            .items
952            .iter()
953            .find(|i| i.status == WarmupStatus::Failed)
954            .unwrap();
955        assert_eq!(failed_item.name, "dict");
956    }
957}