crate_seq_registry/
backoff.rs1#[cfg(test)]
4#[path = "backoff_tests.rs"]
5mod tests;
6
7use std::path::Path;
8use std::time::Duration;
9
10use crate::publish::{run_cargo_publish, PublishOutcome};
11use crate::Error;
12
13pub struct BackoffConfig {
15 pub base_ms: u64,
17 pub cap_ms: u64,
19 pub max_retries: u32,
21 pub jitter_max_ms: u64,
23}
24
25impl Default for BackoffConfig {
26 fn default() -> Self {
27 Self {
28 base_ms: 1_000,
29 cap_ms: 60_000,
30 max_retries: 5,
31 jitter_max_ms: 1_000,
32 }
33 }
34}
35
36pub(crate) fn compute_delay(attempt: u32, config: &BackoffConfig, jitter: u64) -> u64 {
38 let multiplier = 1u64.checked_shl(attempt).unwrap_or(u64::MAX);
40 let exponential = config.base_ms.saturating_mul(multiplier);
41 exponential.min(config.cap_ms).saturating_add(jitter)
42}
43
44fn jitter_for_attempt(attempt: u32, jitter_max_ms: u64) -> u64 {
49 if jitter_max_ms == 0 {
50 return 0;
51 }
52 let hash = u64::from(attempt).wrapping_mul(2_654_435_761);
54 hash % jitter_max_ms
55}
56
57pub fn backoff_publish(
66 dir: &Path,
67 token: Option<&str>,
68 config: &BackoffConfig,
69) -> Result<PublishOutcome, Error> {
70 for attempt in 0..=config.max_retries {
71 let outcome = run_cargo_publish(dir, token)?;
72
73 match outcome {
74 PublishOutcome::Success | PublishOutcome::AlreadyPublished => {
75 return Ok(PublishOutcome::Success);
76 }
77 PublishOutcome::Failed(_) => return Ok(outcome),
78 PublishOutcome::RateLimited => {
79 if attempt < config.max_retries {
80 let jitter = jitter_for_attempt(attempt, config.jitter_max_ms);
81 let delay_ms = compute_delay(attempt, config, jitter);
82 std::thread::sleep(Duration::from_millis(delay_ms));
83 } else {
84 return Ok(PublishOutcome::RateLimited);
85 }
86 }
87 }
88 }
89
90 Ok(PublishOutcome::RateLimited)
91}