CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
121 lines
3.7 KiB
Rust
121 lines
3.7 KiB
Rust
//! 指数退避 + 抖动。
|
||
//!
|
||
//! 修 HMCL 的坑(analyse/03 §7.6 第 5 点):它的重试循环里没有 sleep,5xx 会变成
|
||
//! 「6 次瞬时连打」,把弱网镜像直接压垮。这里给出 1s / 2s / 4s… 上限 30s,
|
||
//! ±30% 抖动打散多实例/多文件的同步重试风暴。
|
||
//!
|
||
//! 抖动源是自带的 xorshift64*,不引 `rand`(体积 + Win7 依赖面)。
|
||
|
||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
/// 退避策略(可序列化进设置)。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||
pub struct BackoffPolicy {
|
||
/// 首次退避(毫秒)。
|
||
pub base_ms: u64,
|
||
/// 单次退避上限(毫秒)。
|
||
pub max_ms: u64,
|
||
/// 抖动比例(0.0~1.0,0.3 = ±30%)。
|
||
pub jitter_ratio: f64,
|
||
/// 每个源的尝试次数上限。
|
||
pub max_attempts: u32,
|
||
}
|
||
|
||
impl BackoffPolicy {
|
||
/// 不重试(一次失败立刻换源)。
|
||
#[must_use]
|
||
pub const fn none() -> Self {
|
||
Self {
|
||
base_ms: 0,
|
||
max_ms: 0,
|
||
jitter_ratio: 0.0,
|
||
max_attempts: 1,
|
||
}
|
||
}
|
||
|
||
/// 第 `attempt` 次重试前的**无抖动**等待时间(`attempt` 从 0 起)。
|
||
#[must_use]
|
||
pub fn delay(&self, attempt: u32) -> Duration {
|
||
let factor = 1u64.checked_shl(attempt.min(32)).unwrap_or(u64::MAX).max(1);
|
||
Duration::from_millis(self.base_ms.saturating_mul(factor).min(self.max_ms))
|
||
}
|
||
|
||
/// 第 `attempt` 次重试前的等待时间,`rng` 为 `[0,1)` 的抖动量(可复现,便于单测)。
|
||
///
|
||
/// 结果永不超过 `max_ms`,且永不低于 `base_ms`(当 `base_ms <= max_ms` 时)。
|
||
#[must_use]
|
||
pub fn delay_jittered(&self, attempt: u32, rng: f64) -> Duration {
|
||
let base_ms = self.delay(attempt).as_millis() as f64;
|
||
let ratio = self.jitter_ratio.clamp(0.0, 1.0);
|
||
let factor = 1.0 + ratio * (2.0 * rng.clamp(0.0, 1.0) - 1.0);
|
||
let ceiling = (self.max_ms as f64).max(base_ms);
|
||
let jittered_ms = (base_ms * factor).clamp(0.0, ceiling);
|
||
Duration::from_millis(jittered_ms as u64)
|
||
}
|
||
|
||
/// 用抖动源取一次等待时间。
|
||
#[must_use]
|
||
pub fn next_delay(&self, attempt: u32, jitter: &mut Jitter) -> Duration {
|
||
self.delay_jittered(attempt, jitter.next_fraction())
|
||
}
|
||
}
|
||
|
||
impl Default for BackoffPolicy {
|
||
fn default() -> Self {
|
||
Self {
|
||
base_ms: 1_000,
|
||
max_ms: 30_000,
|
||
jitter_ratio: 0.3,
|
||
max_attempts: 3,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// xorshift64* 抖动源:确定性、无外部依赖。
|
||
#[derive(Debug, Clone)]
|
||
pub struct Jitter {
|
||
state: u64,
|
||
}
|
||
|
||
impl Jitter {
|
||
/// 固定种子构造(同种子的序列可复现 → 单测友好)。
|
||
#[must_use]
|
||
pub const fn new(seed: u64) -> Self {
|
||
Self {
|
||
state: if seed == 0 {
|
||
0x9E37_79B9_7F4A_7C15
|
||
} else {
|
||
seed
|
||
},
|
||
}
|
||
}
|
||
|
||
/// 下一个 64 位随机数。
|
||
pub fn next_u64(&mut self) -> u64 {
|
||
let mut x = self.state;
|
||
x ^= x << 13;
|
||
x ^= x >> 7;
|
||
x ^= x << 17;
|
||
self.state = x;
|
||
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
|
||
}
|
||
|
||
/// 下一个 `[0,1)` 的浮点。
|
||
pub fn next_fraction(&mut self) -> f64 {
|
||
// 取高 53 位映射到 [0,1),与 f64 的尾数精度一致。
|
||
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
|
||
}
|
||
}
|
||
|
||
impl Default for Jitter {
|
||
fn default() -> Self {
|
||
let seed = SystemTime::now()
|
||
.duration_since(UNIX_EPOCH)
|
||
.map(|d| d.as_nanos() as u64)
|
||
.unwrap_or(0x1234_5678_9ABC_DEF0);
|
||
Self::new(seed)
|
||
}
|
||
}
|