oxihuman_core/
http_retry_policy.rs1#![allow(dead_code)]
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum BackoffStrategy {
10 Fixed,
11 Exponential,
12 LinearJitter,
13}
14
15#[derive(Clone, Debug)]
17pub struct HttpRetryPolicyConfig {
18 pub max_attempts: u32,
19 pub base_delay_ms: u64,
20 pub max_delay_ms: u64,
21 pub strategy: BackoffStrategy,
22 pub jitter_ms: u64,
23}
24
25impl Default for HttpRetryPolicyConfig {
26 fn default() -> Self {
27 Self {
28 max_attempts: 3,
29 base_delay_ms: 100,
30 max_delay_ms: 30_000,
31 strategy: BackoffStrategy::Exponential,
32 jitter_ms: 50,
33 }
34 }
35}
36
37pub struct RetryState {
39 pub config: HttpRetryPolicyConfig,
40 pub attempt: u32,
41}
42
43pub fn new_retry_state(config: HttpRetryPolicyConfig) -> RetryState {
45 RetryState { config, attempt: 0 }
46}
47
48pub fn delay_for_attempt(config: &HttpRetryPolicyConfig, attempt: u32) -> u64 {
50 let raw = match config.strategy {
51 BackoffStrategy::Fixed => config.base_delay_ms,
52 BackoffStrategy::Exponential => {
53 let shift = attempt.min(30);
54 let factor = 1u64.checked_shl(shift).unwrap_or(u64::MAX);
55 config.base_delay_ms.saturating_mul(factor)
56 }
57 BackoffStrategy::LinearJitter => config.base_delay_ms.saturating_mul(attempt as u64 + 1),
58 };
59 raw.min(config.max_delay_ms)
60 .saturating_add(config.jitter_ms)
61}
62
63pub fn can_retry(state: &RetryState) -> bool {
65 state.attempt < state.config.max_attempts
66}
67
68pub fn next_delay_ms(state: &mut RetryState) -> Option<u64> {
70 if !can_retry(state) {
71 return None;
72 }
73 let delay = delay_for_attempt(&state.config, state.attempt);
74 state.attempt += 1;
75 Some(delay)
76}
77
78pub fn reset_retry(state: &mut RetryState) {
80 state.attempt = 0;
81}
82
83pub fn remaining_attempts(state: &RetryState) -> u32 {
85 state.config.max_attempts.saturating_sub(state.attempt)
86}
87
88impl RetryState {
89 pub fn new(config: HttpRetryPolicyConfig) -> Self {
91 new_retry_state(config)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 fn make_state(max: u32, strategy: BackoffStrategy) -> RetryState {
100 new_retry_state(HttpRetryPolicyConfig {
101 max_attempts: max,
102 base_delay_ms: 100,
103 max_delay_ms: 10_000,
104 strategy,
105 jitter_ms: 0,
106 })
107 }
108
109 #[test]
110 fn test_can_retry_when_attempts_remain() {
111 let state = make_state(3, BackoffStrategy::Fixed);
112 assert!(can_retry(&state));
113 }
114
115 #[test]
116 fn test_cannot_retry_when_exhausted() {
117 let mut state = make_state(2, BackoffStrategy::Fixed);
118 next_delay_ms(&mut state);
119 next_delay_ms(&mut state);
120 assert!(!can_retry(&state));
121 }
122
123 #[test]
124 fn test_fixed_delay_constant() {
125 let cfg = HttpRetryPolicyConfig {
126 strategy: BackoffStrategy::Fixed,
127 jitter_ms: 0,
128 ..Default::default()
129 };
130 let d0 = delay_for_attempt(&cfg, 0);
131 let d1 = delay_for_attempt(&cfg, 1);
132 assert_eq!(d0, d1);
133 }
134
135 #[test]
136 fn test_exponential_delay_grows() {
137 let cfg = HttpRetryPolicyConfig {
138 strategy: BackoffStrategy::Exponential,
139 jitter_ms: 0,
140 ..Default::default()
141 };
142 let d0 = delay_for_attempt(&cfg, 0);
143 let d1 = delay_for_attempt(&cfg, 1);
144 assert!(d1 > d0);
145 }
146
147 #[test]
148 fn test_delay_capped_at_max() {
149 let cfg = HttpRetryPolicyConfig {
150 max_delay_ms: 200,
151 base_delay_ms: 100,
152 strategy: BackoffStrategy::Exponential,
153 jitter_ms: 0,
154 max_attempts: 10,
155 };
156 let d = delay_for_attempt(&cfg, 20);
157 assert!(d <= 200);
158 }
159
160 #[test]
161 fn test_next_delay_ms_returns_none_when_exhausted() {
162 let mut state = make_state(1, BackoffStrategy::Fixed);
163 let _ = next_delay_ms(&mut state);
164 assert!(next_delay_ms(&mut state).is_none());
165 }
166
167 #[test]
168 fn test_reset_restores_full_budget() {
169 let mut state = make_state(3, BackoffStrategy::Fixed);
170 next_delay_ms(&mut state);
171 next_delay_ms(&mut state);
172 reset_retry(&mut state);
173 assert_eq!(remaining_attempts(&state), 3);
174 }
175
176 #[test]
177 fn test_remaining_attempts_decrements() {
178 let mut state = make_state(3, BackoffStrategy::Fixed);
179 assert_eq!(remaining_attempts(&state), 3);
180 next_delay_ms(&mut state);
181 assert_eq!(remaining_attempts(&state), 2);
182 }
183
184 #[test]
185 fn test_linear_jitter_grows_linearly() {
186 let cfg = HttpRetryPolicyConfig {
187 strategy: BackoffStrategy::LinearJitter,
188 jitter_ms: 0,
189 ..Default::default()
190 };
191 let d0 = delay_for_attempt(&cfg, 0);
192 let d2 = delay_for_attempt(&cfg, 2);
193 assert!(d2 > d0);
194 }
195}