Skip to main content

nautilus_polymarket/common/
retry.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Retry timing utilities for the Polymarket adapter.
17
18use std::time::Duration;
19
20use rand::RngExt;
21
22/// Computes the auto-load retry sleep duration for `attempt`.
23///
24/// Matches the Python adapter's `auto_load_retry_delay` formula: takes
25/// `base_secs * 2^attempt`, caps at `max_secs`, then adds positive jitter of up
26/// to 25% of the capped value. Jitter decorrelates concurrent retries after a
27/// venue lifecycle race so they do not all rehit Gamma at the same instant.
28#[must_use]
29pub fn auto_load_retry_delay(attempt: u32, base_secs: f64, max_secs: f64) -> Duration {
30    auto_load_retry_delay_with_jitter(
31        attempt,
32        base_secs,
33        max_secs,
34        rand::rng().random_range(0.0..1.0),
35    )
36}
37
38#[must_use]
39pub(crate) fn auto_load_retry_delay_with_jitter(
40    attempt: u32,
41    base_secs: f64,
42    max_secs: f64,
43    jitter_unit: f64,
44) -> Duration {
45    let base = base_secs.max(0.0);
46    let max = max_secs.max(base);
47    let raw = base * 2f64.powi(attempt.min(30) as i32);
48    let capped = raw.min(max);
49    let total = capped + capped * 0.25 * jitter_unit.clamp(0.0, 1.0);
50    Duration::from_secs_f64(total.max(0.0))
51}
52
53#[cfg(test)]
54mod tests {
55    use rstest::rstest;
56
57    use super::*;
58
59    #[rstest]
60    fn test_attempt_zero_no_jitter_returns_base() {
61        let d = auto_load_retry_delay_with_jitter(0, 5.0, 15.0, 0.0);
62        assert_eq!(d, Duration::from_secs_f64(5.0));
63    }
64
65    #[rstest]
66    fn test_attempt_zero_full_jitter_returns_base_plus_25_percent() {
67        let d = auto_load_retry_delay_with_jitter(0, 5.0, 15.0, 1.0);
68        assert!((d.as_secs_f64() - 6.25).abs() < 1e-9);
69    }
70
71    #[rstest]
72    fn test_attempt_exponentiates_until_max_cap() {
73        let d = auto_load_retry_delay_with_jitter(2, 5.0, 15.0, 0.0);
74        assert_eq!(d, Duration::from_secs_f64(15.0));
75    }
76
77    #[rstest]
78    fn test_capped_delay_still_takes_jitter() {
79        let d = auto_load_retry_delay_with_jitter(2, 5.0, 15.0, 1.0);
80        assert!((d.as_secs_f64() - 18.75).abs() < 1e-9);
81    }
82
83    #[rstest]
84    fn test_high_attempt_count_does_not_overflow() {
85        let d = auto_load_retry_delay_with_jitter(50, 5.0, 15.0, 0.0);
86        assert_eq!(d, Duration::from_secs_f64(15.0));
87    }
88
89    #[rstest]
90    fn test_zero_base_returns_zero() {
91        let d = auto_load_retry_delay_with_jitter(3, 0.0, 15.0, 0.5);
92        assert_eq!(d, Duration::ZERO);
93    }
94}