use super::*;
#[tokio::test]
async fn retry_budget_exhaustion_on_connection_error_with_middleware() {
let dead_port = {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
port
};
let error_count = Arc::new(AtomicU32::new(0));
let error_count_clone = error_count.clone();
let retry_count = Arc::new(AtomicU32::new(0));
let retry_count_clone = retry_count.clone();
struct TrackingMiddleware {
error_count: Arc<AtomicU32>,
retry_count: Arc<AtomicU32>,
}
impl aioduct::Middleware for TrackingMiddleware {
fn on_error(&self, _error: &aioduct::Error, _uri: &http::Uri, _method: &http::Method) {
self.error_count.fetch_add(1, Ordering::SeqCst);
}
fn on_retry(
&self,
_error: &aioduct::Error,
_uri: &http::Uri,
_method: &http::Method,
_attempt: u32,
) {
self.retry_count.fetch_add(1, Ordering::SeqCst);
}
}
let budget = aioduct::RetryBudget::new(0, 1);
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.middleware(TrackingMiddleware {
error_count: error_count_clone,
retry_count: retry_count_clone,
})
.build()
.unwrap();
let result = client
.get(&format!("http://127.0.0.1:{dead_port}/"))
.unwrap()
.retry(
aioduct::RetryConfig::default()
.max_retries(5)
.initial_backoff(Duration::from_millis(1))
.budget(budget),
)
.timeout(Duration::from_secs(2))
.send()
.await;
assert!(result.is_err(), "should fail when budget is exhausted");
assert!(
error_count.load(Ordering::SeqCst) >= 1,
"on_error should be called when budget exhausted, got {}",
error_count.load(Ordering::SeqCst)
);
}
#[tokio::test]
async fn retry_fully_exhausted_with_middleware_fires_error() {
let dead_port = {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
port
};
let error_count = Arc::new(AtomicU32::new(0));
let error_count_clone = error_count.clone();
let retry_count = Arc::new(AtomicU32::new(0));
let retry_count_clone = retry_count.clone();
struct ErrorTrackMw {
error_count: Arc<AtomicU32>,
retry_count: Arc<AtomicU32>,
}
impl aioduct::Middleware for ErrorTrackMw {
fn on_error(&self, _error: &aioduct::Error, _uri: &http::Uri, _method: &http::Method) {
self.error_count.fetch_add(1, Ordering::SeqCst);
}
fn on_retry(
&self,
_error: &aioduct::Error,
_uri: &http::Uri,
_method: &http::Method,
_attempt: u32,
) {
self.retry_count.fetch_add(1, Ordering::SeqCst);
}
}
let budget = aioduct::RetryBudget::new(100, 1);
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.middleware(ErrorTrackMw {
error_count: error_count_clone,
retry_count: retry_count_clone,
})
.build()
.unwrap();
let result = client
.get(&format!("http://127.0.0.1:{dead_port}/"))
.unwrap()
.retry(
aioduct::RetryConfig::default()
.max_retries(2)
.initial_backoff(Duration::from_millis(1))
.budget(budget),
)
.timeout(Duration::from_secs(5))
.send()
.await;
assert!(result.is_err(), "all retries should be exhausted");
assert_eq!(
retry_count.load(Ordering::SeqCst),
2,
"on_retry should be called for each retry attempt"
);
assert_eq!(
error_count.load(Ordering::SeqCst),
1,
"on_error should be called once when retries exhausted"
);
}
#[tokio::test]
async fn non_retryable_error_with_middleware() {
let error_count = Arc::new(AtomicU32::new(0));
let error_count_clone = error_count.clone();
struct NonRetryErrorMw {
error_count: Arc<AtomicU32>,
}
impl aioduct::Middleware for NonRetryErrorMw {
fn on_error(&self, _error: &aioduct::Error, _uri: &http::Uri, _method: &http::Method) {
self.error_count.fetch_add(1, Ordering::SeqCst);
}
}
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.middleware(NonRetryErrorMw {
error_count: error_count_clone,
})
.https_only(true)
.build()
.unwrap();
let result = client
.get("http://example.com/")
.unwrap()
.retry(
aioduct::RetryConfig::default()
.max_retries(3)
.initial_backoff(Duration::from_millis(1)),
)
.send()
.await;
assert!(result.is_err());
assert_eq!(
error_count.load(Ordering::SeqCst),
1,
"on_error should fire for non-retryable errors"
);
}