Skip to main content

opcua_client/session/
retry.rs

1use std::time::Duration;
2
3use futures::FutureExt;
4use opcua_types::StatusCode;
5
6use crate::retry::ExponentialBackoff;
7
8use super::{session_debug, Session, UARequest};
9
10/// Trait for generic retry policies, used with [`Session::send_with_retry`].
11/// For simple use cases you can use [`DefaultRetryPolicy`].
12pub trait RequestRetryPolicy {
13    /// Return the time until the next retry, or [`None`] if no more retries should be attempted.
14    fn get_next_delay(&mut self, status: StatusCode) -> Option<Duration>;
15}
16
17impl RequestRetryPolicy for Box<dyn RequestRetryPolicy + Send> {
18    fn get_next_delay(&mut self, status: StatusCode) -> Option<Duration> {
19        (**self).get_next_delay(status)
20    }
21}
22
23/// A simple default retry policy. This will retry using the given [`ExponentialBackoff`] if
24/// the error matches one of the following status codes:
25///
26/// - StatusCode::BadUnexpectedError
27/// - StatusCode::BadInternalError
28/// - StatusCode::BadOutOfMemory
29/// - StatusCode::BadResourceUnavailable
30/// - StatusCode::BadCommunicationError
31/// - StatusCode::BadTimeout
32/// - StatusCode::BadShutdown
33/// - StatusCode::BadServerNotConnected
34/// - StatusCode::BadServerHalted
35/// - StatusCode::BadNonceInvalid
36/// - StatusCode::BadSessionClosed
37/// - StatusCode::BadSessionIdInvalid
38/// - StatusCode::BadSessionNotActivated
39/// - StatusCode::BadNoCommunication
40/// - StatusCode::BadTooManySessions
41/// - StatusCode::BadTcpServerTooBusy
42/// - StatusCode::BadTcpSecureChannelUnknown
43/// - StatusCode::BadTcpNotEnoughResources
44/// - StatusCode::BadTcpInternalError
45/// - StatusCode::BadSecureChannelClosed
46/// - StatusCode::BadSecureChannelIdInvalid
47/// - StatusCode::BadNotConnected
48/// - StatusCode::BadDeviceFailure
49/// - StatusCode::BadSensorFailure
50/// - StatusCode::BadDisconnect
51/// - StatusCode::BadConnectionClosed
52/// - StatusCode::BadEndOfStream
53/// - StatusCode::BadInvalidState
54/// - StatusCode::BadMaxConnectionsReached
55/// - StatusCode::BadConnectionRejected
56///
57/// or if it's in the configured `extra_status_codes`.
58#[derive(Clone)]
59pub struct DefaultRetryPolicy<'a> {
60    backoff: ExponentialBackoff,
61    extra_status_codes: &'a [StatusCode],
62}
63
64impl<'a> DefaultRetryPolicy<'a> {
65    /// Create a new default retry policy with the given backoff generator.
66    pub fn new(backoff: ExponentialBackoff) -> Self {
67        Self {
68            backoff,
69            extra_status_codes: &[],
70        }
71    }
72
73    /// Create a retry policy with extra status codes to retry.
74    pub fn new_with_extras(
75        backoff: ExponentialBackoff,
76        extra_status_codes: &'a [StatusCode],
77    ) -> Self {
78        Self {
79            backoff,
80            extra_status_codes,
81        }
82    }
83}
84
85impl RequestRetryPolicy for DefaultRetryPolicy<'_> {
86    fn get_next_delay(&mut self, status: StatusCode) -> Option<Duration> {
87        // These status codes should generally be safe to retry, by default.
88        // If users disagree they can simply implement `RequestRetryPolicy` themselves.
89
90        let should_retry = matches!(
91            status,
92            StatusCode::BadUnexpectedError
93                | StatusCode::BadInternalError
94                | StatusCode::BadOutOfMemory
95                | StatusCode::BadResourceUnavailable
96                | StatusCode::BadCommunicationError
97                | StatusCode::BadTimeout
98                | StatusCode::BadShutdown
99                | StatusCode::BadServerNotConnected
100                | StatusCode::BadServerHalted
101                | StatusCode::BadNonceInvalid
102                | StatusCode::BadSessionClosed
103                | StatusCode::BadSessionIdInvalid
104                | StatusCode::BadSessionNotActivated
105                | StatusCode::BadNoCommunication
106                | StatusCode::BadTooManySessions
107                | StatusCode::BadTcpServerTooBusy
108                | StatusCode::BadTcpSecureChannelUnknown
109                | StatusCode::BadTcpNotEnoughResources
110                | StatusCode::BadTcpInternalError
111                | StatusCode::BadSecureChannelClosed
112                | StatusCode::BadSecureChannelIdInvalid
113                | StatusCode::BadNotConnected
114                | StatusCode::BadDeviceFailure
115                | StatusCode::BadSensorFailure
116                | StatusCode::BadDisconnect
117                | StatusCode::BadConnectionClosed
118                | StatusCode::BadEndOfStream
119                | StatusCode::BadInvalidState
120                | StatusCode::BadMaxConnectionsReached
121                | StatusCode::BadConnectionRejected
122        ) || self.extra_status_codes.contains(&status);
123
124        if should_retry {
125            self.backoff.next()
126        } else {
127            None
128        }
129    }
130}
131
132impl Session {
133    /// Send a UARequest, retrying if the request fails.
134    /// Note that this will always clone the request at least once.
135    pub async fn send_with_retry<T: UARequest + Clone>(
136        &self,
137        request: T,
138        mut policy: impl RequestRetryPolicy,
139    ) -> Result<T::Out, StatusCode> {
140        loop {
141            let next_request = request.clone();
142            // Removing `boxed` here causes any futures calling this to be non-send,
143            // due to a compiler bug. Look into removing this in the future.
144            // TODO: Check if tests compile without this in future rustc versions, especially
145            // if https://github.com/rust-lang/rust/issues/100013 is closed.
146            match next_request.send(&self.channel).boxed().await {
147                Ok(r) => break Ok(r),
148                Err(e) => {
149                    if let Some(delay) = policy.get_next_delay(e) {
150                        session_debug!(self, "Request failed, retrying after {delay:?}");
151                        tokio::time::sleep(delay).await;
152                    } else {
153                        break Err(e);
154                    }
155                }
156            }
157        }
158    }
159}