Skip to main content

sz_rust_orm_facade/
pool_warmer.rs

1//! PoolWarmer — 连接池预热(P3 L3 调优)
2//!
3//! 启动时并发建立 N 个连接放入连接池,消除首次请求冷启动延迟。
4//! 预热失败时降级到懒加载(首次请求时建立连接)。
5//!
6//! ## 用法
7//!
8//! ```rust,ignore
9//! use sz_rust_orm_facade::pool_warmer::PoolWarmer;
10//!
11//! let warmer = PoolWarmer::new(10, || async {
12//!     // 建立连接的逻辑
13//!     Ok(())
14//! });
15//! warmer.warm().await?;
16//! ```
17
18use std::future::Future;
19use std::pin::Pin;
20use std::sync::Arc;
21use std::time::Duration;
22
23/// 预热错误
24#[derive(Debug, thiserror::Error)]
25pub enum WarmupError {
26    /// 连接建立失败
27    #[error("connection failed: {0}")]
28    ConnectionFailed(String),
29    /// 预热超时
30    #[error("warmup timeout after {0:?}")]
31    Timeout(Duration),
32    /// 部分预热失败
33    #[error("partial warmup failure: {succeeded}/{total} succeeded")]
34    PartialFailure { succeeded: u32, total: u32 },
35}
36
37/// 连接建立工厂(async 闭包,返回连接或错误)
38pub type ConnectFn =
39    Arc<dyn Fn() -> Pin<Box<dyn Future<Output = Result<(), WarmupError>> + Send>> + Send + Sync>;
40
41/// 连接池预热器
42///
43/// 启动时并发建立 `warmup_count` 个连接,消除首次请求冷启动。
44/// 预热失败时降级到懒加载,不阻塞启动。
45pub struct PoolWarmer {
46    /// 预热连接数
47    warmup_count: u32,
48    /// 连接建立工厂
49    connect_fn: ConnectFn,
50    /// 单连接超时
51    connect_timeout: Duration,
52}
53
54impl PoolWarmer {
55    /// 创建 PoolWarmer
56    ///
57    /// - `warmup_count`:预热连接数
58    /// - `connect_fn`:连接建立闭包(每次调用建立一个连接)
59    pub fn new<F, Fut>(warmup_count: u32, connect_fn: F) -> Self
60    where
61        F: Fn() -> Fut + Send + Sync + 'static,
62        Fut: Future<Output = Result<(), WarmupError>> + Send + 'static,
63    {
64        Self {
65            warmup_count,
66            connect_fn: Arc::new(move || Box::pin(connect_fn())),
67            connect_timeout: Duration::from_secs(10),
68        }
69    }
70
71    /// 设置单连接超时
72    pub fn with_timeout(mut self, timeout: Duration) -> Self {
73        self.connect_timeout = timeout;
74        self
75    }
76
77    /// 预热连接数
78    pub fn warmup_count(&self) -> u32 {
79        self.warmup_count
80    }
81
82    /// 执行预热(并发建立 N 个连接)
83    ///
84    /// 成功返回 `Ok(())`,部分失败返回 `WarmupError::PartialFailure`。
85    /// 全部失败返回 `WarmupError::ConnectionFailed`。
86    pub async fn warm(&self) -> Result<(), WarmupError> {
87        if self.warmup_count == 0 {
88            return Ok(());
89        }
90
91        let mut handles = Vec::with_capacity(self.warmup_count as usize);
92        for _ in 0..self.warmup_count {
93            let connect_fn = self.connect_fn.clone();
94            let timeout = self.connect_timeout;
95            handles.push(tokio::spawn(async move {
96                tokio::time::timeout(timeout, connect_fn()).await
97            }));
98        }
99
100        let mut succeeded = 0u32;
101        let mut last_error = String::new();
102        for handle in handles {
103            match handle.await {
104                Ok(Ok(Ok(()))) => succeeded += 1,
105                Ok(Ok(Err(e))) => last_error = e.to_string(),
106                Ok(Err(_)) => last_error = "timeout".to_string(),
107                Err(e) => last_error = e.to_string(),
108            }
109        }
110
111        if succeeded == self.warmup_count {
112            Ok(())
113        } else if succeeded > 0 {
114            Err(WarmupError::PartialFailure {
115                succeeded,
116                total: self.warmup_count,
117            })
118        } else {
119            Err(WarmupError::ConnectionFailed(last_error))
120        }
121    }
122}
123
124impl std::fmt::Debug for PoolWarmer {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(
127            f,
128            "PoolWarmer {{ warmup_count: {}, timeout: {:?} }}",
129            self.warmup_count, self.connect_timeout
130        )
131    }
132}
133
134// ============================================================================
135// 单元测试
136// ============================================================================
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use std::sync::atomic::{AtomicU32, Ordering};
142
143    #[tokio::test]
144    async fn test_pool_warmer_success() {
145        let counter = Arc::new(AtomicU32::new(0));
146        let counter_clone = counter.clone();
147        let warmer = PoolWarmer::new(5, move || {
148            let c = counter_clone.clone();
149            async move {
150                c.fetch_add(1, Ordering::Relaxed);
151                Ok(())
152            }
153        });
154        let result = warmer.warm().await;
155        assert!(result.is_ok());
156        assert_eq!(counter.load(Ordering::Relaxed), 5);
157    }
158
159    #[tokio::test]
160    async fn test_pool_warmer_zero_count() {
161        let warmer = PoolWarmer::new(0, || async { Ok(()) });
162        let result = warmer.warm().await;
163        assert!(result.is_ok());
164    }
165
166    #[tokio::test]
167    async fn test_pool_warmer_partial_failure() {
168        let counter = Arc::new(AtomicU32::new(0));
169        let counter_clone = counter.clone();
170        let warmer = PoolWarmer::new(4, move || {
171            let c = counter_clone.clone();
172            async move {
173                let n = c.fetch_add(1, Ordering::Relaxed);
174                if n < 2 {
175                    Ok(())
176                } else {
177                    Err(WarmupError::ConnectionFailed("mock fail".to_string()))
178                }
179            }
180        });
181        let result = warmer.warm().await;
182        assert!(matches!(
183            result,
184            Err(WarmupError::PartialFailure {
185                succeeded: 2,
186                total: 4
187            })
188        ));
189    }
190
191    #[tokio::test]
192    async fn test_pool_warmer_all_fail() {
193        let warmer = PoolWarmer::new(3, || async {
194            Err(WarmupError::ConnectionFailed("mock fail".to_string()))
195        });
196        let result = warmer.warm().await;
197        assert!(matches!(result, Err(WarmupError::ConnectionFailed(_))));
198    }
199
200    #[tokio::test]
201    async fn test_pool_warmer_timeout() {
202        let warmer = PoolWarmer::new(1, || async {
203            tokio::time::sleep(Duration::from_secs(60)).await;
204            Ok(())
205        })
206        .with_timeout(Duration::from_millis(50));
207        let result = warmer.warm().await;
208        assert!(result.is_err());
209    }
210
211    #[test]
212    fn test_pool_warmer_warmup_count() {
213        let warmer = PoolWarmer::new(10, || async { Ok(()) });
214        assert_eq!(warmer.warmup_count(), 10);
215    }
216
217    #[test]
218    fn test_pool_warmer_debug() {
219        let warmer = PoolWarmer::new(5, || async { Ok(()) });
220        let s = format!("{warmer:?}");
221        assert!(s.contains("warmup_count: 5"));
222    }
223}