perf(db): add exponential backoff retry helper with tests

引入 src/db/retry.rs,提供 base * 2^attempt + jitter 的退避序列,
取代 pool.rs 中固定 2s 重试。MAX_RETRIES 将在下一个提交中被 get_conn 使用。
This commit is contained in:
xfy 2026-06-18 11:05:22 +08:00
parent c6161764a9
commit 104ff427f9
2 changed files with 61 additions and 0 deletions

View File

@ -12,6 +12,10 @@
#[cfg(feature = "server")]
pub mod pool;
/// 连接获取的指数退避重试策略,仅在启用 server feature 时编译。
#[cfg(feature = "server")]
pub mod retry;
/// 占位连接池实现,仅在不启用 server feature 时编译。
///
/// `DummyPool` 是一个最小 stub它提供与真实连接池相同的公开接口形状

57
src/db/retry.rs Normal file
View File

@ -0,0 +1,57 @@
//! 数据库连接获取的指数退避重试策略。
//!
//! 取代 pool.rs 中固定 2s 间隔的重试:每次重试间隔 = base * 2^attempt
//! 再叠加 [0, base) 的随机 jitter避免多请求同步重试形成惊群。
//! 仅在 `feature = "server"` 时编译。
#[cfg(feature = "server")]
use std::time::Duration;
/// 退避基准间隔(首次重试前的等待约为 base随后翻倍
#[cfg(feature = "server")]
const BASE_BACKOFF: Duration = Duration::from_millis(200);
/// 最大重试次数(不含首次尝试)。
#[cfg(feature = "server")]
pub const MAX_RETRIES: u32 = 3;
/// 计算第 `attempt` 次重试attempt 从 0 开始)前的等待时长。
///
/// 公式base * 2^attempt再叠加 [0, base) 的 jitter。
/// jitter 由调用方传入的随机比例 [0.0, 1.0) 决定,便于测试时锁定为 0。
#[cfg(feature = "server")]
pub fn backoff_for(attempt: u32, jitter_ratio: f64) -> Duration {
debug_assert!((0.0..=1.0).contains(&jitter_ratio));
let exp = u32::checked_shl(1, attempt).unwrap_or(1 << 30);
let base_ms = BASE_BACKOFF.as_millis() as u64;
let core = base_ms.saturating_mul(exp as u64);
let jitter = (base_ms as f64 * jitter_ratio) as u64;
Duration::from_millis(core.saturating_add(jitter))
}
#[cfg(all(test, feature = "server"))]
mod tests {
use super::*;
#[test]
fn backoff_grows_exponentially_without_jitter() {
// jitter=0 时序列应严格翻倍200, 400, 800 ms。
assert_eq!(backoff_for(0, 0.0), Duration::from_millis(200));
assert_eq!(backoff_for(1, 0.0), Duration::from_millis(400));
assert_eq!(backoff_for(2, 0.0), Duration::from_millis(800));
}
#[test]
fn backoff_includes_jitter_within_base_range() {
// jitter_ratio=0.5 时在 core 上叠加 base*0.5 = 100ms。
assert_eq!(backoff_for(0, 0.5), Duration::from_millis(300));
assert_eq!(backoff_for(1, 0.5), Duration::from_millis(500));
}
#[test]
fn backoff_clamps_large_attempt() {
// 超大 attempt 不应 panic应靠 saturating 保护返回一个大但有界的值。
let d = backoff_for(40, 0.0);
assert!(d.as_millis() > 0);
}
}