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}