#![allow(clippy::expect_used, clippy::unwrap_used)]
mod support;
use ant_quic::{ConnectionCloseReason, ConnectionError};
use std::time::{Duration, Instant};
use support::{
connect_pair, make_node, normalize_local_addr, reset_lifecycle_events, spawn_accept_loop,
test_guard, wait_until,
};
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn peer_shutdown_cleans_up_remote_view_without_waiting_for_reaper() {
let _guard = test_guard().await;
reset_lifecycle_events();
let a = make_node(vec![]).await;
let b = make_node(vec![]).await;
let accept_a = spawn_accept_loop(a.clone());
let accept_b = spawn_accept_loop(b.clone());
let (_, b_id, _) = connect_pair(&a, &b).await;
let start = Instant::now();
b.shutdown().await;
let mut cleanup_elapsed = None;
wait_until(Duration::from_millis(2200), || {
let is_cleaned_up = a
.get_quic_connection(&b_id)
.expect("connection lookup during cleanup wait")
.is_none();
if is_cleaned_up {
cleanup_elapsed = Some(start.elapsed());
}
is_cleaned_up
})
.await;
let cleanup_elapsed = match cleanup_elapsed {
Some(elapsed) => elapsed,
None => start.elapsed(),
};
assert!(
cleanup_elapsed < Duration::from_secs(2),
"disconnect cleanup should be driven by reader exit, not the 30s stale reaper"
);
a.shutdown().await;
accept_a.abort();
accept_b.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn superseded_connection_surfaces_close_reason_quickly() {
let _guard = test_guard().await;
reset_lifecycle_events();
let a = make_node(vec![]).await;
let b = make_node(vec![]).await;
let a_addr = normalize_local_addr(a.local_addr().expect("a addr"));
let b_addr = normalize_local_addr(b.local_addr().expect("b addr"));
let a_id = a.peer_id();
let b_id = b.peer_id();
let accept_a = spawn_accept_loop(a.clone());
let accept_b = spawn_accept_loop(b.clone());
let a_task = {
let a = a.clone();
tokio::spawn(async move { a.connect_addr(b_addr).await })
};
let b_task = {
let b = b.clone();
tokio::spawn(async move { b.connect_addr(a_addr).await })
};
let _ = a_task.await.expect("a join");
let _ = b_task.await.expect("b join");
wait_until(Duration::from_secs(5), || {
a.get_quic_connection(&b_id).ok().flatten().is_some()
&& b.get_quic_connection(&a_id).ok().flatten().is_some()
})
.await;
let a_conn = a
.get_quic_connection(&b_id)
.expect("a lookup")
.expect("a live conn");
let b_conn = b
.get_quic_connection(&a_id)
.expect("b lookup")
.expect("b live conn");
for _ in 0..10 {
if a_conn
.close_reason()
.as_ref()
.map(ConnectionCloseReason::from_connection_error)
== Some(ConnectionCloseReason::Superseded)
|| b_conn
.close_reason()
.as_ref()
.map(ConnectionCloseReason::from_connection_error)
== Some(ConnectionCloseReason::Superseded)
{
break;
}
let a_task = {
let a = a.clone();
tokio::spawn(async move { a.connect_addr(b_addr).await })
};
let b_task = {
let b = b.clone();
tokio::spawn(async move { b.connect_addr(a_addr).await })
};
let _ = a_task.await.expect("a join");
let _ = b_task.await.expect("b join");
}
wait_until(Duration::from_secs(10), || {
a_conn
.close_reason()
.as_ref()
.map(ConnectionCloseReason::from_connection_error)
== Some(ConnectionCloseReason::Superseded)
|| b_conn
.close_reason()
.as_ref()
.map(ConnectionCloseReason::from_connection_error)
== Some(ConnectionCloseReason::Superseded)
})
.await;
let close_reason = if a_conn
.close_reason()
.as_ref()
.map(ConnectionCloseReason::from_connection_error)
== Some(ConnectionCloseReason::Superseded)
{
a_conn.close_reason().expect("a close reason")
} else {
b_conn.close_reason().expect("b close reason")
};
match close_reason {
ConnectionError::ApplicationClosed(frame) => {
assert_eq!(
ConnectionCloseReason::from_app_error_code(frame.error_code),
Some(ConnectionCloseReason::Superseded)
);
}
other => panic!("expected application close, got {other:?}"),
}
let _ = a.shutdown().await;
let _ = b.shutdown().await;
accept_a.abort();
accept_b.abort();
}