fix(rustproxy-http): validate pooled HTTP/2 connections asynchronously before reuse and evict stale senders

This commit is contained in:
2026-03-16 12:29:15 +00:00
parent b1f4181139
commit 8e76c42cea
4 changed files with 53 additions and 16 deletions

View File

@@ -10,7 +10,7 @@ use bytes::Bytes;
use dashmap::DashMap;
use http_body_util::combinators::BoxBody;
use hyper::client::conn::{http1, http2};
use tracing::debug;
use tracing::{debug, warn};
/// Maximum idle connections per backend key.
const MAX_IDLE_PER_KEY: usize = 16;
@@ -115,20 +115,27 @@ impl ConnectionPool {
/// Try to get a cloned HTTP/2 sender for the given key.
/// HTTP/2 senders are Clone-able (multiplexed), so we clone rather than remove.
pub fn checkout_h2(&self, key: &PoolKey) -> Option<http2::SendRequest<BoxBody<Bytes, hyper::Error>>> {
pub fn checkout_h2(&self, key: &PoolKey) -> Option<(http2::SendRequest<BoxBody<Bytes, hyper::Error>>, Duration)> {
let entry = self.h2_pool.get(key)?;
let pooled = entry.value();
let age = pooled.created_at.elapsed();
// Check if the h2 connection is still alive and not too old
if pooled.sender.is_closed() || pooled.created_at.elapsed() >= MAX_H2_AGE {
if pooled.sender.is_closed() || age >= MAX_H2_AGE {
let reason = if pooled.sender.is_closed() { "closed" } else { "max_age" };
debug!("Pool evict (h2): {}:{} (reason={}, age={:.1}s)", key.host, key.port, reason, age.as_secs_f64());
drop(entry);
self.h2_pool.remove(key);
return None;
}
if pooled.sender.is_ready() {
debug!("Pool hit (h2): {}:{}", key.host, key.port);
return Some(pooled.sender.clone());
if age > Duration::from_secs(30) {
warn!("Pool hit (h2): {}:{} — connection age {:.1}s (>30s, may be stale)", key.host, key.port, age.as_secs_f64());
} else {
debug!("Pool hit (h2): {}:{} (age={:.1}s)", key.host, key.port, age.as_secs_f64());
}
return Some((pooled.sender.clone(), age));
}
None
}