1use {
2 retry::delay::Exponential,
3 std::{error::Error, future::Future, time::Duration},
4 tracing::{error, warn},
5};
6
7pub fn is_transient(err: &etcd_client::Error) -> bool {
17 match err {
18 etcd_client::Error::TransportError(_) => true,
19 etcd_client::Error::GRpcStatus(status) => {
20 match status.source() {
21 Some(e) => {
22 match e.downcast_ref::<tonic::transport::Error>() {
23 Some(_) => {
24 true
26 }
27 None => status_code_is_transient(status),
28 }
29 }
30 None => status_code_is_transient(status),
31 }
32 }
33 etcd_client::Error::InvalidArgs(_) => false,
34 etcd_client::Error::InvalidUri(_) => false,
35 etcd_client::Error::IoError(_) => true,
36 etcd_client::Error::WatchError(_) => true,
37 etcd_client::Error::Utf8Error(_) => false,
38 etcd_client::Error::LeaseKeepAliveError(_) => true,
39 etcd_client::Error::ElectError(_) => false,
40 etcd_client::Error::InvalidMetadataValue(_) => false,
41 etcd_client::Error::EndpointError(_) => false,
42 etcd_client::Error::EndpointsNotManaged => false,
43 etcd_client::Error::Internal(_) => true,
44 }
45}
46
47pub fn status_code_is_transient(status: &tonic::Status) -> bool {
48 match status.code() {
49 tonic::Code::Ok => true,
50 tonic::Code::Cancelled => true,
51 tonic::Code::Unknown => true,
52 tonic::Code::InvalidArgument => false,
53 tonic::Code::DeadlineExceeded => true,
54 tonic::Code::NotFound => false,
55 tonic::Code::AlreadyExists => false,
56 tonic::Code::PermissionDenied => false,
57 tonic::Code::ResourceExhausted => true,
58 tonic::Code::FailedPrecondition => false,
59 tonic::Code::Aborted => false,
60 tonic::Code::OutOfRange => false,
61 tonic::Code::Unimplemented => false,
62 tonic::Code::Internal => true,
63 tonic::Code::Unavailable => true,
64 tonic::Code::DataLoss => true,
65 tonic::Code::Unauthenticated => false,
66 }
67}
68
69pub async fn retry_etcd_txn(
73 etcd: etcd_client::Client,
74 txn: etcd_client::Txn,
75) -> Result<etcd_client::TxnResponse, etcd_client::Error> {
76 retry_etcd(etcd, (txn,), move |etcd, (txn,)| async move {
77 etcd.kv_client().txn(txn).await
78 })
79 .await
80}
81
82pub async fn retry_etcd_get(
86 etcd: etcd_client::Client,
87 key: String,
88 opts: Option<etcd_client::GetOptions>,
89) -> Result<etcd_client::GetResponse, etcd_client::Error> {
90 retry_etcd(etcd, (key, opts), move |etcd, (key, opts)| async move {
91 etcd.kv_client().get(key, opts).await
92 })
93 .await
94}
95
96pub async fn retry_etcd<A, T, F, Fut>(
127 etcd: etcd_client::Client,
128 reusable_args: A,
129 f: F,
130) -> Result<T, etcd_client::Error>
131where
132 A: Clone + Send + 'static,
133 Fut: Future<Output = Result<T, etcd_client::Error>> + Send + 'static,
134 F: FnMut(etcd_client::Client, A) -> Fut,
135 T: Send + 'static,
136{
137 let retry_strategy = Exponential::from_millis_with_factor(10, 10.0).take(3);
138 retry_etcd_with_strategy(etcd, reusable_args, retry_strategy, f).await
139}
140
141pub async fn retry_etcd_with_strategy<A, T, F, Fut>(
145 etcd: etcd_client::Client,
146 reusable_args: A,
147 retry_strategy: impl IntoIterator<Item = Duration>,
148 mut f: F,
149) -> Result<T, etcd_client::Error>
150where
151 A: Clone + Send + 'static,
152 Fut: Future<Output = Result<T, etcd_client::Error>> + Send + 'static,
153 F: FnMut(etcd_client::Client, A) -> Fut,
154 T: Send + 'static,
155{
156 let mut retry_strategy = retry_strategy.into_iter();
157 loop {
158 match f(etcd.clone(), reusable_args.clone()).await {
159 Ok(o) => return Ok(o),
160 Err(e) => {
161 if is_transient(&e) {
162 warn!("failed due to transient state {:?}", e);
163 match retry_strategy.next() {
164 Some(duration) => {
165 tokio::time::sleep(duration).await;
166 }
167 None => return Err(e),
168 }
169 } else {
170 error!("failed due to non-transient state: {:?}", e);
171 return Err(e);
172 }
173 }
174 }
175 }
176}
177
178pub(crate) async fn retry_etcd_legacy<T, F, Fut>(
179 retry_strategy: impl IntoIterator<Item = Duration>,
180 mut f: F,
181) -> Result<T, etcd_client::Error>
182where
183 Fut: Future<Output = Result<T, etcd_client::Error>> + Send + 'static,
184 F: FnMut() -> Fut,
185 T: Send + 'static,
186{
187 let mut retry_strategy = retry_strategy.into_iter();
188 loop {
189 match f().await {
190 Ok(o) => return Ok(o),
191 Err(e) => {
192 if is_transient(&e) {
193 warn!("failed due to transient state {:?}", e);
194 match retry_strategy.next() {
195 Some(duration) => {
196 tokio::time::sleep(duration).await;
197 }
198 None => return Err(e),
199 }
200 } else {
201 error!("failed due to non-transient state: {:?}", e);
202 return Err(e);
203 }
204 }
205 }
206 }
207}