restate_sdk_shared_core/
retries.rs

1use crate::EntryRetryInfo;
2use std::cmp;
3use std::time::Duration;
4
5/// This struct represents the policy to execute retries.
6#[derive(Debug, Clone, Default)]
7pub enum RetryPolicy {
8    /// # Infinite
9    ///
10    /// Infinite retry strategy.
11    #[default]
12    Infinite,
13    /// # None
14    ///
15    /// No retry strategy, fail on first failure.
16    None,
17    /// # Fixed delay
18    ///
19    /// Retry with a fixed delay strategy.
20    FixedDelay {
21        /// # Interval
22        ///
23        /// Interval between retries.
24        interval: Duration,
25
26        /// # Max attempts
27        ///
28        /// Gives up retrying when either this number of attempts is reached,
29        /// or `max_duration` (if set) is reached first.
30        /// Infinite retries if this field and `max_duration` are unset.
31        max_attempts: Option<u32>,
32
33        /// # Max duration
34        ///
35        /// Gives up retrying when either the retry loop lasted for this given max duration,
36        /// or `max_attempts` (if set) is reached first.
37        /// Infinite retries if this field and `max_attempts` are unset.
38        max_duration: Option<Duration>,
39    },
40    /// # Exponential
41    ///
42    /// Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
43    Exponential {
44        /// # Initial Interval
45        ///
46        /// Initial interval for the first retry attempt.
47        initial_interval: Duration,
48
49        /// # Factor
50        ///
51        /// The factor to use to compute the next retry attempt. This value should be higher than 1.0
52        factor: f32,
53
54        /// # Max interval
55        ///
56        /// Maximum interval between retries.
57        max_interval: Option<Duration>,
58
59        /// # Max attempts
60        ///
61        /// Gives up retrying when either this number of attempts is reached,
62        /// or `max_duration` (if set) is reached first.
63        /// Infinite retries if this field and `max_duration` are unset.
64        max_attempts: Option<u32>,
65
66        /// # Max duration
67        ///
68        /// Gives up retrying when either the retry loop lasted for this given max duration,
69        /// or `max_attempts` (if set) is reached first.
70        /// Infinite retries if this field and `max_attempts` are unset.
71        max_duration: Option<Duration>,
72    },
73}
74
75#[derive(Debug, Clone, Eq, PartialEq)]
76pub(crate) enum NextRetry {
77    Retry(Option<Duration>),
78    DoNotRetry,
79}
80
81impl RetryPolicy {
82    pub fn fixed_delay(
83        interval: Duration,
84        max_attempts: Option<u32>,
85        max_duration: Option<Duration>,
86    ) -> Self {
87        Self::FixedDelay {
88            interval,
89            max_attempts,
90            max_duration,
91        }
92    }
93
94    pub fn exponential(
95        initial_interval: Duration,
96        factor: f32,
97        max_attempts: Option<u32>,
98        max_interval: Option<Duration>,
99        max_duration: Option<Duration>,
100    ) -> Self {
101        Self::Exponential {
102            initial_interval,
103            factor,
104            max_attempts,
105            max_interval,
106            max_duration,
107        }
108    }
109
110    pub(crate) fn next_retry(&self, retry_info: EntryRetryInfo) -> NextRetry {
111        match self {
112            RetryPolicy::Infinite => NextRetry::Retry(None),
113            RetryPolicy::None => NextRetry::DoNotRetry,
114            RetryPolicy::FixedDelay {
115                interval,
116                max_attempts,
117                max_duration,
118            } => {
119                if max_attempts.is_some_and(|max_attempts| max_attempts <= retry_info.retry_count)
120                    || max_duration
121                        .is_some_and(|max_duration| max_duration <= retry_info.retry_loop_duration)
122                {
123                    // Reached either max_attempts or max_duration bound
124                    return NextRetry::DoNotRetry;
125                }
126
127                // No bound reached, we need to retry
128                NextRetry::Retry(Some(*interval))
129            }
130            RetryPolicy::Exponential {
131                initial_interval,
132                factor,
133                max_interval,
134                max_attempts,
135                max_duration,
136            } => {
137                if max_attempts.is_some_and(|max_attempts| max_attempts <= retry_info.retry_count)
138                    || max_duration
139                        .is_some_and(|max_duration| max_duration <= retry_info.retry_loop_duration)
140                {
141                    // Reached either max_attempts or max_duration bound
142                    return NextRetry::DoNotRetry;
143                }
144
145                NextRetry::Retry(Some(cmp::min(
146                    max_interval.unwrap_or(Duration::MAX),
147                    initial_interval.mul_f32(factor.powi((retry_info.retry_count - 1) as i32)),
148                )))
149            }
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_exponential_policy() {
160        let policy = RetryPolicy::Exponential {
161            initial_interval: Duration::from_millis(100),
162            factor: 2.0,
163            max_interval: Some(Duration::from_millis(500)),
164            max_attempts: None,
165            max_duration: Some(Duration::from_secs(10)),
166        };
167
168        assert_eq!(
169            policy.next_retry(EntryRetryInfo {
170                retry_count: 2,
171                retry_loop_duration: Duration::from_secs(1)
172            }),
173            NextRetry::Retry(Some(Duration::from_millis(100).mul_f32(2.0)))
174        );
175        assert_eq!(
176            policy.next_retry(EntryRetryInfo {
177                retry_count: 3,
178                retry_loop_duration: Duration::from_secs(1)
179            }),
180            NextRetry::Retry(Some(Duration::from_millis(100).mul_f32(4.0)))
181        );
182        assert_eq!(
183            policy.next_retry(EntryRetryInfo {
184                retry_count: 4,
185                retry_loop_duration: Duration::from_secs(1)
186            }),
187            NextRetry::Retry(Some(Duration::from_millis(500)))
188        );
189        assert_eq!(
190            policy.next_retry(EntryRetryInfo {
191                retry_count: 4,
192                retry_loop_duration: Duration::from_secs(10)
193            }),
194            NextRetry::DoNotRetry
195        );
196    }
197}