openzeppelin_relayer/queues/
retry_config.rs1use crate::models::NetworkType;
2
3use super::QueueType;
4
5#[derive(Debug, Clone, Copy)]
7pub struct RetryBackoffConfig {
8 pub initial_ms: u64,
10 pub max_ms: u64,
12 pub jitter: f64,
14}
15
16pub const TX_REQUEST_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
18 initial_ms: 500,
19 max_ms: 5000,
20 jitter: 0.99,
21};
22pub const TX_SUBMISSION_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
24 initial_ms: 500,
25 max_ms: 2000,
26 jitter: 0.99,
27};
28pub const STATUS_GENERIC_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
30 initial_ms: 5000,
31 max_ms: 8000,
32 jitter: 0.99,
33};
34pub const STATUS_EVM_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
36 initial_ms: 8000,
37 max_ms: 12000,
38 jitter: 0.99,
39};
40pub const STATUS_STELLAR_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
42 initial_ms: 2000,
43 max_ms: 3000,
44 jitter: 0.99,
45};
46pub const NOTIFICATION_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
48 initial_ms: 2000,
49 max_ms: 8000,
50 jitter: 0.99,
51};
52pub const TOKEN_SWAP_REQUEST_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
54 initial_ms: 5000,
55 max_ms: 20000,
56 jitter: 0.99,
57};
58pub const TX_CLEANUP_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
60 initial_ms: 5000,
61 max_ms: 20000,
62 jitter: 0.99,
63};
64pub const SYSTEM_CLEANUP_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
66 initial_ms: 5000,
67 max_ms: 20000,
68 jitter: 0.99,
69};
70pub const RELAYER_HEALTH_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
72 initial_ms: 2000,
73 max_ms: 10000,
74 jitter: 0.99,
75};
76pub const TOKEN_SWAP_CRON_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
78 initial_ms: 2000,
79 max_ms: 5000,
80 jitter: 0.99,
81};
82
83pub fn status_backoff_config(network_type: Option<NetworkType>) -> RetryBackoffConfig {
89 match network_type {
90 Some(NetworkType::Evm) => STATUS_EVM_BACKOFF,
91 Some(NetworkType::Stellar) => STATUS_STELLAR_BACKOFF,
92 Some(NetworkType::Solana) | None => STATUS_GENERIC_BACKOFF,
93 }
94}
95
96pub fn retry_delay_secs(config: RetryBackoffConfig, attempt: usize) -> i32 {
102 let factor = 2_u64.saturating_pow(attempt.min(16) as u32);
103 let delay_ms = config.initial_ms.saturating_mul(factor).min(config.max_ms);
104 delay_ms.div_ceil(1000) as i32
105}
106
107pub fn status_check_retry_delay_secs(network_type: Option<NetworkType>, attempt: usize) -> i32 {
111 retry_delay_secs(status_backoff_config(network_type), attempt)
112}
113
114pub fn backoff_config_for_queue(queue_type: QueueType) -> RetryBackoffConfig {
119 match queue_type {
120 QueueType::TransactionRequest => TX_REQUEST_BACKOFF,
121 QueueType::TransactionSubmission => TX_SUBMISSION_BACKOFF,
122 QueueType::Notification => NOTIFICATION_BACKOFF,
123 QueueType::TokenSwapRequest => TOKEN_SWAP_REQUEST_BACKOFF,
124 QueueType::RelayerHealthCheck => RELAYER_HEALTH_BACKOFF,
125 QueueType::StatusCheck | QueueType::StatusCheckEvm | QueueType::StatusCheckStellar => {
126 STATUS_GENERIC_BACKOFF
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_status_check_retry_delay_secs_caps() {
137 assert_eq!(status_check_retry_delay_secs(Some(NetworkType::Evm), 0), 8);
138 assert_eq!(status_check_retry_delay_secs(Some(NetworkType::Evm), 1), 12);
139 assert_eq!(
140 status_check_retry_delay_secs(Some(NetworkType::Evm), 10),
141 12
142 );
143
144 assert_eq!(
145 status_check_retry_delay_secs(Some(NetworkType::Stellar), 0),
146 2
147 );
148 assert_eq!(
149 status_check_retry_delay_secs(Some(NetworkType::Stellar), 1),
150 3
151 );
152 assert_eq!(
153 status_check_retry_delay_secs(Some(NetworkType::Stellar), 10),
154 3
155 );
156
157 assert_eq!(
158 status_check_retry_delay_secs(Some(NetworkType::Solana), 0),
159 5
160 );
161 assert_eq!(
162 status_check_retry_delay_secs(Some(NetworkType::Solana), 1),
163 8
164 );
165 assert_eq!(
166 status_check_retry_delay_secs(Some(NetworkType::Solana), 10),
167 8
168 );
169 }
170
171 #[test]
172 fn test_status_backoff_config_selects_correct_profile() {
173 let evm = status_backoff_config(Some(NetworkType::Evm));
174 assert_eq!(evm.initial_ms, STATUS_EVM_BACKOFF.initial_ms);
175 assert_eq!(evm.max_ms, STATUS_EVM_BACKOFF.max_ms);
176
177 let stellar = status_backoff_config(Some(NetworkType::Stellar));
178 assert_eq!(stellar.initial_ms, STATUS_STELLAR_BACKOFF.initial_ms);
179
180 let solana = status_backoff_config(Some(NetworkType::Solana));
181 assert_eq!(solana.initial_ms, STATUS_GENERIC_BACKOFF.initial_ms);
182 }
183
184 #[test]
185 fn test_status_backoff_config_none_uses_generic() {
186 let none_cfg = status_backoff_config(None);
187 let solana_cfg = status_backoff_config(Some(NetworkType::Solana));
188 assert_eq!(none_cfg.initial_ms, solana_cfg.initial_ms);
189 assert_eq!(none_cfg.max_ms, solana_cfg.max_ms);
190 }
191
192 #[test]
193 fn test_retry_delay_secs_basic() {
194 assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 0), 1); assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 1), 1); assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 2), 2); assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 3), 4); assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 4), 5); assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 10), 5); }
202
203 #[test]
204 fn test_retry_delay_secs_never_exceeds_max() {
205 let configs = [
206 TX_REQUEST_BACKOFF,
207 TX_SUBMISSION_BACKOFF,
208 NOTIFICATION_BACKOFF,
209 TOKEN_SWAP_REQUEST_BACKOFF,
210 RELAYER_HEALTH_BACKOFF,
211 ];
212 for config in configs {
213 for attempt in 0..20 {
214 let delay = retry_delay_secs(config, attempt);
215 assert!(
216 delay <= config.max_ms.div_ceil(1000) as i32,
217 "attempt {attempt} delay {delay}s exceeds max {}ms",
218 config.max_ms
219 );
220 }
221 }
222 }
223
224 #[test]
225 fn test_retry_delay_secs_delegates_correctly() {
226 for attempt in 0..10 {
228 assert_eq!(
229 status_check_retry_delay_secs(Some(NetworkType::Evm), attempt),
230 retry_delay_secs(STATUS_EVM_BACKOFF, attempt),
231 );
232 }
233 }
234
235 #[test]
236 fn test_backoff_config_for_queue_maps_correctly() {
237 assert_eq!(
238 backoff_config_for_queue(QueueType::TransactionRequest).initial_ms,
239 TX_REQUEST_BACKOFF.initial_ms
240 );
241 assert_eq!(
242 backoff_config_for_queue(QueueType::TransactionSubmission).initial_ms,
243 TX_SUBMISSION_BACKOFF.initial_ms
244 );
245 assert_eq!(
246 backoff_config_for_queue(QueueType::Notification).initial_ms,
247 NOTIFICATION_BACKOFF.initial_ms
248 );
249 assert_eq!(
250 backoff_config_for_queue(QueueType::TokenSwapRequest).initial_ms,
251 TOKEN_SWAP_REQUEST_BACKOFF.initial_ms
252 );
253 assert_eq!(
254 backoff_config_for_queue(QueueType::RelayerHealthCheck).initial_ms,
255 RELAYER_HEALTH_BACKOFF.initial_ms
256 );
257 assert_eq!(
258 backoff_config_for_queue(QueueType::StatusCheck).initial_ms,
259 STATUS_GENERIC_BACKOFF.initial_ms
260 );
261 assert_eq!(
262 backoff_config_for_queue(QueueType::StatusCheckEvm).initial_ms,
263 STATUS_GENERIC_BACKOFF.initial_ms
264 );
265 assert_eq!(
266 backoff_config_for_queue(QueueType::StatusCheckStellar).initial_ms,
267 STATUS_GENERIC_BACKOFF.initial_ms
268 );
269 }
270
271 #[test]
272 fn test_status_check_retry_delay_never_exceeds_max() {
273 for attempt in 0..20 {
274 let evm_delay = status_check_retry_delay_secs(Some(NetworkType::Evm), attempt);
275 assert!(
276 evm_delay <= STATUS_EVM_BACKOFF.max_ms.div_ceil(1000) as i32,
277 "EVM attempt {attempt} delay {evm_delay}s exceeds max"
278 );
279
280 let stellar_delay = status_check_retry_delay_secs(Some(NetworkType::Stellar), attempt);
281 assert!(
282 stellar_delay <= STATUS_STELLAR_BACKOFF.max_ms.div_ceil(1000) as i32,
283 "Stellar attempt {attempt} delay {stellar_delay}s exceeds max"
284 );
285 }
286 }
287}