use std::{
collections::HashMap,
fmt,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use async_lock::{MutexGuardArc, Semaphore, SemaphoreGuardArc};
use http::{Request, Response, uri::Scheme};
#[cfg(feature = "http2")]
use hyper::client::conn::http2;
use hyper::{
body::Incoming,
client::conn::{TrySendError, http1},
};
#[cfg(http3)]
use tracing::debug;
use super::connect::{Protocol, Via};
#[cfg(http3)]
use crate::backend::{
alt_svc::{self, AltSvc},
h3::H3Connection,
};
pub const MAX_H1_PER_ORIGIN: usize = 6;
const IDLE_TIMEOUT: Duration = Duration::from_secs(90);
#[cfg(http3)]
const H3_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(http3)]
#[expect(clippy::duration_suboptimal_units)]
const H3_BACKOFF: Duration = Duration::from_secs(5 * 60);
#[cfg(http3)]
#[expect(clippy::duration_suboptimal_units)]
const HTTPS_RR_NEGATIVE_TTL: Duration = Duration::from_secs(5 * 60);
pub struct Pool {
origins: Mutex<HashMap<Origin, Arc<OriginEntry>>>,
idle_timeout: Duration,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Origin {
pub scheme: Scheme,
pub host: String,
pub port: u16,
}
pub struct OriginEntry {
h1_idle: Mutex<Vec<IdleH1>>,
h1_slots: Arc<Semaphore>,
#[cfg(feature = "http2")]
h2: Mutex<Option<http2::SendRequest<http_kit::Body>>>,
protocol: Mutex<Option<Protocol>>,
#[cfg(http3)]
h3: Mutex<Option<H3Connection>>,
#[cfg(http3)]
alt_svc: Mutex<Option<AltSvc>>,
#[cfg(http3)]
https_rr: Mutex<Option<HttpsRr>>,
#[cfg(http3)]
h3_discovery: Mutex<H3Discovery>,
#[cfg(http3)]
h3_broken_until: Mutex<Option<Instant>>,
#[cfg(http3)]
h3_connect_timeout: Mutex<Duration>,
#[cfg(http3)]
h3_backoff: Mutex<Duration>,
#[cfg(http3)]
port: u16,
dialing: Arc<async_lock::Mutex<()>>,
}
#[cfg(http3)]
#[derive(Clone, Copy, Debug)]
pub struct HttpsRr {
pub h3: bool,
pub port: Option<u16>,
pub expires: Instant,
}
#[cfg(http3)]
impl HttpsRr {
pub(crate) fn negative(now: Instant) -> Self {
Self {
h3: false,
port: None,
expires: now + HTTPS_RR_NEGATIVE_TTL,
}
}
}
#[cfg(http3)]
#[derive(Clone, Copy, Debug)]
enum H3Discovery {
NotStarted,
InFlight,
Done,
}
#[cfg(http3)]
pub struct DiscoveryGuard(Arc<OriginEntry>);
#[cfg(http3)]
impl DiscoveryGuard {
pub(crate) fn insert_https_rr(self, rr: HttpsRr) {
self.0.insert_https_rr(rr);
}
}
#[cfg(http3)]
impl Drop for DiscoveryGuard {
fn drop(&mut self) {
*self.0.h3_discovery.lock().expect("pool state poisoned") = H3Discovery::Done;
}
}
impl OriginEntry {
fn new(origin: &Origin) -> Self {
Self {
h1_idle: Mutex::new(Vec::new()),
h1_slots: Arc::new(Semaphore::new(MAX_H1_PER_ORIGIN)),
#[cfg(feature = "http2")]
h2: Mutex::new(None),
protocol: Mutex::new((origin.scheme == Scheme::HTTP).then_some(Protocol::Http1)),
#[cfg(http3)]
h3: Mutex::new(None),
#[cfg(http3)]
alt_svc: Mutex::new(None),
#[cfg(http3)]
https_rr: Mutex::new(None),
#[cfg(http3)]
h3_discovery: Mutex::new(H3Discovery::NotStarted),
#[cfg(http3)]
h3_broken_until: Mutex::new(None),
#[cfg(http3)]
h3_connect_timeout: Mutex::new(H3_CONNECT_TIMEOUT),
#[cfg(http3)]
h3_backoff: Mutex::new(H3_BACKOFF),
#[cfg(http3)]
port: origin.port,
dialing: Arc::new(async_lock::Mutex::new(())),
}
}
fn has_live_connections(&self, idle_timeout: Duration) -> bool {
{
let mut idle = self.h1_idle.lock().expect("pool state poisoned");
idle.retain(|idle| {
!idle.sender.is_closed() && idle.idle_since.elapsed() < idle_timeout
});
if !idle.is_empty() {
return true;
}
}
#[cfg(feature = "http2")]
{
let mut h2 = self.h2.lock().expect("pool state poisoned");
if h2.as_ref().is_some_and(http2::SendRequest::is_closed) {
*h2 = None;
}
if h2.is_some() {
return true;
}
}
#[cfg(http3)]
let live = self.has_live_h3();
#[cfg(not(http3))]
let live = false;
live
}
#[cfg(http3)]
fn has_live_h3(&self) -> bool {
let live = {
let mut h3 = self.h3.lock().expect("pool state poisoned");
if h3.as_ref().is_some_and(H3Connection::is_closed) {
*h3 = None;
}
h3.is_some()
};
live || self.has_live_h3_knowledge(Instant::now())
}
#[cfg(http3)]
fn has_live_h3_knowledge(&self, now: Instant) -> bool {
if self
.h3_broken_until
.lock()
.expect("pool state poisoned")
.is_some_and(|until| until > now)
{
return true;
}
{
let mut alt_svc = self.alt_svc.lock().expect("pool state poisoned");
if alt_svc.as_ref().is_some_and(|alt| alt.expires <= now) {
*alt_svc = None;
}
if alt_svc.is_some() {
return true;
}
}
let mut https_rr = self.https_rr.lock().expect("pool state poisoned");
if https_rr.as_ref().is_some_and(|rr| rr.expires <= now) {
*https_rr = None;
}
https_rr.is_some()
}
#[cfg(http3)]
fn h3_candidate(&self, now: Instant) -> Option<u16> {
if self
.h3_broken_until
.lock()
.expect("pool state poisoned")
.is_some_and(|until| until > now)
{
return None;
}
{
let mut alt_svc = self.alt_svc.lock().expect("pool state poisoned");
if alt_svc.as_ref().is_some_and(|alt| alt.expires <= now) {
*alt_svc = None;
}
if let Some(alt) = *alt_svc {
return Some(alt.port);
}
}
let mut https_rr = self.https_rr.lock().expect("pool state poisoned");
if https_rr.as_ref().is_some_and(|rr| rr.expires <= now) {
*https_rr = None;
}
https_rr.and_then(|rr| rr.h3.then(|| rr.port.unwrap_or(self.port)))
}
#[cfg(http3)]
pub(crate) fn insert_alt_svc(&self, origin_host: &str, value: &str, now: Instant) {
match alt_svc::parse(value) {
Ok(advertised) => {
*self.alt_svc.lock().expect("pool state poisoned") =
alt_svc::accept(&advertised, origin_host, now);
}
Err(error) => debug!(%value, %error, "ignoring malformed Alt-Svc header"),
}
}
#[cfg(http3)]
pub(crate) fn insert_https_rr(&self, rr: HttpsRr) {
*self.https_rr.lock().expect("pool state poisoned") = Some(rr);
}
#[cfg(http3)]
pub(crate) fn begin_h3_discovery(self: &Arc<Self>, now: Instant) -> Option<DiscoveryGuard> {
{
let mut discovery = self.h3_discovery.lock().expect("pool state poisoned");
let due = match *discovery {
H3Discovery::NotStarted => true,
H3Discovery::InFlight => false,
H3Discovery::Done => self
.https_rr
.lock()
.expect("pool state poisoned")
.is_none_or(|rr| rr.expires <= now),
};
if !due {
return None;
}
*discovery = H3Discovery::InFlight;
}
Some(DiscoveryGuard(self.clone()))
}
#[cfg(all(http3, test))]
pub(crate) fn https_rr(&self) -> Option<HttpsRr> {
*self.https_rr.lock().expect("pool state poisoned")
}
#[cfg(http3)]
pub(crate) fn mark_h3_broken(&self, now: Instant) {
let backoff = *self.h3_backoff.lock().expect("pool state poisoned");
*self.h3_broken_until.lock().expect("pool state poisoned") = Some(now + backoff);
}
#[cfg(all(http3, test))]
pub(crate) fn h3_broken_until(&self) -> Option<Instant> {
*self.h3_broken_until.lock().expect("pool state poisoned")
}
#[cfg(http3)]
pub(crate) fn h3_connect_timeout(&self) -> Duration {
*self.h3_connect_timeout.lock().expect("pool state poisoned")
}
#[cfg(all(http3, test))]
pub(crate) fn set_h3_timeouts(&self, connect_timeout: Duration, backoff: Duration) {
*self.h3_connect_timeout.lock().expect("pool state poisoned") = connect_timeout;
*self.h3_backoff.lock().expect("pool state poisoned") = backoff;
}
#[cfg(http3)]
pub(crate) fn insert_h3(&self, connection: H3Connection) {
let mut h3 = self.h3.lock().expect("pool state poisoned");
if h3.as_ref().is_none_or(H3Connection::is_closed) {
*h3 = Some(connection);
}
}
#[cfg(all(http3, test))]
pub(crate) fn h3_connection(&self) -> Option<H3Connection> {
self.h3.lock().expect("pool state poisoned").clone()
}
}
impl fmt::Debug for OriginEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OriginEntry").finish_non_exhaustive()
}
}
struct IdleH1 {
sender: http1::SendRequest<http_kit::Body>,
via: Via,
idle_since: Instant,
}
pub enum Reuse {
Pooled,
FreshDial,
}
#[cfg(http3)]
#[derive(Clone, Copy, Debug)]
pub enum H3 {
Allowed,
Blocked,
}
pub enum Checkout {
#[cfg(http3)]
H3(H3Connection),
#[cfg(feature = "http2")]
H2(http2::SendRequest<http_kit::Body>),
H1(H1Lease),
Dial {
permit: DialPermit,
#[cfg(http3)]
h3_port: Option<u16>,
},
}
#[cfg(http3)]
impl Checkout {
pub(crate) const fn h3_port(&self) -> Option<u16> {
match self {
Self::Dial { h3_port, .. } => *h3_port,
_ => None,
}
}
}
impl fmt::Debug for Checkout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(http3)]
Self::H3(_) => f.write_str("Checkout::H3"),
#[cfg(feature = "http2")]
Self::H2(_) => f.write_str("Checkout::H2"),
Self::H1(_) => f.write_str("Checkout::H1"),
Self::Dial { .. } => f.write_str("Checkout::Dial"),
}
}
}
pub struct H1Lease {
sender: http1::SendRequest<http_kit::Body>,
entry: Arc<OriginEntry>,
via: Via,
permit: SemaphoreGuardArc,
}
impl H1Lease {
pub const fn via(&self) -> &Via {
&self.via
}
#[allow(clippy::result_large_err)]
pub async fn send(
mut self,
request: Request<http_kit::Body>,
) -> Result<(Response<Incoming>, Self), TrySendError<Request<http_kit::Body>>> {
self.sender
.try_send_request(request)
.await
.map(|response| (response, self))
}
pub fn release(self) {
let Self {
sender,
entry,
via,
permit,
} = self;
if !sender.is_closed() {
entry
.h1_idle
.lock()
.expect("pool state poisoned")
.push(IdleH1 {
sender,
via,
idle_since: Instant::now(),
});
}
drop(permit);
}
}
impl fmt::Debug for H1Lease {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("H1Lease").finish_non_exhaustive()
}
}
pub struct DialPermit {
entry: Arc<OriginEntry>,
hold: DialHold,
}
#[cfg(http3)]
impl DialPermit {
pub(crate) fn entry(&self) -> Arc<OriginEntry> {
self.entry.clone()
}
}
enum DialHold {
Coalesced(MutexGuardArc<()>),
H1(SemaphoreGuardArc),
}
impl fmt::Debug for DialPermit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DialPermit").finish_non_exhaustive()
}
}
impl Pool {
#[allow(clippy::missing_const_for_fn)] pub fn new() -> Self {
Self {
origins: Mutex::new(HashMap::new()),
idle_timeout: IDLE_TIMEOUT,
}
}
pub async fn checkout(&self, origin: Origin, reuse: Reuse, #[cfg(http3)] h3: H3) -> Checkout {
let entry = self.entry(&origin);
let pooled = matches!(reuse, Reuse::Pooled);
loop {
#[cfg(http3)]
if matches!(h3, H3::Allowed) {
if pooled && let Some(connection) = live_h3(&entry) {
return Checkout::H3(connection);
}
if entry.h3_candidate(Instant::now()).is_some() {
let dialing = entry.dialing.lock_arc().await;
if pooled && let Some(connection) = live_h3(&entry) {
return Checkout::H3(connection);
}
match entry.h3_candidate(Instant::now()) {
Some(h3_port) => {
return Checkout::Dial {
permit: DialPermit {
entry,
hold: DialHold::Coalesced(dialing),
},
h3_port: Some(h3_port),
};
}
None => drop(dialing),
}
}
}
#[cfg(feature = "http2")]
if pooled && let Some(sender) = live_h2(&entry) {
return Checkout::H2(sender);
}
if matches!(
*entry.protocol.lock().expect("pool state poisoned"),
Some(Protocol::Http1)
) {
let permit = entry.h1_slots.acquire_arc().await;
if !pooled {
return Checkout::Dial {
permit: DialPermit {
entry,
hold: DialHold::H1(permit),
},
#[cfg(http3)]
h3_port: None,
};
}
match self.lease_idle(&entry, permit).await {
Ok(lease) => return Checkout::H1(lease),
Err(permit) => {
return Checkout::Dial {
permit: DialPermit {
entry,
hold: DialHold::H1(permit),
},
#[cfg(http3)]
h3_port: None,
};
}
}
}
let dialing = entry.dialing.lock_arc().await;
#[cfg(feature = "http2")]
if pooled && let Some(sender) = live_h2(&entry) {
return Checkout::H2(sender);
}
if matches!(
*entry.protocol.lock().expect("pool state poisoned"),
Some(Protocol::Http1)
) {
drop(dialing);
continue;
}
return Checkout::Dial {
permit: DialPermit {
entry,
hold: DialHold::Coalesced(dialing),
},
#[cfg(http3)]
h3_port: None,
};
}
}
pub async fn insert_h1(
permit: DialPermit,
sender: http1::SendRequest<http_kit::Body>,
via: Via,
) -> H1Lease {
let entry = permit.entry;
*entry.protocol.lock().expect("pool state poisoned") = Some(Protocol::Http1);
let slot = match permit.hold {
DialHold::H1(slot) => slot,
DialHold::Coalesced(dialing) => {
drop(dialing);
entry.h1_slots.acquire_arc().await
}
};
H1Lease {
sender,
entry,
via,
permit: slot,
}
}
#[cfg(http3)]
pub fn insert_h3(permit: DialPermit, connection: H3Connection) {
permit.entry.insert_h3(connection);
drop(permit);
}
#[cfg(feature = "http2")]
pub fn insert_h2(permit: DialPermit, sender: http2::SendRequest<http_kit::Body>) {
*permit.entry.protocol.lock().expect("pool state poisoned") = Some(Protocol::Http2);
*permit.entry.h2.lock().expect("pool state poisoned") = Some(sender);
permit
.entry
.h1_idle
.lock()
.expect("pool state poisoned")
.clear();
drop(permit);
}
pub(crate) fn entry(&self, origin: &Origin) -> Arc<OriginEntry> {
let mut origins = self.origins.lock().expect("pool state poisoned");
if let Some(entry) = origins.get(origin) {
return entry.clone();
}
let entry = Arc::new(OriginEntry::new(origin));
origins.insert(origin.clone(), entry.clone());
origins.retain(|_, entry| {
Arc::strong_count(entry) > 1 || entry.has_live_connections(self.idle_timeout)
});
entry
}
async fn lease_idle(
&self,
entry: &Arc<OriginEntry>,
permit: SemaphoreGuardArc,
) -> Result<H1Lease, SemaphoreGuardArc> {
loop {
let idle = {
let mut idle = entry.h1_idle.lock().expect("pool state poisoned");
idle.retain(|idle| {
!idle.sender.is_closed() && idle.idle_since.elapsed() < self.idle_timeout
});
idle.pop()
};
let Some(IdleH1 {
mut sender, via, ..
}) = idle
else {
return Err(permit);
};
if sender.ready().await.is_ok() {
return Ok(H1Lease {
sender,
entry: entry.clone(),
via,
permit,
});
}
}
}
}
#[cfg(feature = "http2")]
fn live_h2(entry: &OriginEntry) -> Option<http2::SendRequest<http_kit::Body>> {
let mut h2 = entry.h2.lock().expect("pool state poisoned");
match h2.as_ref() {
Some(sender) if sender.is_closed() => {
*h2 = None;
None
}
Some(sender) => Some(sender.clone()),
None => None,
}
}
#[cfg(http3)]
fn live_h3(entry: &OriginEntry) -> Option<H3Connection> {
let mut h3 = entry.h3.lock().expect("pool state poisoned");
match h3.as_ref() {
Some(connection) if connection.is_closed() => {
*h3 = None;
None
}
Some(connection) => Some(connection.clone()),
None => None,
}
}
impl fmt::Debug for Pool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pool").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use std::{
collections::HashMap,
io::Read as _,
net::TcpListener,
sync::Mutex,
thread,
time::{Duration, Instant},
};
use async_net::TcpStream;
use futures_executor::block_on;
use http::uri::Scheme;
use hyper::client::conn::http1;
use super::{Checkout, IdleH1, Origin, Pool, Reuse};
#[cfg(http3)]
use super::{H3, HttpsRr};
use crate::transport::{
connect::{Protocol, Via},
hyper_io::HyperIo,
stream::Stream,
};
async fn h1_sender() -> http1::SendRequest<http_kit::Body> {
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("test server must bind");
let address = listener.local_addr().expect("test address must exist");
thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("test connection must arrive");
let mut buffer = [0_u8; 128];
while socket.read(&mut buffer).unwrap_or(0) > 0 {}
});
let tcp = TcpStream::connect(address)
.await
.expect("test must connect");
let (mut sender, driver) = http1::handshake(HyperIo(Stream::Tcp(tcp)))
.await
.expect("h1 handshake must succeed");
thread::spawn(move || {
let _ = async_io::block_on(driver);
});
sender
.ready()
.await
.expect("fresh connection must be ready");
sender
}
fn origin() -> Origin {
Origin {
scheme: Scheme::HTTP,
host: "127.0.0.1".to_owned(),
port: 9,
}
}
fn park_idle(pool: &Pool, origin: &Origin, sender: http1::SendRequest<http_kit::Body>) {
let entry = pool.entry(origin);
*entry.protocol.lock().expect("pool state poisoned") = Some(Protocol::Http1);
entry
.h1_idle
.lock()
.expect("pool state poisoned")
.push(IdleH1 {
sender,
via: Via::Direct,
idle_since: Instant::now(),
});
}
#[test]
fn fresh_idle_connections_are_leased() {
block_on(async {
let pool = Pool::new();
let origin = origin();
park_idle(&pool, &origin, h1_sender().await);
assert!(
matches!(
pool.checkout(
origin,
Reuse::Pooled,
#[cfg(http3)]
H3::Blocked
)
.await,
Checkout::H1(_)
),
"a live idle connection must be leased, not dialed past"
);
});
}
#[test]
fn fresh_dial_skips_idle_connections() {
block_on(async {
let pool = Pool::new();
let origin = origin();
park_idle(&pool, &origin, h1_sender().await);
assert!(
matches!(
pool.checkout(
origin,
Reuse::FreshDial,
#[cfg(http3)]
H3::Blocked
)
.await,
Checkout::Dial { .. }
),
"a fresh-dial checkout must ignore idle connections"
);
});
}
#[test]
fn expired_idle_connections_are_evicted() {
block_on(async {
let pool = Pool {
origins: Mutex::new(HashMap::new()),
idle_timeout: Duration::ZERO,
};
let origin = origin();
park_idle(&pool, &origin, h1_sender().await);
let entry = pool.entry(&origin);
assert!(
matches!(
pool.checkout(
origin,
Reuse::Pooled,
#[cfg(http3)]
H3::Blocked
)
.await,
Checkout::Dial { .. }
),
"an expired idle connection must be evicted, not leased"
);
assert!(
entry
.h1_idle
.lock()
.expect("pool state poisoned")
.is_empty(),
"the expired entry must be gone"
);
});
}
#[test]
fn released_leases_return_to_idle() {
block_on(async {
let pool = Pool::new();
let origin = origin();
let Checkout::Dial { permit, .. } = pool
.checkout(
origin.clone(),
Reuse::Pooled,
#[cfg(http3)]
H3::Blocked,
)
.await
else {
panic!("an empty pool must hand out a dial permit");
};
Pool::insert_h1(permit, h1_sender().await, Via::Direct)
.await
.release();
assert!(
matches!(
pool.checkout(
origin,
Reuse::Pooled,
#[cfg(http3)]
H3::Blocked
)
.await,
Checkout::H1(_)
),
"a released connection must come back out of idle"
);
});
}
#[test]
fn finished_origins_are_swept_on_insert() {
block_on(async {
let pool = Pool {
origins: Mutex::new(HashMap::new()),
idle_timeout: Duration::ZERO,
};
for port in [9_u16, 10, 11] {
let origin = Origin { port, ..origin() };
park_idle(&pool, &origin, h1_sender().await);
}
let held = Origin {
port: 12,
..origin()
};
let Checkout::Dial { permit, .. } = pool
.checkout(
held.clone(),
Reuse::Pooled,
#[cfg(http3)]
H3::Blocked,
)
.await
else {
panic!("an empty origin must hand out a dial permit");
};
pool.entry(&Origin {
port: 13,
..origin()
});
let origins = pool.origins.lock().expect("pool state poisoned");
assert!(
!origins.contains_key(&origin())
&& !origins.contains_key(&Origin {
port: 10,
..origin()
})
&& !origins.contains_key(&Origin {
port: 11,
..origin()
}),
"expired, unreferenced entries must be swept"
);
assert!(
origins.contains_key(&held),
"an entry still referenced by a permit must survive"
);
assert!(
origins.contains_key(&Origin {
port: 13,
..origin()
}),
"the newly inserted origin must survive"
);
assert_eq!(origins.len(), 2);
drop(origins);
drop(permit);
});
}
#[cfg(http3)]
fn https_origin() -> Origin {
Origin {
scheme: Scheme::HTTPS,
..origin()
}
}
#[cfg(http3)]
#[test]
fn h3_candidate_prefers_alt_svc_over_https_rr() {
let pool = Pool::new();
let entry = pool.entry(&https_origin());
let now = Instant::now();
entry.insert_https_rr(HttpsRr {
h3: true,
port: Some(8443),
expires: now + Duration::from_secs(60),
});
assert_eq!(
entry.h3_candidate(now),
Some(8443),
"an h3 HTTPS record alone names its port"
);
entry.insert_alt_svc("127.0.0.1", r#"h3=":9443"; ma=60"#, now);
assert_eq!(
entry.h3_candidate(now),
Some(9443),
"a fresh Alt-Svc wins over the DNS answer"
);
}
#[cfg(http3)]
#[test]
fn expired_knowledge_is_evicted_on_read() {
let pool = Pool::new();
let entry = pool.entry(&https_origin());
let now = Instant::now();
let stale = now
.checked_sub(Duration::from_secs(60))
.expect("test instant is far from the epoch");
entry.insert_https_rr(HttpsRr {
h3: true,
port: Some(8443),
expires: stale,
});
assert_eq!(entry.h3_candidate(now), None);
assert!(
entry.https_rr().is_none(),
"a stale record must be dropped on read"
);
}
#[cfg(http3)]
#[test]
fn backoff_hides_the_candidate() {
let pool = Pool::new();
let entry = pool.entry(&https_origin());
let now = Instant::now();
entry.insert_alt_svc("127.0.0.1", r#"h3=":9443"; ma=60"#, now);
entry.mark_h3_broken(now);
assert_eq!(
entry.h3_candidate(now),
None,
"an origin under QUIC backoff must not be offered h3"
);
assert!(entry.h3_broken_until().is_some());
}
#[cfg(http3)]
#[test]
fn concurrent_h3_checkouts_coalesce() {
block_on(async {
use crate::backend::test_support;
let server = test_support::H3Server::start(&test_support::TestCa::new());
let (_transport, connection) = test_support::h3_connection(&server).await;
let pool = Pool::new();
let origin = https_origin();
pool.entry(&origin).insert_alt_svc(
&origin.host,
&format!(r#"h3=":{}"; ma=60"#, server.addr().port()),
Instant::now(),
);
let first = pool.checkout(origin.clone(), Reuse::Pooled, H3::Allowed);
let second = pool.checkout(origin.clone(), Reuse::Pooled, H3::Allowed);
futures_util::pin_mut!(first);
futures_util::pin_mut!(second);
let Checkout::Dial { h3_port, permit } = first.await else {
panic!("an h3-capable origin must dial with a QUIC port")
};
assert_eq!(h3_port, Some(server.addr().port()));
let pending = futures_util::future::poll_fn(|cx| {
std::task::Poll::Ready(second.as_mut().poll(cx).is_pending())
})
.await;
assert!(
pending,
"the second checkout must wait on `dialing`, not race its own"
);
Pool::insert_h3(permit, connection);
assert!(
matches!(second.await, Checkout::H3(_)),
"once the dial lands its h3 handle, the waiter must ride it"
);
});
}
#[cfg(http3)]
#[test]
fn h3_knowledge_survives_the_origin_sweep() {
let pool = Pool::new();
let now = Instant::now();
let fresh = Origin {
port: 20,
..https_origin()
};
let stale = Origin {
port: 21,
..https_origin()
};
pool.entry(&fresh)
.insert_alt_svc(&fresh.host, r#"h3=":9443"; ma=60"#, now);
pool.entry(&stale)
.insert_alt_svc(&stale.host, r#"h3=":9443"; ma=0"#, now);
pool.entry(&Origin {
port: 22,
..https_origin()
});
let origins = pool.origins.lock().expect("pool state poisoned");
assert!(
origins.contains_key(&fresh),
"an unexpired Alt-Svc must keep its entry"
);
assert!(
!origins.contains_key(&stale),
"an expired Alt-Svc must not keep its entry"
);
drop(origins);
}
#[cfg(http3)]
#[test]
fn h3_discovery_is_issued_once_and_recovers() {
let pool = Pool::new();
let entry = pool.entry(&https_origin());
let now = Instant::now();
let guard = entry
.begin_h3_discovery(now)
.expect("the first lookup must be issued");
assert!(
entry.begin_h3_discovery(now).is_none(),
"a lookup in flight must not be issued again"
);
drop(guard);
let guard = entry
.begin_h3_discovery(now)
.expect("a lookup dropped without an answer must be reissuable");
guard.insert_https_rr(HttpsRr {
h3: false,
port: None,
expires: now + Duration::from_secs(60),
});
assert!(
entry.begin_h3_discovery(now).is_none(),
"a fresh answer must not be re-queried"
);
entry.insert_https_rr(HttpsRr {
h3: false,
port: None,
expires: now,
});
assert!(
entry.begin_h3_discovery(now).is_some(),
"an expired answer must be re-queried"
);
}
}