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::GRpcStatus(status) => match status.code() {
19 tonic::Code::Ok => false,
20 tonic::Code::Cancelled => false,
21 tonic::Code::Unknown => {
22 match status.source() {
23 Some(e) => {
24 match e.downcast_ref::<tonic::transport::Error>() {
25 Some(_) => {
26 true
28 }
29 None => false,
30 }
31 }
32 None => true,
33 }
34 }
35 tonic::Code::InvalidArgument => false,
36 tonic::Code::DeadlineExceeded => true,
37 tonic::Code::NotFound => false,
38 tonic::Code::AlreadyExists => false,
39 tonic::Code::PermissionDenied => false,
40 tonic::Code::ResourceExhausted => true,
41 tonic::Code::FailedPrecondition => false,
42 tonic::Code::Aborted => false,
43 tonic::Code::OutOfRange => false,
44 tonic::Code::Unimplemented => false,
45 tonic::Code::Internal => true,
46 tonic::Code::Unavailable => true,
47 tonic::Code::DataLoss => true,
48 tonic::Code::Unauthenticated => false,
49 },
50 _ => false,
51 }
52}
53
54pub async fn retry_etcd_txn(
58 etcd: etcd_client::Client,
59 txn: etcd_client::Txn,
60) -> Result<etcd_client::TxnResponse, etcd_client::Error> {
61 retry_etcd(etcd, (txn,), move |etcd, (txn,)| async move {
62 etcd.kv_client().txn(txn).await
63 })
64 .await
65}
66
67pub async fn retry_etcd_get(
71 etcd: etcd_client::Client,
72 key: String,
73 opts: Option<etcd_client::GetOptions>,
74) -> Result<etcd_client::GetResponse, etcd_client::Error> {
75 retry_etcd(etcd, (key, opts), move |etcd, (key, opts)| async move {
76 etcd.kv_client().get(key, opts).await
77 })
78 .await
79}
80
81pub async fn retry_etcd<A, T, F, Fut>(
112 etcd: etcd_client::Client,
113 reusable_args: A,
114 f: F,
115) -> Result<T, etcd_client::Error>
116where
117 A: Clone + Send + 'static,
118 Fut: Future<Output = Result<T, etcd_client::Error>> + Send + 'static,
119 F: FnMut(etcd_client::Client, A) -> Fut,
120 T: Send + 'static,
121{
122 let retry_strategy = Exponential::from_millis_with_factor(10, 10.0).take(3);
123 retry_etcd_with_strategy(etcd, reusable_args, retry_strategy, f).await
124}
125
126pub async fn retry_etcd_with_strategy<A, T, F, Fut>(
130 etcd: etcd_client::Client,
131 reusable_args: A,
132 retry_strategy: impl IntoIterator<Item = Duration>,
133 mut f: F,
134) -> Result<T, etcd_client::Error>
135where
136 A: Clone + Send + 'static,
137 Fut: Future<Output = Result<T, etcd_client::Error>> + Send + 'static,
138 F: FnMut(etcd_client::Client, A) -> Fut,
139 T: Send + 'static,
140{
141 let mut retry_strategy = retry_strategy.into_iter();
142 loop {
143 match f(etcd.clone(), reusable_args.clone()).await {
144 Ok(o) => return Ok(o),
145 Err(e) => {
146 if is_transient(&e) {
147 warn!("failed due to transient state {:?}", e);
148 match retry_strategy.next() {
149 Some(duration) => {
150 tokio::time::sleep(duration).await;
151 }
152 None => return Err(e),
153 }
154 } else {
155 error!("failed due to non-transient state: {:?}", e);
156 return Err(e);
157 }
158 }
159 }
160 }
161}
162
163pub(crate) async fn retry_etcd_legacy<T, F, Fut>(
164 retry_strategy: impl IntoIterator<Item = Duration>,
165 mut f: F,
166) -> Result<T, etcd_client::Error>
167where
168 Fut: Future<Output = Result<T, etcd_client::Error>> + Send + 'static,
169 F: FnMut() -> Fut,
170 T: Send + 'static,
171{
172 let mut retry_strategy = retry_strategy.into_iter();
173 loop {
174 match f().await {
175 Ok(o) => return Ok(o),
176 Err(e) => {
177 if is_transient(&e) {
178 warn!("failed due to transient state {:?}", e);
179 match retry_strategy.next() {
180 Some(duration) => {
181 tokio::time::sleep(duration).await;
182 }
183 None => return Err(e),
184 }
185 } else {
186 error!("failed due to non-transient state: {:?}", e);
187 return Err(e);
188 }
189 }
190 }
191 }
192}