134 lines
4.7 KiB
Rust
134 lines
4.7 KiB
Rust
const MCP_BASE_RESTART_DELAY_MS: u64 = 1000; // Start with 1 second
|
|
const MCP_MAX_RESTART_DELAY_MS: u64 = 30000; // Cap at 30 seconds
|
|
const MCP_BACKOFF_MULTIPLIER: f64 = 2.0; // Double the delay each time
|
|
|
|
/// Calculate exponential backoff delay with jitter
|
|
///
|
|
/// # Arguments
|
|
/// * `attempt` - The current restart attempt number (1-based)
|
|
///
|
|
/// # Returns
|
|
/// * `u64` - Delay in milliseconds, capped at MCP_MAX_RESTART_DELAY_MS
|
|
pub fn calculate_exponential_backoff_delay(attempt: u32) -> u64 {
|
|
use std::cmp;
|
|
|
|
// Calculate base exponential delay: base_delay * multiplier^(attempt-1)
|
|
let exponential_delay =
|
|
(MCP_BASE_RESTART_DELAY_MS as f64) * MCP_BACKOFF_MULTIPLIER.powi((attempt - 1) as i32);
|
|
|
|
// Cap the delay at maximum
|
|
let capped_delay = cmp::min(exponential_delay as u64, MCP_MAX_RESTART_DELAY_MS);
|
|
|
|
// Add jitter (±25% randomness) to prevent thundering herd
|
|
let jitter_range = (capped_delay as f64 * 0.25) as u64;
|
|
let jitter = if jitter_range > 0 {
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
// Use attempt number as seed for deterministic but varied jitter
|
|
let mut hasher = DefaultHasher::new();
|
|
attempt.hash(&mut hasher);
|
|
let hash = hasher.finish();
|
|
|
|
// Convert hash to jitter value in range [-jitter_range, +jitter_range]
|
|
let jitter_offset = (hash % (jitter_range * 2)) as i64 - jitter_range as i64;
|
|
jitter_offset
|
|
} else {
|
|
0
|
|
};
|
|
|
|
// Apply jitter while ensuring delay stays positive and within bounds
|
|
let final_delay = cmp::max(
|
|
100, // Minimum 100ms delay
|
|
cmp::min(
|
|
MCP_MAX_RESTART_DELAY_MS,
|
|
(capped_delay as i64 + jitter) as u64,
|
|
),
|
|
);
|
|
|
|
final_delay
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_calculate_exponential_backoff_delay_basic() {
|
|
let delay1 = calculate_exponential_backoff_delay(1);
|
|
let delay2 = calculate_exponential_backoff_delay(2);
|
|
let delay3 = calculate_exponential_backoff_delay(3);
|
|
|
|
// First attempt should be around base delay (1000ms) ± jitter
|
|
assert!(delay1 >= 100 && delay1 <= 2000);
|
|
|
|
// Second attempt should be roughly double
|
|
assert!(delay2 >= 1000 && delay2 <= 4000);
|
|
|
|
// Third attempt should be roughly quadruple
|
|
assert!(delay3 >= 2000 && delay3 <= 6000);
|
|
|
|
// Generally increasing pattern
|
|
assert!(delay1 < delay3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_exponential_backoff_delay_max_cap() {
|
|
// Very high attempt numbers should be capped at MAX_RESTART_DELAY_MS
|
|
let high_attempt_delay = calculate_exponential_backoff_delay(100);
|
|
assert!(high_attempt_delay <= MCP_MAX_RESTART_DELAY_MS);
|
|
assert!(high_attempt_delay >= 100); // Minimum delay
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_exponential_backoff_delay_minimum() {
|
|
// Even with jitter, should never go below minimum
|
|
for attempt in 1..=10 {
|
|
let delay = calculate_exponential_backoff_delay(attempt);
|
|
assert!(delay >= 100, "Delay {} for attempt {} is below minimum", delay, attempt);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_exponential_backoff_delay_deterministic() {
|
|
// Same attempt number should produce same delay (deterministic jitter)
|
|
let delay1_a = calculate_exponential_backoff_delay(5);
|
|
let delay1_b = calculate_exponential_backoff_delay(5);
|
|
assert_eq!(delay1_a, delay1_b);
|
|
|
|
let delay2_a = calculate_exponential_backoff_delay(10);
|
|
let delay2_b = calculate_exponential_backoff_delay(10);
|
|
assert_eq!(delay2_a, delay2_b);
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_exponential_backoff_delay_progression() {
|
|
// Test the general progression pattern
|
|
let mut delays = Vec::new();
|
|
for attempt in 1..=8 {
|
|
delays.push(calculate_exponential_backoff_delay(attempt));
|
|
}
|
|
|
|
// Should not exceed maximum
|
|
for delay in &delays {
|
|
assert!(*delay <= MCP_MAX_RESTART_DELAY_MS);
|
|
}
|
|
|
|
// Earlier attempts should generally be smaller than later ones
|
|
// (allowing some variance due to jitter)
|
|
assert!(delays[0] < delays[6]); // 1st vs 7th attempt
|
|
assert!(delays[1] < delays[7]); // 2nd vs 8th attempt
|
|
}
|
|
|
|
#[test]
|
|
fn test_constants() {
|
|
// Verify our constants are reasonable
|
|
assert_eq!(MCP_BASE_RESTART_DELAY_MS, 1000);
|
|
assert_eq!(MCP_MAX_RESTART_DELAY_MS, 30000);
|
|
assert_eq!(MCP_BACKOFF_MULTIPLIER, 2.0);
|
|
|
|
// Max should be greater than base
|
|
assert!(MCP_MAX_RESTART_DELAY_MS > MCP_BASE_RESTART_DELAY_MS);
|
|
}
|
|
}
|