#![cfg(feature = "tokio")]
use std::time::Duration;
use aioduct::HttpEngineSend;
use aioduct::runtime::TokioRuntime;
use aioduct::runtime::tokio_rt::TcpConnector;
fn client() -> HttpEngineSend<TokioRuntime, TcpConnector> {
HttpEngineSend::builder()
.pool_idle_timeout(Duration::from_secs(60))
.timeout(Duration::from_secs(10))
.build()
.unwrap()
}
#[tokio::test]
async fn h1_sequential_100_requests_all_succeed() {
let (addr, counter) = aioduct_test_server::h1::h1_server().await;
let client = client();
let url = format!("http://{addr}/");
let mut failures = 0;
for _ in 0..100 {
match client.get(&url).unwrap().send().await {
Ok(resp) if resp.status() == 200 => {
let _ = resp.text().await;
}
_ => failures += 1,
}
}
assert_eq!(failures, 0, "all 100 sequential GETs should succeed");
assert!(
counter.connections() <= 3,
"sequential requests should reuse connections, got {} connections",
counter.connections()
);
}
#[tokio::test]
async fn h1_concurrent_50_requests() {
let (addr, _counter) = aioduct_test_server::h1::h1_server().await;
let client = client();
let url = format!("http://{addr}/");
let mut handles = Vec::new();
for _ in 0..50 {
let client = client.clone();
let url = url.clone();
handles.push(tokio::spawn(async move {
let resp = client.get(&url).unwrap().send().await.unwrap();
assert_eq!(resp.status(), 200);
let _ = resp.text().await.unwrap();
}));
}
let mut failures = 0;
for h in handles {
if h.await.is_err() {
failures += 1;
}
}
assert_eq!(failures, 0, "all 50 concurrent GETs should succeed");
}
#[tokio::test]
async fn h2_concurrent_100_requests() {
let (addr, counter) = aioduct_test_server::h2::h2_server().await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.pool_idle_timeout(Duration::from_secs(60))
.timeout(Duration::from_secs(10))
.build()
.unwrap();
let url = format!("http://{addr}/");
let mut handles = Vec::new();
for _ in 0..100 {
let client = client.clone();
let url = url.clone();
handles.push(tokio::spawn(async move {
let resp = client
.get(&url)
.unwrap()
.h2c_prior_knowledge()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let _ = resp.text().await.unwrap();
}));
}
let mut failures = 0;
for h in handles {
if h.await.is_err() {
failures += 1;
}
}
assert_eq!(failures, 0, "all 100 concurrent H2 GETs should succeed");
assert_eq!(counter.requests(), 100);
}
#[tokio::test]
async fn h1_pool_saturation_and_recovery() {
let (addr, _counter) = aioduct_test_server::h1::h1_server().await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.pool_max_idle_per_host(2)
.pool_idle_timeout(Duration::from_secs(60))
.timeout(Duration::from_secs(10))
.build()
.unwrap();
let url = format!("http://{addr}/");
let mut handles = Vec::new();
for _ in 0..10 {
let client = client.clone();
let url = url.clone();
handles.push(tokio::spawn(async move {
let resp = client.get(&url).unwrap().send().await.unwrap();
assert_eq!(resp.status(), 200);
let _ = resp.text().await.unwrap();
}));
}
let mut failures = 0;
for h in handles {
if h.await.is_err() {
failures += 1;
}
}
assert_eq!(
failures, 0,
"pool_max_idle(2) with 10 concurrent should still succeed"
);
}
#[tokio::test]
async fn mixed_methods_under_load() {
let (addr, _) = aioduct_test_server::h1::h1_echo_server().await;
let client = client();
let url = format!("http://{addr}/");
let mut handles = Vec::new();
for _ in 0..50 {
let client = client.clone();
let url = url.clone();
handles.push(tokio::spawn(async move {
let resp = client.get(&url).unwrap().send().await.unwrap();
assert_eq!(resp.status(), 200);
let _ = resp.text().await.unwrap();
}));
}
for _ in 0..25 {
let client = client.clone();
let url = url.clone();
handles.push(tokio::spawn(async move {
let resp = client
.post(&url)
.unwrap()
.body("data")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let _ = resp.text().await.unwrap();
}));
}
for _ in 0..25 {
let client = client.clone();
let url = url.clone();
handles.push(tokio::spawn(async move {
let resp = client.put(&url).unwrap().body("data").send().await.unwrap();
assert_eq!(resp.status(), 200);
let _ = resp.text().await.unwrap();
}));
}
let mut failures = 0;
for h in handles {
if h.await.is_err() {
failures += 1;
}
}
assert_eq!(
failures, 0,
"50 GETs + 25 POSTs + 25 PUTs concurrent should all succeed"
);
}
#[tokio::test]
async fn stale_retry_under_concurrent_load() {
let (addr, _counter) = aioduct_test_server::stale::h1_rst_every_n(2).await;
let client = client();
let url = format!("http://{addr}/");
let mut handles = Vec::new();
for _ in 0..50 {
let client = client.clone();
let url = url.clone();
handles.push(tokio::spawn(async move {
client.get(&url).unwrap().send().await
}));
}
let mut successes = 0;
let mut failures = 0;
for h in handles {
match h.await.unwrap() {
Ok(resp) if resp.status() == 200 => {
let _ = resp.text().await;
successes += 1;
}
_ => failures += 1,
}
}
assert!(
successes > 40,
"most requests should succeed with stale retry, got {successes} successes and {failures} failures"
);
}
#[tokio::test]
async fn h1_large_body_concurrent() {
let body_size = 64 * 1024;
let (addr, _) = aioduct_test_server::h1::h1_large_body_server(body_size).await;
let client = client();
let url = format!("http://{addr}/");
let mut handles = Vec::new();
for _ in 0..20 {
let client = client.clone();
let url = url.clone();
handles.push(tokio::spawn(async move {
let resp = client.get(&url).unwrap().send().await.unwrap();
assert_eq!(resp.status(), 200);
let body = resp.bytes().await.unwrap();
assert_eq!(body.len(), body_size);
}));
}
let mut failures = 0;
for h in handles {
if h.await.is_err() {
failures += 1;
}
}
assert_eq!(
failures, 0,
"20 concurrent 64KB body reads should all succeed"
);
}
#[tokio::test]
async fn rate_limiter_should_throttle_send_client() {
let (addr, counter) = aioduct_test_server::h1::h1_server().await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.rate_limiter(aioduct::RateLimiter::new(5, Duration::from_secs(1)))
.timeout(Duration::from_secs(10))
.build()
.unwrap();
let url = format!("http://{addr}/");
let start = std::time::Instant::now();
for _ in 0..10 {
let resp = client.get(&url).unwrap().send().await.unwrap();
assert_eq!(resp.status(), 200);
let _ = resp.text().await.unwrap();
}
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_millis(800),
"BUG: RateLimiter is not applied on HttpEngineSend (execute_send.rs has no rate_limiter code). \
10 requests at rate_per_second(5) should take >= 1s, but took {:?}. \
Requests sent: {}",
elapsed,
counter.requests()
);
}
#[tokio::test]
async fn rate_limiter_large_max_tokens_truncation() {
let limiter = aioduct::RateLimiter::new(4_294_967_297, Duration::from_secs(1));
let mut acquired = 0;
for _ in 0..100 {
if limiter.try_acquire() {
acquired += 1;
}
}
assert_eq!(
acquired, 100,
"should acquire 100 tokens from a pool of 4294967297"
);
let wait = limiter.wait_duration();
assert!(
wait < Duration::from_millis(100),
"BUG: throttle.rs:28 truncates max_tokens to u32. \
RateLimiter(4294967297, 1s) should have near-zero wait, got {:?}",
wait
);
}
#[tokio::test]
async fn h2_sequential_50_requests() {
let (addr, counter) = aioduct_test_server::h2::h2_server().await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.pool_idle_timeout(Duration::from_secs(60))
.timeout(Duration::from_secs(10))
.build()
.unwrap();
let url = format!("http://{addr}/");
for i in 0..50 {
let resp = client
.get(&url)
.unwrap()
.h2c_prior_knowledge()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200, "request {i} failed");
let _ = resp.text().await.unwrap();
}
assert_eq!(counter.requests(), 50);
assert_eq!(
counter.connections(),
1,
"50 sequential H2 requests should reuse 1 connection"
);
}
#[tokio::test]
async fn bandwidth_limiter_should_not_busy_loop() {
let (addr, _) = aioduct_test_server::h1::h1_large_body_server(64 * 1024).await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.max_download_speed(512) .build()
.unwrap();
let start = std::time::Instant::now();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let result = tokio::time::timeout(Duration::from_secs(5), resp.bytes()).await;
let elapsed = start.elapsed();
assert!(
result.is_err() || elapsed >= Duration::from_secs(2),
"bandwidth limited body should either timeout or take significant time, \
but completed in {:?}",
elapsed
);
}
#[tokio::test]
async fn bandwidth_wait_duration_overflow_for_large_deficit() {
let limiter = aioduct::BandwidthLimiter::new(1);
limiter.try_consume(1);
let wait = limiter.wait_duration(20_000_000_000);
assert!(
wait >= Duration::from_secs(1_000_000),
"BUG: bandwidth.rs:67 `deficit * 1_000_000_000` overflows u64 for large deficits. \
wait_duration(20GB) at 1 B/s should be ~20 billion seconds, but got {:?}",
wait
);
}
#[tokio::test]
async fn rate_limiter_uses_system_time_not_monotonic() {
let limiter = aioduct::RateLimiter::new(100, Duration::from_secs(1));
for _ in 0..100 {
limiter.try_acquire();
}
tokio::time::sleep(Duration::from_millis(50)).await;
let acquired = limiter.try_acquire();
assert!(
acquired,
"RateLimiter should refill tokens after sleeping. \
Note: throttle.rs:71-74 uses SystemTime which is not monotonic — \
clock jumps backwards will cause token starvation."
);
}