mbus_core/transport/retry.rs
1//! Retry scheduling: backoff strategies and jitter.
2
3/// Application-provided callback used to generate randomness for retry jitter.
4///
5/// The callback returns a raw `u32` value that is consumed by jitter logic.
6/// The distribution does not need to be cryptographically secure. A simple
7/// pseudo-random source from the target platform is sufficient.
8pub type RetryRandomFn = fn() -> u32;
9
10/// Retry delay strategy used after a request times out.
11///
12/// The delay is computed per retry attempt in a poll-driven manner. No internal
13/// sleeping or blocking waits are performed by the library.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum BackoffStrategy {
16 /// Retry immediately after timeout detection.
17 #[default]
18 Immediate,
19 /// Retry using a constant delay in milliseconds.
20 Fixed {
21 /// Delay applied before each retry.
22 delay_ms: u32,
23 },
24 /// Retry with an exponential sequence: `base_delay_ms * 2^(attempt-1)`.
25 Exponential {
26 /// Base delay for the first retry attempt.
27 base_delay_ms: u32,
28 /// Upper bound used to clamp growth.
29 max_delay_ms: u32,
30 },
31 /// Retry with a linear sequence: `initial_delay_ms + (attempt-1) * increment_ms`.
32 Linear {
33 /// Delay for the first retry attempt.
34 initial_delay_ms: u32,
35 /// Increment added on every subsequent retry.
36 increment_ms: u32,
37 /// Upper bound used to clamp growth.
38 max_delay_ms: u32,
39 },
40}
41
42impl BackoffStrategy {
43 /// Computes the base retry delay in milliseconds for a 1-based retry attempt index.
44 ///
45 /// `retry_attempt` is expected to start at `1` for the first retry after the
46 /// initial request timeout.
47 pub fn delay_ms_for_retry(&self, retry_attempt: u8) -> u32 {
48 let attempt = retry_attempt.max(1);
49 match self {
50 BackoffStrategy::Immediate => 0,
51 BackoffStrategy::Fixed { delay_ms } => *delay_ms,
52 BackoffStrategy::Exponential {
53 base_delay_ms,
54 max_delay_ms,
55 } => {
56 let shift = (attempt.saturating_sub(1)).min(31);
57 let factor = 1u32 << shift;
58 base_delay_ms.saturating_mul(factor).min(*max_delay_ms)
59 }
60 BackoffStrategy::Linear {
61 initial_delay_ms,
62 increment_ms,
63 max_delay_ms,
64 } => {
65 let growth = increment_ms.saturating_mul((attempt.saturating_sub(1)) as u32);
66 initial_delay_ms.saturating_add(growth).min(*max_delay_ms)
67 }
68 }
69 }
70}
71
72/// Jitter strategy applied on top of computed backoff delay.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub enum JitterStrategy {
75 /// Do not apply jitter.
76 #[default]
77 None,
78 /// Apply symmetric percentage jitter around the base delay.
79 ///
80 /// For example, with `percent = 20` and base `100ms`, the final delay is
81 /// in the range `[80ms, 120ms]`.
82 Percentage {
83 /// Maximum percentage variation from the base delay.
84 percent: u8,
85 },
86 /// Apply symmetric bounded jitter in milliseconds around the base delay.
87 ///
88 /// For example, with `max_jitter_ms = 15` and base `100ms`, the final delay is
89 /// in the range `[85ms, 115ms]`.
90 BoundedMs {
91 /// Maximum absolute jitter in milliseconds.
92 max_jitter_ms: u32,
93 },
94}
95
96impl JitterStrategy {
97 /// Applies jitter to `base_delay_ms` using an application-provided random callback.
98 ///
99 /// If jitter is disabled or no callback is provided, this method returns `base_delay_ms`.
100 pub fn apply(self, base_delay_ms: u32, random_fn: Option<RetryRandomFn>) -> u32 {
101 let delta = match self {
102 JitterStrategy::None => return base_delay_ms,
103 JitterStrategy::Percentage { percent } => {
104 if percent == 0 || base_delay_ms == 0 {
105 return base_delay_ms;
106 }
107 base_delay_ms.saturating_mul((percent.min(100)) as u32) / 100
108 }
109 JitterStrategy::BoundedMs { max_jitter_ms } => {
110 if max_jitter_ms == 0 {
111 return base_delay_ms;
112 }
113 max_jitter_ms
114 }
115 };
116
117 let random = match random_fn {
118 Some(cb) => cb(),
119 None => return base_delay_ms,
120 };
121
122 let span = delta.saturating_mul(2).saturating_add(1);
123 if span == 0 {
124 return base_delay_ms;
125 }
126
127 let offset = (random % span) as i64 - delta as i64;
128 let jittered = base_delay_ms as i64 + offset;
129 jittered.max(0) as u32
130 }
131}