Skip to main content

opcua_client/
retry.rs

1use std::time::Duration;
2
3#[derive(Debug, Clone)]
4/// A type implementing [`Iterator<Item = Option<Duration>`] with simple exponential backoff.
5pub struct ExponentialBackoff {
6    max_sleep: Duration,
7    max_retries: Option<u32>,
8    current_sleep: Duration,
9    retry_count: u32,
10}
11
12impl ExponentialBackoff {
13    /// Create a new exponential backoff generator.
14    pub fn new(max_sleep: Duration, max_retries: Option<u32>, initial_sleep: Duration) -> Self {
15        Self {
16            max_sleep,
17            max_retries,
18            current_sleep: initial_sleep,
19            retry_count: 0,
20        }
21    }
22}
23
24impl Iterator for ExponentialBackoff {
25    type Item = Duration;
26
27    fn next(&mut self) -> Option<Self::Item> {
28        if self.max_retries.is_some_and(|max| max <= self.retry_count) {
29            return None;
30        }
31
32        let next_sleep = self.current_sleep;
33        self.current_sleep = self.max_sleep.min(self.current_sleep * 2);
34        self.retry_count += 1;
35
36        Some(next_sleep)
37    }
38}
39
40#[derive(Debug, Clone)]
41/// Session retry policy.
42///
43/// Configure the maximum delay between reconnect attempts, the
44/// initial delay between reconnect attempts, and the maximum number of reconnect attempts.
45pub struct SessionRetryPolicy {
46    reconnect_max_sleep: Duration,
47    reconnect_retry_limit: Option<u32>,
48    reconnect_initial_sleep: Duration,
49}
50
51impl Default for SessionRetryPolicy {
52    fn default() -> Self {
53        Self {
54            reconnect_max_sleep: Duration::from_millis(Self::DEFAULT_MAX_SLEEP_MS),
55            reconnect_retry_limit: Some(Self::DEFAULT_RETRY_LIMIT),
56            reconnect_initial_sleep: Duration::from_millis(Self::DEFAULT_INITIAL_SLEEP_MS),
57        }
58    }
59}
60
61impl SessionRetryPolicy {
62    /// Default maximum number of retries.
63    pub const DEFAULT_RETRY_LIMIT: u32 = 10;
64    /// Default initial delay between requests in milliseconds.
65    pub const DEFAULT_INITIAL_SLEEP_MS: u64 = 500;
66    /// Default maximum delay between requests in milliseconds.
67    pub const DEFAULT_MAX_SLEEP_MS: u64 = 30000;
68
69    /// Create a new session retry policy.
70    pub fn new(max_sleep: Duration, retry_limit: Option<u32>, initial_sleep: Duration) -> Self {
71        Self {
72            reconnect_max_sleep: max_sleep,
73            reconnect_retry_limit: retry_limit,
74            reconnect_initial_sleep: initial_sleep,
75        }
76    }
77
78    pub(crate) fn new_backoff(&self) -> ExponentialBackoff {
79        ExponentialBackoff::new(
80            self.reconnect_max_sleep,
81            self.reconnect_retry_limit,
82            self.reconnect_initial_sleep,
83        )
84    }
85
86    /// Retry forever with the given max delay and initial delay.
87    pub fn infinity(max_sleep: Duration, initial_sleep: Duration) -> Self {
88        Self {
89            reconnect_initial_sleep: initial_sleep,
90            reconnect_retry_limit: None,
91            reconnect_max_sleep: max_sleep,
92        }
93    }
94
95    /// Never reconnect.
96    pub fn never() -> Self {
97        Self {
98            reconnect_retry_limit: Some(0),
99            ..Default::default()
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use std::time::Duration;
107
108    use super::SessionRetryPolicy;
109
110    #[test]
111    fn session_retry() {
112        let policy = SessionRetryPolicy::default();
113
114        let mut backoff = policy.new_backoff();
115
116        assert_eq!(Some(Duration::from_millis(500)), backoff.next());
117        assert_eq!(Some(Duration::from_millis(1000)), backoff.next());
118        assert_eq!(Some(Duration::from_millis(2000)), backoff.next());
119        assert_eq!(Some(Duration::from_millis(4000)), backoff.next());
120        assert_eq!(Some(Duration::from_millis(8000)), backoff.next());
121        assert_eq!(Some(Duration::from_millis(16000)), backoff.next());
122        assert_eq!(Some(Duration::from_millis(30000)), backoff.next());
123        assert_eq!(Some(Duration::from_millis(30000)), backoff.next());
124        assert_eq!(Some(Duration::from_millis(30000)), backoff.next());
125        assert_eq!(Some(Duration::from_millis(30000)), backoff.next());
126        assert_eq!(None, backoff.next());
127        assert_eq!(None, backoff.next());
128    }
129
130    #[test]
131    fn session_retry_infinity() {
132        let policy =
133            SessionRetryPolicy::infinity(Duration::from_millis(3000), Duration::from_millis(500));
134
135        let mut backoff = policy.new_backoff();
136
137        for _ in 0..100 {
138            assert!(backoff.next().is_some());
139        }
140
141        assert_eq!(Some(Duration::from_millis(3000)), backoff.next());
142    }
143
144    #[test]
145    fn session_retry_never() {
146        let policy = SessionRetryPolicy::never();
147        let mut backoff = policy.new_backoff();
148        assert!(backoff.next().is_none());
149    }
150}