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