mailrs_outbound_queue/
retry.rs1use mailrs_backoff::Backoff;
8
9pub fn retry_delay_secs(attempt: u32) -> u64 {
18 Backoff::smtp_outbound().base_delay(attempt).as_secs()
19}
20
21pub fn retry_delay_secs_jittered(attempt: u32, seed: u64) -> u64 {
30 Backoff::smtp_outbound().delay(attempt, seed).as_secs()
31}
32
33pub fn should_bounce(attempt: u32, max_attempts: u32) -> bool {
37 Backoff::should_give_up(attempt, max_attempts)
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43
44 #[test]
57 fn delay_schedule_new_curve() {
58 assert_eq!(retry_delay_secs(0), 60);
59 assert_eq!(retry_delay_secs(1), 150);
60 assert_eq!(retry_delay_secs(2), 375);
61 let d3 = retry_delay_secs(3);
63 assert!((937..=938).contains(&d3), "expected ~937, got {d3}");
64 let d4 = retry_delay_secs(4);
65 assert!((2343..=2344).contains(&d4), "expected ~2343, got {d4}");
66 }
67
68 #[test]
69 fn cap_at_8h() {
70 assert_eq!(retry_delay_secs(7), 28800);
72 assert_eq!(retry_delay_secs(8), 28800);
73 assert_eq!(retry_delay_secs(100), 28800);
74 }
75
76 #[test]
77 fn bounce_at_max() {
78 assert!(should_bounce(5, 5));
79 assert!(should_bounce(10, 5));
80 }
81
82 #[test]
83 fn no_bounce_below_max() {
84 assert!(!should_bounce(0, 5));
85 assert!(!should_bounce(4, 5));
86 }
87
88 #[test]
89 fn delay_is_monotonically_increasing() {
90 let mut prev = 0u64;
91 for i in 0..8u32 {
92 let d = retry_delay_secs(i);
93 assert!(
94 d > prev,
95 "delay at slot {i} ({d}) is not greater than previous ({prev})"
96 );
97 prev = d;
98 }
99 }
100
101 #[test]
102 fn should_bounce_boundary_exact() {
103 assert!(should_bounce(1, 1));
104 assert!(should_bounce(0, 0));
105 }
106
107 #[test]
108 fn should_bounce_large_values() {
109 assert!(!should_bounce(u32::MAX - 1, u32::MAX));
110 assert!(should_bounce(u32::MAX, u32::MAX));
111 }
112
113 #[test]
114 fn delay_first_attempt_is_one_minute() {
115 assert_eq!(retry_delay_secs(0), 60);
116 }
117
118 #[test]
119 fn delay_cap_at_various_overflow_attempts() {
120 for attempt in [8, 9, 10, 50, 100, 255, 1000, u32::MAX] {
121 assert_eq!(
122 retry_delay_secs(attempt),
123 28800,
124 "attempt {attempt} should be capped at 8h"
125 );
126 }
127 }
128
129 #[test]
130 fn delay_each_step_grows() {
131 for i in 1..7u32 {
132 let prev = retry_delay_secs(i - 1);
133 let curr = retry_delay_secs(i);
134 assert!(
135 curr >= prev * 2,
136 "step {i}: {curr} should be at least 2× {prev}"
137 );
138 }
139 }
140
141 #[test]
142 fn delay_minimum_is_60_seconds() {
143 assert_eq!(retry_delay_secs(0), 60);
144 for n in 0..8u32 {
146 assert!(retry_delay_secs(n) >= 60, "attempt {n} returned < 60");
147 }
148 }
149
150 #[test]
153 fn jittered_delay_within_full_range() {
154 let base = retry_delay_secs(3);
155 for seed in 0..50u64 {
156 let d = retry_delay_secs_jittered(3, seed);
157 assert!(d < base, "seed {seed}: jittered {d} >= base {base}");
159 }
160 }
161
162 #[test]
163 fn jittered_deterministic_with_same_seed() {
164 assert_eq!(
165 retry_delay_secs_jittered(3, 42),
166 retry_delay_secs_jittered(3, 42),
167 );
168 }
169
170 #[test]
171 fn jittered_attempt_zero_bounded() {
172 for seed in 0..20u64 {
174 let d = retry_delay_secs_jittered(0, seed);
175 assert!(d < 60, "seed {seed}: {d} >= 60");
176 }
177 }
178
179 #[test]
180 fn should_bounce_one_attempt_max() {
181 assert!(!should_bounce(0, 1));
182 assert!(should_bounce(1, 1));
183 assert!(should_bounce(2, 1));
184 }
185
186 #[test]
187 fn should_bounce_high_max_attempts() {
188 let max = 100;
189 assert!(!should_bounce(99, max));
190 assert!(should_bounce(100, max));
191 assert!(should_bounce(101, max));
192 }
193
194 #[test]
195 fn delay_returns_u64() {
196 let d: u64 = retry_delay_secs(0);
197 assert_eq!(d, 60u64);
198 }
199}