use std::future::Future;
use std::time::Duration;
use crate::neo_error::unified::NeoError;
pub(crate) const DEFAULT_RETRY_DELAY: Duration = Duration::from_millis(250);
pub(crate) async fn retry_network<T, E, F, Fut>(
context: &str,
attempts: u32,
delay: Duration,
mut operation: F,
) -> Result<T, NeoError>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
E: std::fmt::Display,
{
let attempts = attempts.max(1);
let mut last_err: Option<E> = None;
for attempt in 1..=attempts {
match operation().await {
Ok(value) => return Ok(value),
Err(err) => {
if attempt < attempts {
tracing::warn!(
attempt = attempt,
max_attempts = attempts,
context = %context,
error = %err,
"sdk operation failed; retrying"
);
tokio::time::sleep(delay).await;
}
last_err = Some(err);
},
}
}
let err = last_err.expect("retry loop ran at least once");
Err(NeoError::network(context, err))
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[tokio::test]
async fn returns_first_success_without_extra_attempts() {
let calls = Arc::new(AtomicUsize::new(0));
let calls_clone = calls.clone();
let result: Result<u32, NeoError> =
retry_network("unit", 3, Duration::from_millis(1), move || {
let calls = calls_clone.clone();
async move {
calls.fetch_add(1, Ordering::SeqCst);
Ok::<u32, &'static str>(7)
}
})
.await;
assert_eq!(result.unwrap(), 7);
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn maps_final_failure_to_network_error_with_context() {
let calls = Arc::new(AtomicUsize::new(0));
let calls_clone = calls.clone();
let result: Result<(), NeoError> =
retry_network("fetch block", 2, Duration::from_millis(1), move || {
let calls = calls_clone.clone();
async move {
calls.fetch_add(1, Ordering::SeqCst);
Err::<(), _>("upstream 503")
}
})
.await;
let err = result.unwrap_err();
assert_eq!(calls.load(Ordering::SeqCst), 2);
assert!(err.is_retryable());
assert!(err.message().contains("fetch block"));
assert!(err.message().contains("upstream 503"));
}
}