openzeppelin_relayer/queues/
retry_config.rs

1use crate::models::NetworkType;
2
3use super::QueueType;
4
5/// Exponential backoff configuration values in milliseconds.
6#[derive(Debug, Clone, Copy)]
7pub struct RetryBackoffConfig {
8    /// Initial retry delay in milliseconds before exponential growth.
9    pub initial_ms: u64,
10    /// Maximum retry delay in milliseconds after capping.
11    pub max_ms: u64,
12    /// Jitter factor applied by worker retry policies using this config.
13    pub jitter: f64,
14}
15
16/// Backoff profile for transaction-request retries.
17pub const TX_REQUEST_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
18    initial_ms: 500,
19    max_ms: 5000,
20    jitter: 0.99,
21};
22/// Backoff profile for transaction-submission retries.
23pub const TX_SUBMISSION_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
24    initial_ms: 500,
25    max_ms: 2000,
26    jitter: 0.99,
27};
28/// Backoff profile for generic status-check retries (Solana/default).
29pub const STATUS_GENERIC_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
30    initial_ms: 5000,
31    max_ms: 8000,
32    jitter: 0.99,
33};
34/// Backoff profile for EVM status-check retries.
35pub const STATUS_EVM_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
36    initial_ms: 8000,
37    max_ms: 12000,
38    jitter: 0.99,
39};
40/// Backoff profile for Stellar status-check retries.
41pub const STATUS_STELLAR_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
42    initial_ms: 2000,
43    max_ms: 3000,
44    jitter: 0.99,
45};
46/// Backoff profile for notification delivery retries.
47pub const NOTIFICATION_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
48    initial_ms: 2000,
49    max_ms: 8000,
50    jitter: 0.99,
51};
52/// Backoff profile for token-swap request retries.
53pub const TOKEN_SWAP_REQUEST_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
54    initial_ms: 5000,
55    max_ms: 20000,
56    jitter: 0.99,
57};
58/// Backoff profile for transaction-cleanup retries.
59pub const TX_CLEANUP_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
60    initial_ms: 5000,
61    max_ms: 20000,
62    jitter: 0.99,
63};
64/// Backoff profile for system-cleanup retries.
65pub const SYSTEM_CLEANUP_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
66    initial_ms: 5000,
67    max_ms: 20000,
68    jitter: 0.99,
69};
70/// Backoff profile for relayer-health-check retries.
71pub const RELAYER_HEALTH_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
72    initial_ms: 2000,
73    max_ms: 10000,
74    jitter: 0.99,
75};
76/// Backoff profile for token-swap cron retries.
77pub const TOKEN_SWAP_CRON_BACKOFF: RetryBackoffConfig = RetryBackoffConfig {
78    initial_ms: 2000,
79    max_ms: 5000,
80    jitter: 0.99,
81};
82
83/// Returns status-check backoff config for a network type.
84///
85/// `network_type` selects network-specific status timing:
86/// EVM -> `STATUS_EVM_BACKOFF`, Stellar -> `STATUS_STELLAR_BACKOFF`,
87/// Solana/`None` -> `STATUS_GENERIC_BACKOFF`.
88pub 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
96/// Computes retry delay in seconds from any backoff config + attempt.
97///
98/// Uses capped exponential backoff: `initial_ms * 2^attempt`, capped at `max_ms`.
99/// The exponent is clamped at `16` to avoid overflow, and the result is rounded
100/// up to whole seconds via `div_ceil(1000)`.
101pub 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
107/// Computes status-check retry delay in seconds using capped exponential backoff.
108///
109/// Delegates to [`retry_delay_secs`] with the network-specific backoff profile.
110pub 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
114/// Returns the backoff config for a given queue type.
115///
116/// Status-check queues return [`STATUS_GENERIC_BACKOFF`] here; for network-specific
117/// status timing use [`status_backoff_config`] instead.
118pub 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        // TX_REQUEST_BACKOFF: initial_ms=500, max_ms=5000
195        assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 0), 1); // 500ms -> 1s
196        assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 1), 1); // 1000ms -> 1s
197        assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 2), 2); // 2000ms -> 2s
198        assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 3), 4); // 4000ms -> 4s
199        assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 4), 5); // capped at 5000ms -> 5s
200        assert_eq!(retry_delay_secs(TX_REQUEST_BACKOFF, 10), 5); // stays capped
201    }
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        // Verify status_check_retry_delay_secs matches retry_delay_secs with same config
227        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}