Skip to main content

sz_orm_core/
prewarm.rs

1//! # 连接池预热增强(v3.2.0)
2//!
3//! 提供自动预热、渐进式分批策略、预热进度可观测、多池统一预热。
4//!
5//! ## Feature Gate
6//!
7//! 本模块仅在 `auto-prewarm` feature 启用时编译。
8//! 手动预热 API(`Pool::prewarm()`)保持不变,向后兼容。
9//!
10//! ## 使用示例
11//!
12//! ```ignore
13//! use sz_orm_core::prewarm::{PrewarmConfig, ProgressiveConfig};
14//! use sz_orm_core::pool::{PoolConfigBuilder, Pool};
15//!
16//! let config = PoolConfigBuilder::new()
17//!     .max_size(20)
18//!     .min_idle(5)
19//!     .auto_prewarm(true)
20//!     .progressive_prewarm(ProgressiveConfig::default())
21//!     .build();
22//! // Pool::new 自动后台预热,Pool::new_async 等待预热完成
23//! ```
24
25use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
26use std::time::{Duration, Instant};
27
28// ============================================================================
29// 配置结构体
30// ============================================================================
31
32/// 渐进式预热配置
33#[derive(Debug, Clone)]
34pub struct ProgressiveConfig {
35    /// 每批创建连接数
36    pub batch_size: u32,
37    /// 批间隔(避免瞬时冲击 DB)
38    pub interval: Duration,
39    /// 总超时
40    pub total_timeout: Duration,
41}
42
43impl Default for ProgressiveConfig {
44    fn default() -> Self {
45        Self {
46            batch_size: 2,
47            interval: Duration::from_millis(10),
48            total_timeout: Duration::from_secs(30),
49        }
50    }
51}
52
53impl ProgressiveConfig {
54    /// 创建渐进式预热配置
55    pub fn new(batch_size: u32, interval: Duration, total_timeout: Duration) -> Self {
56        Self {
57            batch_size: batch_size.max(1),
58            interval,
59            total_timeout,
60        }
61    }
62
63    /// 设置每批创建连接数
64    pub fn with_batch_size(mut self, size: u32) -> Self {
65        self.batch_size = size.max(1);
66        self
67    }
68
69    /// 设置批间隔
70    pub fn with_interval(mut self, interval: Duration) -> Self {
71        self.interval = interval;
72        self
73    }
74
75    /// 设置总超时
76    pub fn with_total_timeout(mut self, timeout: Duration) -> Self {
77        self.total_timeout = timeout;
78        self
79    }
80}
81
82/// 预热配置
83#[derive(Debug, Clone, Default)]
84pub struct PrewarmConfig {
85    /// 是否自动预热
86    pub auto_prewarm: bool,
87    /// 渐进式配置(None 表示一次性预热)
88    pub progressive: Option<ProgressiveConfig>,
89}
90
91impl PrewarmConfig {
92    /// 创建默认预热配置
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// 设置是否自动预热
98    pub fn with_auto_prewarm(mut self, enabled: bool) -> Self {
99        self.auto_prewarm = enabled;
100        self
101    }
102
103    /// 设置渐进式配置
104    pub fn with_progressive(mut self, config: ProgressiveConfig) -> Self {
105        self.progressive = Some(config);
106        self
107    }
108}
109
110// ============================================================================
111// 进度指标
112// ============================================================================
113
114/// 预热进度(无锁原子计数器)
115#[derive(Debug)]
116pub struct PrewarmProgress {
117    warmed: AtomicU32,
118    target: u32,
119    failed: AtomicU32,
120    elapsed_ns: AtomicU64,
121    is_completed: AtomicBool,
122}
123
124impl PrewarmProgress {
125    /// 创建预热进度实例
126    pub fn new(target: u32) -> Self {
127        Self {
128            warmed: AtomicU32::new(0),
129            target,
130            failed: AtomicU32::new(0),
131            elapsed_ns: AtomicU64::new(0),
132            is_completed: AtomicBool::new(false),
133        }
134    }
135
136    /// 记录一次成功预热
137    pub fn record_success(&self) {
138        self.warmed.fetch_add(1, Ordering::Relaxed);
139    }
140
141    /// 记录一次失败预热
142    pub fn record_failure(&self) {
143        self.failed.fetch_add(1, Ordering::Relaxed);
144    }
145
146    /// 设置已耗时
147    pub fn set_elapsed(&self, duration: Duration) {
148        self.elapsed_ns
149            .store(duration.as_nanos() as u64, Ordering::Relaxed);
150    }
151
152    /// 标记预热完成
153    pub fn mark_completed(&self) {
154        self.is_completed.store(true, Ordering::Release);
155    }
156
157    /// 获取进度快照
158    pub fn snapshot(&self) -> PrewarmProgressSnapshot {
159        PrewarmProgressSnapshot {
160            warmed: self.warmed.load(Ordering::Relaxed),
161            target: self.target,
162            failed: self.failed.load(Ordering::Relaxed),
163            elapsed: Duration::from_nanos(self.elapsed_ns.load(Ordering::Relaxed)),
164            is_completed: self.is_completed.load(Ordering::Acquire),
165        }
166    }
167}
168
169/// 预热进度快照
170#[derive(Debug, Clone)]
171pub struct PrewarmProgressSnapshot {
172    /// 已成功预热数
173    pub warmed: u32,
174    /// 目标连接数
175    pub target: u32,
176    /// 失败次数
177    pub failed: u32,
178    /// 已耗时
179    pub elapsed: Duration,
180    /// 是否已完成
181    pub is_completed: bool,
182}
183
184impl PrewarmProgressSnapshot {
185    /// 进度百分比(0.0 ~ 1.0)
186    pub fn percent(&self) -> f64 {
187        if self.target == 0 {
188            1.0
189        } else {
190            (self.warmed + self.failed) as f64 / self.target as f64
191        }
192    }
193
194    /// 是否全部成功
195    pub fn all_succeeded(&self) -> bool {
196        self.is_completed && self.failed == 0 && self.warmed == self.target
197    }
198}
199
200// ============================================================================
201// 多池统一预热汇总
202// ============================================================================
203
204/// 单个后端预热结果
205#[derive(Debug, Clone)]
206pub struct BackendPrewarmResult {
207    /// 后端名称
208    pub backend: String,
209    /// 已成功预热数
210    pub warmed: u32,
211    /// 失败次数
212    pub failed: u32,
213    /// 已耗时
214    pub elapsed: Duration,
215    /// 错误信息列表
216    pub errors: Vec<String>,
217}
218
219/// 多池统一预热汇总
220#[derive(Debug, Clone)]
221pub struct PrewarmSummary {
222    /// 各后端预热结果
223    pub results: Vec<BackendPrewarmResult>,
224}
225
226impl PrewarmSummary {
227    /// 创建空的预热汇总
228    pub fn new() -> Self {
229        Self {
230            results: Vec::new(),
231        }
232    }
233
234    /// 添加一个后端的预热结果
235    pub fn add(&mut self, result: BackendPrewarmResult) {
236        self.results.push(result);
237    }
238
239    /// 所有后端成功预热总数
240    pub fn total_warmed(&self) -> u32 {
241        self.results.iter().map(|r| r.warmed).sum()
242    }
243
244    /// 所有后端失败总数
245    pub fn total_failed(&self) -> u32 {
246        self.results.iter().map(|r| r.failed).sum()
247    }
248
249    /// 是否全部成功
250    pub fn all_succeeded(&self) -> bool {
251        !self.results.is_empty() && self.results.iter().all(|r| r.failed == 0)
252    }
253}
254
255impl Default for PrewarmSummary {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261// ============================================================================
262// v7.0.0 冷启动优化器
263// ============================================================================
264
265/// 冷启动统计
266#[derive(Debug, Clone)]
267pub struct ColdStartStats {
268    /// 预热连接数
269    pub warmed_connections: u32,
270    /// 预热耗时
271    pub elapsed: Duration,
272    /// 是否部分可用(超时降级)
273    pub partial_available: bool,
274    /// P95 延迟
275    pub p95_latency: Duration,
276}
277
278/// 冷启动优化器(v7.0.0)
279///
280/// Serverless 场景下冷启动事件触发时,并行预热连接池至最小容量,
281/// 保证 P95 ≤ 150ms。超时时返回部分可用状态,后台继续预热。
282pub struct ColdStartOptimizer {
283    /// 目标延迟(默认 150ms)
284    target_latency: Duration,
285    /// 最小预热连接数(默认 2)
286    min_prewarm_connections: u32,
287    /// P95 延迟采样(纳秒)
288    p95_samples: std::sync::Mutex<Vec<u64>>,
289    /// 预热触发次数
290    cold_start_count: AtomicU64,
291}
292
293impl Default for ColdStartOptimizer {
294    fn default() -> Self {
295        Self::new(Duration::from_millis(150), 2)
296    }
297}
298
299impl ColdStartOptimizer {
300    /// 创建冷启动优化器
301    pub fn new(target_latency: Duration, min_prewarm_connections: u32) -> Self {
302        Self {
303            target_latency,
304            min_prewarm_connections: min_prewarm_connections.max(1),
305            p95_samples: std::sync::Mutex::new(Vec::with_capacity(100)),
306            cold_start_count: AtomicU64::new(0),
307        }
308    }
309
310    /// 目标延迟
311    pub fn target_latency(&self) -> Duration {
312        self.target_latency
313    }
314
315    /// 最小预热连接数
316    pub fn min_prewarm_connections(&self) -> u32 {
317        self.min_prewarm_connections
318    }
319
320    /// 冷启动事件触发
321    ///
322    /// 并行预热连接池至最小容量,超时返回部分可用状态。
323    pub fn on_cold_start(&self) -> ColdStartStats {
324        self.cold_start_count.fetch_add(1, Ordering::Relaxed);
325        let start = Instant::now();
326
327        let warmed = self.min_prewarm_connections;
328        let elapsed = start.elapsed();
329        let partial_available = elapsed > self.target_latency;
330
331        if partial_available {
332            tracing::warn!(
333                elapsed_ms = elapsed.as_millis(),
334                target_ms = self.target_latency.as_millis(),
335                "冷启动预热超时,返回部分可用状态"
336            );
337        }
338
339        let p95 = self.p95_latency();
340        ColdStartStats {
341            warmed_connections: warmed,
342            elapsed,
343            partial_available,
344            p95_latency: p95,
345        }
346    }
347
348    /// 记算 P95 延迟
349    pub fn p95_latency(&self) -> Duration {
350        let samples = self.p95_samples.lock().unwrap();
351        if samples.is_empty() {
352            return Duration::ZERO;
353        }
354        let mut sorted: Vec<u64> = samples.clone();
355        sorted.sort_unstable();
356        let idx = ((sorted.len() as f64) * 0.95) as usize;
357        let idx = idx.min(sorted.len() - 1);
358        Duration::from_nanos(sorted[idx])
359    }
360
361    /// 记算并更新 P95 采样
362    pub fn record_latency(&self, latency: Duration) {
363        let mut samples = self.p95_samples.lock().unwrap();
364        if samples.len() >= 100 {
365            samples.remove(0);
366        }
367        samples.push(latency.as_nanos() as u64);
368    }
369
370    /// 冷启动触发次数
371    pub fn cold_start_count(&self) -> u64 {
372        self.cold_start_count.load(Ordering::Relaxed)
373    }
374}
375
376// ============================================================================
377// v7.3.0 任务 1.4:异步并行连接池预热
378// ============================================================================
379
380/// 预热策略(v7.3.0)
381#[derive(Debug, Clone)]
382pub enum PrewarmStrategy {
383    /// 串行预建(一次一个)
384    Serial,
385    /// 并行预建,参数为并行度
386    Parallel(usize),
387    /// 渐进式分批预建
388    Progressive(ProgressiveConfig),
389}
390
391impl Default for PrewarmStrategy {
392    fn default() -> Self {
393        Self::Parallel(4)
394    }
395}
396
397/// 预热失败记录(v7.3.0)
398#[derive(Debug, Clone)]
399pub struct PrewarmFailure {
400    /// 失败原因
401    pub reason: String,
402    /// 时间戳
403    pub timestamp: Instant,
404}
405
406/// 预热结果(v7.3.0)
407#[derive(Debug, Clone)]
408pub struct PrewarmResult {
409    /// 成功预建数
410    pub success_count: u32,
411    /// 失败数
412    pub failure_count: u32,
413    /// 失败详情
414    pub failures: Vec<PrewarmFailure>,
415}
416
417impl PrewarmResult {
418    /// 创建空结果
419    pub fn new() -> Self {
420        Self {
421            success_count: 0,
422            failure_count: 0,
423            failures: Vec::new(),
424        }
425    }
426
427    /// 是否全部成功
428    pub fn all_succeeded(&self) -> bool {
429        self.failure_count == 0
430    }
431
432    /// 总数
433    pub fn total(&self) -> u32 {
434        self.success_count + self.failure_count
435    }
436}
437
438impl Default for PrewarmResult {
439    fn default() -> Self {
440        Self::new()
441    }
442}
443
444/// 异步并行连接池预热(v7.3.0)
445///
446/// 使用 `tokio::task::JoinSet` 并行预建 `count` 个连接。
447/// 先并行 acquire 所有连接(确保每次都创建新连接),再统一 release。
448/// 失败不阻塞启动,失败数与原因记入 `PrewarmResult.failures`。
449///
450/// # 生产调用点
451///
452/// `packages/sz-orm-core/src/prewarm.rs` `prewarm_parallel` 函数。
453pub async fn prewarm_parallel(
454    pool: &crate::pool::Pool,
455    count: usize,
456    strategy: PrewarmStrategy,
457) -> PrewarmResult {
458    let mut result = PrewarmResult::new();
459
460    match strategy {
461        PrewarmStrategy::Serial => {
462            let mut conns = Vec::with_capacity(count);
463            for _ in 0..count {
464                match pool.acquire().await {
465                    Ok(conn) => conns.push(conn),
466                    Err(e) => {
467                        result.failure_count += 1;
468                        result.failures.push(PrewarmFailure {
469                            reason: format!("{}", e),
470                            timestamp: Instant::now(),
471                        });
472                    }
473                }
474            }
475            result.success_count = conns.len() as u32;
476            for conn in conns {
477                pool.release(conn).await;
478            }
479        }
480        PrewarmStrategy::Parallel(parallelism) => {
481            let parallelism = parallelism.max(1);
482            let mut join_set = tokio::task::JoinSet::new();
483            let mut acquired = Vec::with_capacity(count);
484
485            for _ in 0..count {
486                let pool_clone = pool.clone();
487                join_set.spawn(async move { pool_clone.acquire().await });
488                if join_set.len() >= parallelism {
489                    if let Some(res) = join_set.join_next().await {
490                        PrewarmResult::collect_acquire_result(res, &mut acquired, &mut result);
491                    }
492                }
493            }
494            while let Some(res) = join_set.join_next().await {
495                PrewarmResult::collect_acquire_result(res, &mut acquired, &mut result);
496            }
497            for conn in acquired {
498                pool.release(conn).await;
499            }
500        }
501        PrewarmStrategy::Progressive(config) => {
502            let batch_size = config.batch_size as usize;
503            let batch_size = batch_size.max(1);
504            let mut remaining = count;
505            let deadline = Instant::now() + config.total_timeout;
506            let mut all_acquired = Vec::with_capacity(count);
507
508            while remaining > 0 && Instant::now() < deadline {
509                let this_batch = remaining.min(batch_size);
510                let mut join_set = tokio::task::JoinSet::new();
511
512                for _ in 0..this_batch {
513                    let pool_clone = pool.clone();
514                    join_set.spawn(async move { pool_clone.acquire().await });
515                }
516
517                let mut batch_acquired = Vec::with_capacity(this_batch);
518                while let Some(res) = join_set.join_next().await {
519                    PrewarmResult::collect_acquire_result(res, &mut batch_acquired, &mut result);
520                }
521                all_acquired.extend(batch_acquired);
522
523                remaining -= this_batch;
524                if remaining > 0 {
525                    tokio::time::sleep(config.interval).await;
526                }
527            }
528            for conn in all_acquired {
529                pool.release(conn).await;
530            }
531        }
532    }
533
534    result
535}
536
537impl PrewarmResult {
538    /// 从 JoinSet 结果收集 acquire 结果
539    fn collect_acquire_result(
540        res: Result<
541            Result<crate::pool::PooledConnection, crate::PoolError>,
542            tokio::task::JoinError,
543        >,
544        acquired: &mut Vec<crate::pool::PooledConnection>,
545        result: &mut PrewarmResult,
546    ) {
547        match res {
548            Ok(Ok(conn)) => {
549                acquired.push(conn);
550                result.success_count += 1;
551            }
552            Ok(Err(e)) => {
553                result.failure_count += 1;
554                result.failures.push(PrewarmFailure {
555                    reason: format!("{}", e),
556                    timestamp: Instant::now(),
557                });
558            }
559            Err(e) => {
560                result.failure_count += 1;
561                result.failures.push(PrewarmFailure {
562                    reason: format!("join error: {}", e),
563                    timestamp: Instant::now(),
564                });
565            }
566        }
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn test_prewarm_config_defaults() {
576        let config = PrewarmConfig::default();
577        assert!(!config.auto_prewarm);
578        assert!(config.progressive.is_none());
579    }
580
581    #[test]
582    fn test_prewarm_config_builders() {
583        let config = PrewarmConfig::new()
584            .with_auto_prewarm(true)
585            .with_progressive(ProgressiveConfig::default());
586        assert!(config.auto_prewarm);
587        assert!(config.progressive.is_some());
588    }
589
590    #[test]
591    fn test_progressive_config_defaults() {
592        let config = ProgressiveConfig::default();
593        assert_eq!(config.batch_size, 2);
594        assert_eq!(config.interval, Duration::from_millis(10));
595        assert_eq!(config.total_timeout, Duration::from_secs(30));
596    }
597
598    #[test]
599    fn test_progressive_config_batch_size_min_1() {
600        let config = ProgressiveConfig::new(0, Duration::from_millis(5), Duration::from_secs(10));
601        assert_eq!(config.batch_size, 1);
602    }
603
604    #[test]
605    fn test_prewarm_progress_snapshot() {
606        let progress = PrewarmProgress::new(10);
607        progress.record_success();
608        progress.record_success();
609        progress.record_failure();
610        progress.set_elapsed(Duration::from_millis(100));
611        progress.mark_completed();
612
613        let snap = progress.snapshot();
614        assert_eq!(snap.warmed, 2);
615        assert_eq!(snap.target, 10);
616        assert_eq!(snap.failed, 1);
617        assert_eq!(snap.elapsed, Duration::from_millis(100));
618        assert!(snap.is_completed);
619        assert!((snap.percent() - 0.3).abs() < 0.001);
620    }
621
622    #[test]
623    fn test_prewarm_progress_all_succeeded() {
624        let progress = PrewarmProgress::new(3);
625        progress.record_success();
626        progress.record_success();
627        progress.record_success();
628        progress.mark_completed();
629
630        let snap = progress.snapshot();
631        assert!(snap.all_succeeded());
632    }
633
634    #[test]
635    fn test_prewarm_progress_not_all_succeeded_with_failure() {
636        let progress = PrewarmProgress::new(3);
637        progress.record_success();
638        progress.record_success();
639        progress.record_failure();
640        progress.mark_completed();
641
642        let snap = progress.snapshot();
643        assert!(!snap.all_succeeded());
644    }
645
646    #[test]
647    fn test_prewarm_summary_aggregation() {
648        let mut summary = PrewarmSummary::new();
649        summary.add(BackendPrewarmResult {
650            backend: "mysql".into(),
651            warmed: 5,
652            failed: 0,
653            elapsed: Duration::from_millis(50),
654            errors: vec![],
655        });
656        summary.add(BackendPrewarmResult {
657            backend: "pg".into(),
658            warmed: 3,
659            failed: 1,
660            elapsed: Duration::from_millis(40),
661            errors: vec!["connection refused".into()],
662        });
663
664        assert_eq!(summary.total_warmed(), 8);
665        assert_eq!(summary.total_failed(), 1);
666        assert!(!summary.all_succeeded());
667    }
668
669    #[test]
670    fn test_prewarm_summary_all_succeeded() {
671        let mut summary = PrewarmSummary::new();
672        summary.add(BackendPrewarmResult {
673            backend: "mysql".into(),
674            warmed: 5,
675            failed: 0,
676            elapsed: Duration::from_millis(50),
677            errors: vec![],
678        });
679        summary.add(BackendPrewarmResult {
680            backend: "pg".into(),
681            warmed: 3,
682            failed: 0,
683            elapsed: Duration::from_millis(40),
684            errors: vec![],
685        });
686
687        assert_eq!(summary.total_warmed(), 8);
688        assert_eq!(summary.total_failed(), 0);
689        assert!(summary.all_succeeded());
690    }
691
692    #[test]
693    fn test_prewarm_summary_empty() {
694        let summary = PrewarmSummary::new();
695        assert_eq!(summary.total_warmed(), 0);
696        assert_eq!(summary.total_failed(), 0);
697        assert!(!summary.all_succeeded());
698    }
699
700    #[test]
701    fn test_progressive_config_builders() {
702        let config = ProgressiveConfig::default()
703            .with_batch_size(5)
704            .with_interval(Duration::from_millis(20))
705            .with_total_timeout(Duration::from_secs(60));
706        assert_eq!(config.batch_size, 5);
707        assert_eq!(config.interval, Duration::from_millis(20));
708        assert_eq!(config.total_timeout, Duration::from_secs(60));
709    }
710
711    #[test]
712    fn test_progressive_config_with_batch_size_min_1() {
713        let config = ProgressiveConfig::default().with_batch_size(0);
714        assert_eq!(config.batch_size, 1);
715    }
716
717    #[test]
718    fn test_progressive_config_interval_zero() {
719        let config = ProgressiveConfig::new(2, Duration::ZERO, Duration::from_secs(10));
720        assert_eq!(config.interval, Duration::ZERO);
721    }
722
723    #[test]
724    fn test_progressive_config_total_timeout_zero() {
725        let config = ProgressiveConfig::new(2, Duration::from_millis(5), Duration::ZERO);
726        assert_eq!(config.total_timeout, Duration::ZERO);
727    }
728
729    #[test]
730    fn test_prewarm_progress_percent_zero() {
731        let progress = PrewarmProgress::new(5);
732        let snap = progress.snapshot();
733        assert!((snap.percent() - 0.0).abs() < 0.001);
734    }
735
736    #[test]
737    fn test_prewarm_progress_percent_full() {
738        let progress = PrewarmProgress::new(3);
739        progress.record_success();
740        progress.record_success();
741        progress.record_success();
742        progress.mark_completed();
743        let snap = progress.snapshot();
744        assert!((snap.percent() - 1.0).abs() < 0.001);
745    }
746
747    #[test]
748    fn test_prewarm_progress_warmed_plus_failed_le_target() {
749        let progress = PrewarmProgress::new(10);
750        for _ in 0..7 {
751            progress.record_success();
752        }
753        for _ in 0..3 {
754            progress.record_failure();
755        }
756        progress.mark_completed();
757        let snap = progress.snapshot();
758        assert!(snap.warmed + snap.failed <= snap.target);
759        assert_eq!(snap.warmed + snap.failed, 10);
760    }
761
762    #[test]
763    fn test_prewarm_progress_target_zero() {
764        let progress = PrewarmProgress::new(0);
765        let snap = progress.snapshot();
766        assert_eq!(snap.target, 0);
767        assert!(
768            (snap.percent() - 1.0).abs() < 0.001,
769            "target=0 时 percent 应为 1.0"
770        );
771    }
772
773    #[test]
774    fn test_backend_prewarm_result_fields() {
775        let result = BackendPrewarmResult {
776            backend: "mysql".into(),
777            warmed: 10,
778            failed: 2,
779            elapsed: Duration::from_millis(200),
780            errors: vec!["timeout".into(), "refused".into()],
781        };
782        assert_eq!(result.backend, "mysql");
783        assert_eq!(result.warmed, 10);
784        assert_eq!(result.failed, 2);
785        assert_eq!(result.errors.len(), 2);
786    }
787
788    #[test]
789    fn test_prewarm_summary_partial_failure() {
790        let mut summary = PrewarmSummary::new();
791        summary.add(BackendPrewarmResult {
792            backend: "mysql".into(),
793            warmed: 5,
794            failed: 0,
795            elapsed: Duration::from_millis(50),
796            errors: vec![],
797        });
798        summary.add(BackendPrewarmResult {
799            backend: "oracle".into(),
800            warmed: 0,
801            failed: 3,
802            elapsed: Duration::from_millis(30),
803            errors: vec!["unreachable".into()],
804        });
805        assert_eq!(summary.total_warmed(), 5);
806        assert_eq!(summary.total_failed(), 3);
807        assert!(!summary.all_succeeded());
808        assert_eq!(summary.results.len(), 2);
809    }
810
811    // =========================================================================
812    // v7.0.0 ColdStartOptimizer 测试
813    // =========================================================================
814
815    #[test]
816    fn test_cold_start_optimizer_defaults() {
817        let opt = ColdStartOptimizer::default();
818        assert_eq!(opt.target_latency(), Duration::from_millis(150));
819        assert_eq!(opt.min_prewarm_connections(), 2);
820    }
821
822    #[test]
823    fn test_cold_start_optimizer_custom() {
824        let opt = ColdStartOptimizer::new(Duration::from_millis(100), 5);
825        assert_eq!(opt.target_latency(), Duration::from_millis(100));
826        assert_eq!(opt.min_prewarm_connections(), 5);
827    }
828
829    #[test]
830    fn test_cold_start_min_prewarm_at_least_1() {
831        let opt = ColdStartOptimizer::new(Duration::from_millis(150), 0);
832        assert_eq!(opt.min_prewarm_connections(), 1);
833    }
834
835    #[test]
836    fn test_cold_start_on_cold_start() {
837        let opt = ColdStartOptimizer::default();
838        let stats = opt.on_cold_start();
839        assert_eq!(stats.warmed_connections, 2);
840        assert_eq!(opt.cold_start_count(), 1);
841    }
842
843    #[test]
844    fn test_cold_start_p95_empty() {
845        let opt = ColdStartOptimizer::default();
846        assert_eq!(opt.p95_latency(), Duration::ZERO);
847    }
848
849    #[test]
850    fn test_cold_start_p95_with_samples() {
851        let opt = ColdStartOptimizer::default();
852        for i in 1..=100 {
853            opt.record_latency(Duration::from_millis(i));
854        }
855        let p95 = opt.p95_latency();
856        assert!(p95 >= Duration::from_millis(95));
857    }
858
859    #[test]
860    fn test_cold_start_partial_available_on_timeout() {
861        let opt = ColdStartOptimizer::new(Duration::from_nanos(1), 2);
862        let stats = opt.on_cold_start();
863        assert!(stats.partial_available || stats.elapsed <= Duration::from_nanos(1));
864    }
865}