1use std::collections::HashSet;
20use std::fmt::{Debug, Display};
21use std::future::Future;
22use std::sync::Arc;
23use std::time::Duration;
24
25use crate::errmapcomponent::ErrMapComponent;
26use crate::error::{Error, ErrorKind};
27use crate::memdx::error::ErrorKind::{Cancelled, Dispatch, Resource, Server};
28use crate::memdx::error::{CancellationErrorKind, ServerError, ServerErrorKind};
29use crate::retryfailfast::FailFastRetryStrategy;
30use crate::tracingcomponent::SPAN_ATTRIB_RETRIES;
31use crate::{analyticsx, error, httpx, mgmtx, queryx, searchx};
32use async_trait::async_trait;
33use tokio::time::sleep;
34use tracing::{debug, info};
35
36lazy_static! {
37 pub(crate) static ref DEFAULT_RETRY_STRATEGY: Arc<dyn RetryStrategy> =
38 Arc::new(FailFastRetryStrategy::default());
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47#[non_exhaustive]
48pub enum RetryReason {
49 KvNotMyVbucket,
51 KvInvalidVbucketMap,
53 KvTemporaryFailure,
55 KvCollectionOutdated,
57 KvErrorMapRetryIndicated,
59 KvLocked,
61 KvSyncWriteInProgress,
63 KvSyncWriteRecommitInProgress,
65 ServiceNotAvailable,
67 SocketClosedWhileInFlight,
69 SocketNotAvailable,
71 QueryPreparedStatementFailure,
73 QueryIndexNotFound,
75 QueryErrorRetryable,
77 SearchTooManyRequests,
79 HttpSendRequestFailed,
81 HttpConnectFailed,
83 NotReady,
85}
86
87impl RetryReason {
88 pub fn allows_non_idempotent_retry(&self) -> bool {
93 matches!(
94 self,
95 RetryReason::KvInvalidVbucketMap
96 | RetryReason::KvNotMyVbucket
97 | RetryReason::KvTemporaryFailure
98 | RetryReason::KvCollectionOutdated
99 | RetryReason::KvErrorMapRetryIndicated
100 | RetryReason::KvLocked
101 | RetryReason::ServiceNotAvailable
102 | RetryReason::SocketNotAvailable
103 | RetryReason::KvSyncWriteInProgress
104 | RetryReason::KvSyncWriteRecommitInProgress
105 | RetryReason::QueryPreparedStatementFailure
106 | RetryReason::QueryIndexNotFound
107 | RetryReason::QueryErrorRetryable
108 | RetryReason::SearchTooManyRequests
109 | RetryReason::HttpSendRequestFailed
110 | RetryReason::HttpConnectFailed
111 | RetryReason::NotReady
112 )
113 }
114
115 pub fn always_retry(&self) -> bool {
118 matches!(
119 self,
120 RetryReason::KvInvalidVbucketMap
121 | RetryReason::KvNotMyVbucket
122 | RetryReason::KvCollectionOutdated
123 )
124 }
125}
126
127impl Display for RetryReason {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 RetryReason::KvNotMyVbucket => write!(f, "KV_NOT_MY_VBUCKET"),
131 RetryReason::KvInvalidVbucketMap => write!(f, "KV_INVALID_VBUCKET_MAP"),
132 RetryReason::KvTemporaryFailure => write!(f, "KV_TEMPORARY_FAILURE"),
133 RetryReason::KvCollectionOutdated => write!(f, "KV_COLLECTION_OUTDATED"),
134 RetryReason::KvErrorMapRetryIndicated => write!(f, "KV_ERROR_MAP_RETRY_INDICATED"),
135 RetryReason::KvLocked => write!(f, "KV_LOCKED"),
136 RetryReason::ServiceNotAvailable => write!(f, "SERVICE_NOT_AVAILABLE"),
137 RetryReason::SocketClosedWhileInFlight => write!(f, "SOCKET_CLOSED_WHILE_IN_FLIGHT"),
138 RetryReason::SocketNotAvailable => write!(f, "SOCKET_NOT_AVAILABLE"),
139 RetryReason::KvSyncWriteInProgress => write!(f, "KV_SYNC_WRITE_IN_PROGRESS"),
140 RetryReason::KvSyncWriteRecommitInProgress => {
141 write!(f, "KV_SYNC_WRITE_RECOMMIT_IN_PROGRESS")
142 }
143 RetryReason::QueryPreparedStatementFailure => {
144 write!(f, "QUERY_PREPARED_STATEMENT_FAILURE")
145 }
146 RetryReason::QueryIndexNotFound => write!(f, "QUERY_INDEX_NOT_FOUND"),
147 RetryReason::QueryErrorRetryable => write!(f, "QUERY_ERROR_RETRYABLE"),
148 RetryReason::SearchTooManyRequests => write!(f, "SEARCH_TOO_MANY_REQUESTS"),
149 RetryReason::NotReady => write!(f, "NOT_READY"),
150 RetryReason::HttpSendRequestFailed => write!(f, "HTTP_SEND_REQUEST_FAILED"),
151 RetryReason::HttpConnectFailed => write!(f, "HTTP_CONNECT_FAILED"),
152 }
153 }
154}
155
156#[derive(Clone, Debug)]
160pub struct RetryAction {
161 pub duration: Duration,
163}
164
165impl RetryAction {
166 pub fn new(duration: Duration) -> Self {
168 Self { duration }
169 }
170}
171
172pub trait RetryStrategy: Debug + Send + Sync {
202 fn retry_after(&self, request: &RetryRequest, reason: &RetryReason) -> Option<RetryAction>;
209}
210
211#[derive(Clone, Debug)]
213pub struct RetryRequest {
214 pub(crate) operation: &'static str,
215 pub is_idempotent: bool,
217 pub retry_attempts: u32,
219 pub retry_reasons: HashSet<RetryReason>,
221 pub(crate) unique_id: Option<String>,
222}
223
224impl RetryRequest {
225 pub(crate) fn new(operation: &'static str, is_idempotent: bool) -> Self {
226 Self {
227 operation,
228 is_idempotent,
229 retry_attempts: 0,
230 retry_reasons: Default::default(),
231 unique_id: None,
232 }
233 }
234
235 pub(crate) fn add_retry_attempt(&mut self, reason: RetryReason) {
236 self.retry_attempts += 1;
237 tracing::Span::current().record(SPAN_ATTRIB_RETRIES, self.retry_attempts);
238 self.retry_reasons.insert(reason);
239 }
240
241 pub fn is_idempotent(&self) -> bool {
242 self.is_idempotent
243 }
244
245 pub fn retry_attempts(&self) -> u32 {
246 self.retry_attempts
247 }
248
249 pub fn retry_reasons(&self) -> &HashSet<RetryReason> {
250 &self.retry_reasons
251 }
252}
253
254impl Display for RetryRequest {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 write!(
257 f,
258 "{{ operation: {}, id: {}, is_idempotent: {}, retry_attempts: {}, retry_reasons: {} }}",
259 self.operation,
260 self.unique_id.as_ref().unwrap_or(&"".to_string()),
261 self.is_idempotent,
262 self.retry_attempts,
263 self.retry_reasons
264 .iter()
265 .map(|r| r.to_string())
266 .collect::<Vec<_>>()
267 .join(", ")
268 )
269 }
270}
271
272pub struct RetryManager {
273 err_map_component: Arc<ErrMapComponent>,
274}
275
276impl RetryManager {
277 pub fn new(err_map_component: Arc<ErrMapComponent>) -> Self {
278 Self { err_map_component }
279 }
280
281 pub async fn maybe_retry(
282 &self,
283 strategy: Arc<dyn RetryStrategy>,
284 request: &mut RetryRequest,
285 reason: RetryReason,
286 ) -> Option<Duration> {
287 if reason.always_retry() {
288 request.add_retry_attempt(reason);
289 let backoff = controlled_backoff(request.retry_attempts);
290
291 return Some(backoff);
292 }
293
294 let action = strategy.retry_after(request, &reason);
295
296 if let Some(a) = action {
297 request.add_retry_attempt(reason);
298
299 return Some(a.duration);
300 }
301
302 None
303 }
304}
305
306pub(crate) async fn orchestrate_retries<Fut, Resp>(
307 rs: Arc<RetryManager>,
308 strategy: Arc<dyn RetryStrategy>,
309 mut retry_info: RetryRequest,
310 operation: impl Fn() -> Fut + Send + Sync,
311) -> error::Result<Resp>
312where
313 Fut: Future<Output = error::Result<Resp>> + Send,
314 Resp: Send,
315{
316 loop {
317 let mut err = match operation().await {
318 Ok(r) => {
319 return Ok(r);
320 }
321 Err(e) => e,
322 };
323
324 if let Some(reason) = error_to_retry_reason(&rs, &mut retry_info, &err) {
325 if let Some(duration) = rs
326 .maybe_retry(strategy.clone(), &mut retry_info, reason)
327 .await
328 {
329 debug!(
330 "Retrying {} after {:?} due to {}",
331 &retry_info, duration, reason
332 );
333 sleep(duration).await;
334 continue;
335 }
336 }
337
338 if retry_info.retry_attempts > 0 {
339 err.set_retry_info(retry_info);
341 }
342
343 return Err(err);
344 }
345}
346
347pub(crate) fn error_to_retry_reason(
348 rs: &Arc<RetryManager>,
349 retry_info: &mut RetryRequest,
350 err: &Error,
351) -> Option<RetryReason> {
352 match err.kind() {
353 ErrorKind::Memdx(err) => {
354 retry_info.unique_id = err.has_opaque().map(|o| o.to_string());
355
356 match err.kind() {
357 Server(e) => return server_error_to_retry_reason(rs, e),
358 Resource(e) => return server_error_to_retry_reason(rs, e.cause()),
359 Cancelled(e) if e == &CancellationErrorKind::ClosedInFlight => {
360 return Some(RetryReason::SocketClosedWhileInFlight);
361 }
362 Dispatch { .. } => return Some(RetryReason::SocketNotAvailable),
363 _ => {}
364 }
365 }
366 ErrorKind::NoVbucketMap => {
367 return Some(RetryReason::KvInvalidVbucketMap);
368 }
369 ErrorKind::ServiceNotAvailable { .. } => {
370 return Some(RetryReason::ServiceNotAvailable);
371 }
372 ErrorKind::Query(e) => match e.kind() {
373 queryx::error::ErrorKind::Server(e) => match e.kind() {
374 queryx::error::ServerErrorKind::PreparedStatementFailure => {
375 return Some(RetryReason::QueryPreparedStatementFailure);
376 }
377 queryx::error::ServerErrorKind::IndexNotFound => {
378 return Some(RetryReason::QueryIndexNotFound);
379 }
380 _ => {
381 if e.retry() {
382 return Some(RetryReason::QueryErrorRetryable);
383 }
384 }
385 },
386 queryx::error::ErrorKind::Http { error, .. } => match error.kind() {
387 httpx::error::ErrorKind::SendRequest(_) => {
388 return Some(RetryReason::HttpSendRequestFailed);
389 }
390 httpx::error::ErrorKind::Connect { .. } => {
391 return Some(RetryReason::HttpConnectFailed);
392 }
393 _ => {}
394 },
395 _ => {}
396 },
397 ErrorKind::Search(e) => match e.kind() {
398 searchx::error::ErrorKind::Server(e) if e.status_code() == 429 => {
399 return Some(RetryReason::SearchTooManyRequests);
400 }
401 searchx::error::ErrorKind::Http { error, .. } => match error.kind() {
402 httpx::error::ErrorKind::SendRequest(_) => {
403 return Some(RetryReason::HttpSendRequestFailed);
404 }
405 httpx::error::ErrorKind::Connect { .. } => {
406 return Some(RetryReason::HttpConnectFailed);
407 }
408 _ => {}
409 },
410 _ => {}
411 },
412 ErrorKind::Analytics(e) => {
413 if let analyticsx::error::ErrorKind::Http { error, .. } = e.kind() {
414 match error.kind() {
415 httpx::error::ErrorKind::SendRequest(_) => {
416 return Some(RetryReason::HttpSendRequestFailed);
417 }
418 httpx::error::ErrorKind::Connect { .. } => {
419 return Some(RetryReason::HttpConnectFailed);
420 }
421 _ => {}
422 }
423 }
424 }
425 ErrorKind::Mgmt(e) => {
426 if let mgmtx::error::ErrorKind::Http(error) = e.kind() {
427 match error.kind() {
428 httpx::error::ErrorKind::SendRequest(_) => {
429 return Some(RetryReason::HttpSendRequestFailed);
430 }
431 httpx::error::ErrorKind::Connect { .. } => {
432 return Some(RetryReason::HttpConnectFailed);
433 }
434 _ => {}
435 }
436 }
437 }
438 _ => {}
439 }
440
441 None
442}
443
444fn server_error_to_retry_reason(rs: &Arc<RetryManager>, e: &ServerError) -> Option<RetryReason> {
445 match e.kind() {
446 ServerErrorKind::NotMyVbucket => {
447 return Some(RetryReason::KvNotMyVbucket);
448 }
449 ServerErrorKind::TmpFail => {
450 return Some(RetryReason::KvTemporaryFailure);
451 }
452 ServerErrorKind::UnknownCollectionID => {
453 return Some(RetryReason::KvCollectionOutdated);
454 }
455 ServerErrorKind::UnknownCollectionName => {
456 return Some(RetryReason::KvCollectionOutdated);
457 }
458 ServerErrorKind::UnknownScopeName => {
459 return Some(RetryReason::KvCollectionOutdated);
460 }
461 ServerErrorKind::Locked => {
462 return Some(RetryReason::KvLocked);
463 }
464 ServerErrorKind::SyncWriteInProgress => {
465 return Some(RetryReason::KvSyncWriteInProgress);
466 }
467 ServerErrorKind::SyncWriteRecommitInProgress => {
468 return Some(RetryReason::KvSyncWriteRecommitInProgress);
469 }
470 ServerErrorKind::UnknownStatus { status } if rs.err_map_component.should_retry(status) => {
471 return Some(RetryReason::KvErrorMapRetryIndicated);
472 }
473 _ => {}
474 }
475
476 None
477}
478
479pub(crate) fn controlled_backoff(retry_attempts: u32) -> Duration {
480 match retry_attempts {
481 0 => Duration::from_millis(1),
482 1 => Duration::from_millis(10),
483 2 => Duration::from_millis(50),
484 3 => Duration::from_millis(100),
485 4 => Duration::from_millis(500),
486 _ => Duration::from_millis(1000),
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use crate::queryx;
494 use http::StatusCode;
495
496 fn make_retry_manager() -> Arc<RetryManager> {
497 Arc::new(RetryManager::new(Arc::new(ErrMapComponent::default())))
498 }
499
500 fn make_query_server_error(kind: queryx::error::ServerErrorKind, retry: bool) -> Error {
501 let server_error = queryx::error::ServerError::new(
502 kind,
503 "localhost:8093",
504 StatusCode::INTERNAL_SERVER_ERROR,
505 12345,
506 retry,
507 "test error",
508 );
509 queryx::error::Error::new_server_error(server_error).into()
510 }
511
512 #[test]
513 fn test_query_error_retryable_when_retry_true() {
514 let rs = make_retry_manager();
515 let mut retry_info = RetryRequest::new("query", false);
516 let err = make_query_server_error(queryx::error::ServerErrorKind::Unknown, true);
517
518 let reason = error_to_retry_reason(&rs, &mut retry_info, &err);
519 assert_eq!(reason, Some(RetryReason::QueryErrorRetryable));
520 }
521
522 #[test]
523 fn test_query_error_not_retryable_when_retry_false() {
524 let rs = make_retry_manager();
525 let mut retry_info = RetryRequest::new("query", false);
526 let err = make_query_server_error(queryx::error::ServerErrorKind::Unknown, false);
527
528 let reason = error_to_retry_reason(&rs, &mut retry_info, &err);
529 assert_eq!(reason, None);
530 }
531
532 #[test]
533 fn test_query_prepared_statement_failure_ignores_retry_flag() {
534 let rs = make_retry_manager();
535 let mut retry_info = RetryRequest::new("query", false);
536 let err = make_query_server_error(
537 queryx::error::ServerErrorKind::PreparedStatementFailure,
538 false,
539 );
540
541 let reason = error_to_retry_reason(&rs, &mut retry_info, &err);
542 assert_eq!(reason, Some(RetryReason::QueryPreparedStatementFailure));
543 }
544
545 #[test]
546 fn test_query_index_not_found_ignores_retry_flag() {
547 let rs = make_retry_manager();
548 let mut retry_info = RetryRequest::new("query", false);
549 let err = make_query_server_error(queryx::error::ServerErrorKind::IndexNotFound, false);
550
551 let reason = error_to_retry_reason(&rs, &mut retry_info, &err);
552 assert_eq!(reason, Some(RetryReason::QueryIndexNotFound));
553 }
554
555 #[test]
556 fn test_query_error_retryable_allows_non_idempotent_retry() {
557 assert!(RetryReason::QueryErrorRetryable.allows_non_idempotent_retry());
558 }
559
560 #[test]
561 fn test_query_error_retryable_does_not_always_retry() {
562 assert!(!RetryReason::QueryErrorRetryable.always_retry());
563 }
564}