1pub use dash_async::{block_on, AsyncError};
2
3use crate::error::Error;
4use dash_context_provider::ContextProviderError;
5use rs_dapi_client::{
6 transport::sleep, update_address_ban_status, AddressList, CanRetry, DapiClientError,
7 ExecutionResult, RequestSettings,
8};
9use std::future::Future;
10use std::time::Duration;
11
12impl From<AsyncError> for crate::Error {
13 fn from(error: AsyncError) -> Self {
14 Self::ContextProviderError(error.into())
15 }
16}
17
18pub async fn retry<Fut, FutureFactoryFn, R>(
80 address_list: &AddressList,
81 settings: RequestSettings,
82 future_factory_fn: FutureFactoryFn,
83) -> ExecutionResult<R, Error>
84where
85 Fut: Future<Output = ExecutionResult<R, Error>>,
86 FutureFactoryFn: FnMut(RequestSettings) -> Fut,
87 R: Send,
88{
89 retry_with_additional_error(
90 address_list,
91 settings,
92 future_factory_fn,
93 attempt_has_quorum_source_failure,
94 )
95 .await
96}
97
98fn attempt_has_quorum_source_failure(error: &Error) -> bool {
106 matches!(
107 error,
108 Error::ContextProviderError(ContextProviderError::QuorumSourceUnavailable(_))
109 )
110}
111
112pub(crate) async fn retry_with_additional_error<Fut, FutureFactoryFn, R, AdditionalError>(
118 address_list: &AddressList,
119 settings: RequestSettings,
120 mut future_factory_fn: FutureFactoryFn,
121 additional_error: AdditionalError,
122) -> ExecutionResult<R, Error>
123where
124 Fut: Future<Output = ExecutionResult<R, Error>>,
125 FutureFactoryFn: FnMut(RequestSettings) -> Fut,
126 R: Send,
127 AdditionalError: Fn(&Error) -> bool,
128{
129 let max_retries = settings.retries.unwrap_or_default();
130 let mut total_retries: usize = 0;
131 let mut current_settings = settings;
132
133 let mut last_meaningful_error: Option<rs_dapi_client::ExecutionError<Error>> = None;
136
137 let mut every_attempt_spared = true;
142
143 loop {
144 let result = future_factory_fn(current_settings).await;
145
146 let mut applied_settings = current_settings.finalize();
152 if result
153 .as_ref()
154 .is_err_and(|error| error.inner.spares_responder())
155 {
156 applied_settings.ban_failed_address = false;
157 }
158 update_address_ban_status(address_list, &result, &applied_settings);
159
160 match result {
161 Ok(response) => return Ok(response),
162 Err(error) => {
163 if error.is_no_available_addresses() {
165 if let Some(prev_error) = last_meaningful_error.take() {
166 tracing::error!(
167 retry = total_retries,
168 max_retries,
169 error = ?prev_error,
170 "no addresses available to retry"
171 );
172 let inner = if every_attempt_spared
176 && matches!(
177 error.inner,
178 Error::DapiClientError(DapiClientError::NoAvailableAddresses)
179 ) {
180 prev_error.inner.into_exhausted_error()
181 } else {
182 Error::NoAvailableAddressesToRetry(Box::new(prev_error.inner))
183 };
184 return Err(rs_dapi_client::ExecutionError {
185 inner,
186 retries: total_retries,
187 address: prev_error.address,
188 });
189 }
190 return Err(error);
192 }
193
194 every_attempt_spared &= error.inner.spares_responder();
195
196 let requests_sent = error.retries + 1;
198 total_retries += requests_sent;
199
200 let retry_additional_error = additional_error(&error.inner);
201 if !error.can_retry() && !retry_additional_error {
202 let mut final_error = error;
204 final_error.retries = total_retries;
205 return Err(final_error);
206 }
207
208 if total_retries > max_retries {
209 tracing::error!(
211 retry = total_retries,
212 max_retries,
213 error = ?error,
214 "no more retries left, giving up"
215 );
216 let mut final_error = error;
217 final_error.retries = total_retries;
218 if every_attempt_spared {
219 final_error.inner = final_error.inner.into_exhausted_error();
220 }
221 return Err(final_error);
222 }
223
224 if retry_additional_error {
225 let excluded = current_settings.finalize().ban_failed_address
234 && error.address.as_ref().is_some_and(|address| {
235 address_list
236 .get_live_addresses()
237 .iter()
238 .any(|candidate| candidate != address)
239 && address_list.ban_for(
240 address,
241 Duration::from_secs(2),
242 Some(error.to_string()),
243 )
244 });
245 if !excluded || address_list.get_live_addresses().is_empty() {
246 tracing::debug!(node = ?error.address, error = %error.inner, "failover stopped: no safely excluded alternative");
247 let mut final_error = error;
248 final_error.retries = total_retries;
249 return Err(final_error);
250 }
251 }
252
253 tracing::warn!(
255 retry = total_retries,
256 max_retries,
257 error = ?error,
258 "retrying request"
259 );
260
261 current_settings.retries = Some(max_retries.saturating_sub(total_retries));
263
264 let delay = Duration::from_millis(10);
267 tracing::warn!(duration = ?delay, error = ?error, "request failed, retrying");
268
269 last_meaningful_error = Some(error);
271
272 sleep(delay).await;
273 }
274 }
275 }
276}
277
278#[cfg(test)]
279mod test {
280 use super::*;
281 use rs_dapi_client::ExecutionError;
282 use std::sync::{
283 atomic::{AtomicUsize, Ordering},
284 Arc,
285 };
286
287 use crate::error::StaleNodeError;
288 use rs_dapi_client::DapiClientError;
289
290 async fn retry_test_function(
291 settings: RequestSettings,
292 counter: Arc<AtomicUsize>,
293 ) -> ExecutionResult<(), Error> {
294 let retries = counter.load(Ordering::Relaxed);
296 let retries = if settings.retries.unwrap_or_default() < retries {
297 settings.retries.unwrap_or_default()
298 } else {
299 retries
300 };
301
302 counter.fetch_add(1 + retries, Ordering::Relaxed);
304
305 Err(ExecutionError {
306 inner: Error::StaleNode(StaleNodeError::Height {
307 expected_height: 100,
308 received_height: 50,
309 tolerance_blocks: 1,
310 }),
311 retries,
312 address: Some("http://localhost".parse().expect("valid address")),
313 })
314 }
315
316 #[test_case::test_matrix([1,2,3,5,7,8,10,11,23,49, usize::MAX])]
317 #[tokio::test]
318 async fn test_retry(expected_requests: usize) {
319 for _ in 0..1 {
320 let counter = Arc::new(AtomicUsize::new(0));
321
322 let address_list = AddressList::default();
323
324 let mut global_settings = RequestSettings::default();
326 global_settings.retries = Some(expected_requests - 1);
327
328 let closure = |s| {
329 let counter = counter.clone();
330 retry_test_function(s, counter)
331 };
332
333 retry(&address_list, global_settings, closure)
334 .await
335 .expect_err("should fail");
336
337 assert_eq!(
338 counter.load(Ordering::Relaxed),
339 expected_requests,
340 "test failed for expected {} requests",
341 expected_requests
342 );
343 }
344 }
345
346 #[tokio::test]
349 async fn test_retry_returns_last_meaningful_error_on_no_addresses() {
350 let call_count = Arc::new(AtomicUsize::new(0));
351 let address_list = AddressList::default();
352
353 let mut settings = RequestSettings::default();
354 settings.retries = Some(5);
355
356 let call_count_clone = call_count.clone();
357 let closure = move |_settings: RequestSettings| {
358 let count = call_count_clone.fetch_add(1, Ordering::Relaxed);
359 async move {
360 if count == 0 {
361 Err(ExecutionError {
362 inner: Error::StaleNode(StaleNodeError::Height {
363 expected_height: 100,
364 received_height: 50,
365 tolerance_blocks: 1,
366 }),
367 retries: 0,
368 address: Some("http://localhost:1".parse().unwrap()),
369 })
370 } else {
371 Err(ExecutionError {
372 inner: Error::DapiClientError(DapiClientError::NoAvailableAddresses),
373 retries: 0,
374 address: None,
375 })
376 }
377 }
378 };
379
380 let result: ExecutionResult<(), Error> = retry(&address_list, settings, closure).await;
381
382 let error = result.expect_err("should fail");
383 match &error.inner {
384 Error::NoAvailableAddressesToRetry(inner) => {
385 assert!(
386 matches!(**inner, Error::StaleNode(_)),
387 "inner error should be StaleNode, got: {:?}",
388 inner
389 );
390 }
391 _ => panic!(
392 "expected NoAvailableAddresses error, got: {:?}",
393 error.inner
394 ),
395 }
396 assert_eq!(
397 call_count.load(Ordering::Relaxed),
398 2,
399 "should have called twice"
400 );
401 }
402
403 #[tokio::test]
408 async fn should_report_a_spared_answer_as_a_configuration_error_when_no_address_is_left() {
409 let mut address_list = AddressList::default();
410 let node: rs_dapi_client::Address = "http://localhost:1".parse().unwrap();
411 address_list.add(node.clone());
412 let settings = RequestSettings {
413 retries: Some(5),
414 ban_failed_address: Some(true),
415 ..Default::default()
416 };
417 let call_count = AtomicUsize::new(0);
418 let closure = |_settings: RequestSettings| {
419 let count = call_count.fetch_add(1, Ordering::Relaxed);
420 let node = node.clone();
421 async move {
422 Err::<rs_dapi_client::ExecutionResponse<()>, _>(if count == 0 {
423 ExecutionError {
424 inner: Error::StaleNode(StaleNodeError::ProtocolVersion {
425 expected_protocol_version: 14,
426 received_protocol_version: 13,
427 }),
428 retries: 0,
429 address: Some(node),
430 }
431 } else {
432 ExecutionError {
433 inner: Error::DapiClientError(DapiClientError::NoAvailableAddresses),
434 retries: 0,
435 address: None,
436 }
437 })
438 }
439 };
440
441 let error = retry(&address_list, settings, closure)
442 .await
443 .expect_err("no node answers at the SDK's version");
444
445 assert!(matches!(error.inner, Error::Config(_)), "{:?}", error.inner);
446 assert_eq!(call_count.load(Ordering::Relaxed), 2);
447 assert!(!address_list.is_banned(&node), "the node is not banned");
448 }
449
450 #[tokio::test]
453 async fn should_report_a_spared_answer_as_is_when_other_attempts_failed_otherwise() {
454 let address_list = AddressList::default();
455 let settings = RequestSettings {
456 retries: Some(1),
457 ..Default::default()
458 };
459 let call_count = AtomicUsize::new(0);
460 let closure = |_settings: RequestSettings| {
461 let count = call_count.fetch_add(1, Ordering::Relaxed);
462 async move {
463 Err::<rs_dapi_client::ExecutionResponse<()>, _>(ExecutionError {
464 inner: if count == 0 {
465 Error::StaleNode(StaleNodeError::Height {
466 expected_height: 100,
467 received_height: 50,
468 tolerance_blocks: 1,
469 })
470 } else {
471 Error::StaleNode(StaleNodeError::ProtocolVersion {
472 expected_protocol_version: 14,
473 received_protocol_version: 13,
474 })
475 },
476 retries: 0,
477 address: Some(format!("http://localhost:{}", 1 + count).parse().unwrap()),
478 })
479 }
480 };
481
482 let error = retry(&address_list, settings, closure)
483 .await
484 .expect_err("no attempt succeeds");
485
486 assert!(
487 matches!(
488 error.inner,
489 Error::StaleNode(StaleNodeError::ProtocolVersion { .. })
490 ),
491 "{:?}",
492 error.inner
493 );
494 assert_eq!(call_count.load(Ordering::Relaxed), 2);
495 }
496
497 fn protocol_version_answer() -> Error {
498 Error::StaleNode(StaleNodeError::ProtocolVersion {
499 expected_protocol_version: 14,
500 received_protocol_version: 13,
501 })
502 }
503
504 async fn retry_failing_with(errors: Vec<Error>) -> Error {
506 let address_list = AddressList::default();
507 let settings = RequestSettings {
508 retries: Some(5),
509 ..Default::default()
510 };
511 let errors = std::sync::Mutex::new(errors.into_iter().enumerate());
512 let closure = |_settings: RequestSettings| {
513 let (index, inner) = errors
514 .lock()
515 .expect("errors lock")
516 .next()
517 .expect("an error for every attempt");
518 async move {
519 let address = (!inner.is_no_available_addresses()).then(|| {
520 format!("http://localhost:{}", 1 + index)
521 .parse()
522 .expect("address")
523 });
524 Err::<rs_dapi_client::ExecutionResponse<()>, _>(ExecutionError {
525 inner,
526 retries: 0,
527 address,
528 })
529 }
530 };
531
532 retry(&address_list, settings, closure)
533 .await
534 .expect_err("no attempt succeeds")
535 .inner
536 }
537
538 fn is_wrapped_protocol_version_answer(error: &Error) -> bool {
539 matches!(
540 error,
541 Error::NoAvailableAddressesToRetry(inner)
542 if matches!(**inner, Error::StaleNode(StaleNodeError::ProtocolVersion { .. }))
543 )
544 }
545
546 #[tokio::test]
550 async fn should_keep_the_no_addresses_wrapper_when_an_earlier_attempt_failed_otherwise() {
551 let error = retry_failing_with(vec![
552 Error::StaleNode(StaleNodeError::Height {
553 expected_height: 100,
554 received_height: 50,
555 tolerance_blocks: 1,
556 }),
557 protocol_version_answer(),
558 Error::DapiClientError(DapiClientError::NoAvailableAddresses),
559 ])
560 .await;
561
562 assert!(is_wrapped_protocol_version_answer(&error), "{error:?}");
563 }
564
565 #[tokio::test]
569 async fn should_keep_the_no_addresses_wrapper_when_the_last_attempt_failed_on_its_nodes() {
570 let error = retry_failing_with(vec![
571 protocol_version_answer(),
572 Error::DapiClientError(DapiClientError::NoAvailableAddressesToRetry(Box::new(
573 rs_dapi_client::transport::TransportError::Grpc(
574 dapi_grpc::tonic::Status::unavailable("node unreachable"),
575 ),
576 ))),
577 ])
578 .await;
579
580 assert!(is_wrapped_protocol_version_answer(&error), "{error:?}");
581 }
582
583 fn quorum_source_unavailable(address: &rs_dapi_client::Address) -> ExecutionError<Error> {
584 ExecutionError {
585 inner: Error::ContextProviderError(
586 dash_context_provider::ContextProviderError::QuorumSourceUnavailable(
587 "quorum service unreachable".to_string(),
588 ),
589 ),
590 retries: 0,
591 address: Some(address.clone()),
592 }
593 }
594
595 fn two_nodes() -> (
596 AddressList,
597 rs_dapi_client::Address,
598 rs_dapi_client::Address,
599 ) {
600 let first: rs_dapi_client::Address = "http://127.0.0.1:1".parse().expect("address");
601 let second: rs_dapi_client::Address = "http://127.0.0.1:2".parse().expect("address");
602 let mut list = AddressList::new();
603 list.add(first.clone());
604 list.add(second.clone());
605 (list, first, second)
606 }
607
608 #[tokio::test]
613 async fn should_fail_over_when_the_quorum_source_gave_no_answer() {
614 let (list, first, second) = two_nodes();
615 let calls = Arc::new(AtomicUsize::new(0));
616 let counted = Arc::clone(&calls);
617 let served = second.clone();
618 let failing = first.clone();
619 let closure = move |_settings: RequestSettings| {
620 let call = counted.fetch_add(1, Ordering::SeqCst);
621 let served = served.clone();
622 let failing = failing.clone();
623 async move {
624 if call == 0 {
625 Err(quorum_source_unavailable(&failing))
626 } else {
627 Ok(rs_dapi_client::ExecutionResponse {
628 inner: (),
629 retries: 0,
630 address: served,
631 })
632 }
633 }
634 };
635
636 let settings = RequestSettings {
637 retries: Some(3),
638 ..RequestSettings::default()
639 };
640
641 let response = retry(&list, settings, closure)
642 .await
643 .expect("another node answers");
644
645 assert_eq!(response.address, second);
646 assert_eq!(calls.load(Ordering::SeqCst), 2);
647 assert!(
648 !list.get_live_addresses().contains(&first),
649 "the node that answered is stepped over for a moment"
650 );
651 let excluded_until = list
652 .ban_info()
653 .into_iter()
654 .find(|info| info.uri == first.uri().to_string())
655 .and_then(|info| info.banned_until)
656 .expect("the node is excluded for a while");
657 assert!(
658 excluded_until <= chrono::Utc::now() + chrono::Duration::seconds(2),
659 "a short flat exclusion, not the health ladder"
660 );
661 }
662
663 #[test_case::test_case(false, None; "single node")]
667 #[test_case::test_case(true, Some(false); "banning off")]
668 #[tokio::test]
669 async fn should_not_fail_over_a_quorum_source_failure_without_an_excludable_node(
670 two_nodes_listed: bool,
671 ban_failed_address: Option<bool>,
672 ) {
673 let (list, first, _) = if two_nodes_listed {
674 two_nodes()
675 } else {
676 let first: rs_dapi_client::Address = "http://127.0.0.1:1".parse().expect("address");
677 let mut list = AddressList::new();
678 list.add(first.clone());
679 (list, first.clone(), first)
680 };
681 let calls = Arc::new(AtomicUsize::new(0));
682 let counted = Arc::clone(&calls);
683 let failing = first.clone();
684 let closure = move |_settings: RequestSettings| {
685 counted.fetch_add(1, Ordering::SeqCst);
686 let failing = failing.clone();
687 async move {
688 Err::<rs_dapi_client::ExecutionResponse<()>, _>(quorum_source_unavailable(&failing))
689 }
690 };
691 let settings = RequestSettings {
692 retries: Some(3),
693 ban_failed_address,
694 ..RequestSettings::default()
695 };
696
697 let error = retry(&list, settings, closure)
698 .await
699 .expect_err("no node can be stepped over");
700
701 assert!(matches!(
702 error.inner,
703 Error::ContextProviderError(
704 dash_context_provider::ContextProviderError::QuorumSourceUnavailable(_)
705 )
706 ));
707 assert_eq!(calls.load(Ordering::SeqCst), 1);
708 assert!(
709 list.get_live_addresses().contains(&first),
710 "no node is excluded"
711 );
712 }
713
714 #[tokio::test]
717 async fn test_retry_returns_no_addresses_if_no_previous_error() {
718 let address_list = AddressList::default();
719
720 let mut settings = RequestSettings::default();
721 settings.retries = Some(5);
722
723 let closure = move |_settings: RequestSettings| async move {
724 Err(ExecutionError {
725 inner: Error::DapiClientError(DapiClientError::NoAvailableAddresses),
726 retries: 0,
727 address: None,
728 })
729 };
730
731 let result: ExecutionResult<(), Error> = retry(&address_list, settings, closure).await;
732
733 let error = result.expect_err("should fail");
734 assert!(
735 matches!(
736 error.inner,
737 Error::DapiClientError(DapiClientError::NoAvailableAddresses)
738 ),
739 "should return 'no available addresses' when there's no previous meaningful error, got: {:?}",
740 error.inner
741 );
742 }
743}