2026-09-10 08:14:58 +08:00
|
|
|
use std::future::Future;
|
|
|
|
|
use std::io;
|
|
|
|
|
use std::net::SocketAddr;
|
|
|
|
|
use std::pin::Pin;
|
|
|
|
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::task::{Context, Poll};
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
|
|
|
|
use tokio::net::{TcpListener, TcpStream};
|
2026-09-10 08:31:47 +08:00
|
|
|
use tokio::sync::{OwnedSemaphorePermit, Semaphore, TryAcquireError};
|
2026-09-10 08:14:58 +08:00
|
|
|
use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned};
|
|
|
|
|
|
|
|
|
|
const MAX_HTTP_CONNECTIONS: usize = 65_536;
|
|
|
|
|
const FD_RESERVE: usize = 256;
|
|
|
|
|
|
|
|
|
|
/// Selects a per-server incoming TCP limit. The FD allowance leaves room for
|
|
|
|
|
/// upstream sockets and process infrastructure; it is not a complete FD budget.
|
|
|
|
|
pub fn http_connection_limit(
|
|
|
|
|
configured: Option<usize>,
|
|
|
|
|
request_limit: usize,
|
|
|
|
|
websocket_limit: usize,
|
|
|
|
|
fd_soft_limit: Option<usize>,
|
|
|
|
|
) -> usize {
|
|
|
|
|
let configured = configured
|
|
|
|
|
.filter(|limit| *limit > 0)
|
|
|
|
|
.unwrap_or_else(|| request_limit.saturating_add(websocket_limit))
|
|
|
|
|
.clamp(1, MAX_HTTP_CONNECTIONS);
|
|
|
|
|
let fd_allowance = fd_soft_limit
|
|
|
|
|
.map(|limit| (limit.saturating_sub(FD_RESERVE) / 2).max(1))
|
|
|
|
|
.unwrap_or(MAX_HTTP_CONNECTIONS);
|
|
|
|
|
configured.min(fd_allowance)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub struct HttpConnectionBudgetSnapshot {
|
|
|
|
|
pub limit: usize,
|
|
|
|
|
pub in_flight: usize,
|
|
|
|
|
pub high_watermark: usize,
|
|
|
|
|
pub rejected_total: u64,
|
|
|
|
|
pub accept_errors_total: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Share one budget across listeners. Admission belongs to the underlying IO,
|
|
|
|
|
/// so HTTP/1 upgrades keep their permit and HTTP/2 streams share one permit.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct HttpConnectionBudget {
|
|
|
|
|
limit: usize,
|
|
|
|
|
permits: Arc<Semaphore>,
|
|
|
|
|
in_flight: AtomicUsize,
|
|
|
|
|
high_watermark: AtomicUsize,
|
|
|
|
|
rejected_total: AtomicU64,
|
|
|
|
|
accept_errors_total: AtomicU64,
|
|
|
|
|
shutdown: CancellationToken,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HttpConnectionBudget {
|
|
|
|
|
pub fn new(limit: usize) -> Self {
|
|
|
|
|
let limit = limit.clamp(1, MAX_HTTP_CONNECTIONS.min(Semaphore::MAX_PERMITS));
|
|
|
|
|
Self {
|
|
|
|
|
limit,
|
|
|
|
|
permits: Arc::new(Semaphore::new(limit)),
|
|
|
|
|
in_flight: AtomicUsize::new(0),
|
|
|
|
|
high_watermark: AtomicUsize::new(0),
|
|
|
|
|
rejected_total: AtomicU64::new(0),
|
|
|
|
|
accept_errors_total: AtomicU64::new(0),
|
|
|
|
|
shutdown: CancellationToken::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Admit after accepting. Waiting for a permit before accept can let idle
|
|
|
|
|
/// reuseport listeners monopolize permits needed by a busy listener.
|
2026-09-10 08:31:47 +08:00
|
|
|
pub fn try_admit<T>(self: &Arc<Self>, io: T) -> Result<AdmittedConnection<T>, TryAcquireError> {
|
|
|
|
|
let permit = Arc::clone(&self.permits)
|
|
|
|
|
.try_acquire_owned()
|
|
|
|
|
.inspect_err(|_| {
|
|
|
|
|
self.rejected_total.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
})?;
|
2026-09-10 08:14:58 +08:00
|
|
|
let in_flight = self.in_flight.fetch_add(1, Ordering::Relaxed) + 1;
|
|
|
|
|
self.high_watermark.fetch_max(in_flight, Ordering::Relaxed);
|
|
|
|
|
Ok(AdmittedConnection {
|
|
|
|
|
io,
|
|
|
|
|
read_shutdown: Box::pin(self.shutdown.clone().cancelled_owned()),
|
|
|
|
|
write_shutdown: Box::pin(self.shutdown.clone().cancelled_owned()),
|
|
|
|
|
_permit: ConnectionPermit {
|
|
|
|
|
budget: Arc::clone(self),
|
|
|
|
|
_permit: permit,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn snapshot(&self) -> HttpConnectionBudgetSnapshot {
|
|
|
|
|
HttpConnectionBudgetSnapshot {
|
|
|
|
|
limit: self.limit,
|
|
|
|
|
in_flight: self.in_flight.load(Ordering::Relaxed),
|
|
|
|
|
high_watermark: self.high_watermark.load(Ordering::Relaxed),
|
|
|
|
|
rejected_total: self.rejected_total.load(Ordering::Relaxed),
|
|
|
|
|
accept_errors_total: self.accept_errors_total.load(Ordering::Relaxed),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// End the drain deadline for all sockets, including upgraded connections.
|
|
|
|
|
pub fn force_close(&self) {
|
|
|
|
|
self.permits.close();
|
|
|
|
|
self.shutdown.cancel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn wait_for_forced_close(&self) {
|
|
|
|
|
self.shutdown.cancelled().await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn accept(&self, listener: &TcpListener) -> (TcpStream, SocketAddr) {
|
|
|
|
|
self.accept_with(|| listener.accept()).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn accept_with<T, F, A>(&self, mut accept: A) -> T
|
|
|
|
|
where
|
|
|
|
|
A: FnMut() -> F,
|
|
|
|
|
F: Future<Output = io::Result<T>>,
|
|
|
|
|
{
|
|
|
|
|
loop {
|
|
|
|
|
match accept().await {
|
|
|
|
|
Ok(connection) => return connection,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
self.accept_errors_total.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
// Match Axum's listener behavior: failed peers can be retried
|
|
|
|
|
// immediately; resource failures such as EMFILE need backoff.
|
|
|
|
|
if matches!(
|
|
|
|
|
error.kind(),
|
|
|
|
|
io::ErrorKind::ConnectionRefused
|
|
|
|
|
| io::ErrorKind::ConnectionAborted
|
|
|
|
|
| io::ErrorKind::ConnectionReset
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
tracing::error!(
|
|
|
|
|
event_name = "http_connection_accept_failed",
|
|
|
|
|
error = %error,
|
|
|
|
|
"HTTP listener accept failed; retrying after one second"
|
|
|
|
|
);
|
|
|
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct ConnectionPermit {
|
|
|
|
|
budget: Arc<HttpConnectionBudget>,
|
|
|
|
|
_permit: OwnedSemaphorePermit,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for ConnectionPermit {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
self.budget.in_flight.fetch_sub(1, Ordering::Relaxed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct AdmittedConnection<T> {
|
|
|
|
|
// Close the socket before returning its permit, including upgrade teardown.
|
|
|
|
|
io: T,
|
|
|
|
|
read_shutdown: Pin<Box<WaitForCancellationFutureOwned>>,
|
|
|
|
|
write_shutdown: Pin<Box<WaitForCancellationFutureOwned>>,
|
|
|
|
|
_permit: ConnectionPermit,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: AsyncRead + Unpin> AsyncRead for AdmittedConnection<T> {
|
|
|
|
|
fn poll_read(
|
|
|
|
|
mut self: Pin<&mut Self>,
|
|
|
|
|
cx: &mut Context<'_>,
|
|
|
|
|
buf: &mut ReadBuf<'_>,
|
|
|
|
|
) -> Poll<io::Result<()>> {
|
|
|
|
|
if self.read_shutdown.as_mut().poll(cx).is_ready() {
|
|
|
|
|
return Poll::Ready(Err(shutdown_error()));
|
|
|
|
|
}
|
|
|
|
|
Pin::new(&mut self.io).poll_read(cx, buf)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: AsyncWrite + Unpin> AsyncWrite for AdmittedConnection<T> {
|
|
|
|
|
fn poll_write(
|
|
|
|
|
mut self: Pin<&mut Self>,
|
|
|
|
|
cx: &mut Context<'_>,
|
|
|
|
|
buf: &[u8],
|
|
|
|
|
) -> Poll<io::Result<usize>> {
|
|
|
|
|
if self.write_shutdown.as_mut().poll(cx).is_ready() {
|
|
|
|
|
return Poll::Ready(Err(shutdown_error()));
|
|
|
|
|
}
|
|
|
|
|
Pin::new(&mut self.io).poll_write(cx, buf)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
|
|
|
if self.write_shutdown.as_mut().poll(cx).is_ready() {
|
|
|
|
|
return Poll::Ready(Err(shutdown_error()));
|
|
|
|
|
}
|
|
|
|
|
Pin::new(&mut self.io).poll_flush(cx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
|
|
|
Pin::new(&mut self.io).poll_shutdown(cx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_write_vectored(&self) -> bool {
|
|
|
|
|
self.io.is_write_vectored()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn poll_write_vectored(
|
|
|
|
|
mut self: Pin<&mut Self>,
|
|
|
|
|
cx: &mut Context<'_>,
|
|
|
|
|
bufs: &[io::IoSlice<'_>],
|
|
|
|
|
) -> Poll<io::Result<usize>> {
|
|
|
|
|
if self.write_shutdown.as_mut().poll(cx).is_ready() {
|
|
|
|
|
return Poll::Ready(Err(shutdown_error()));
|
|
|
|
|
}
|
|
|
|
|
Pin::new(&mut self.io).poll_write_vectored(cx, bufs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn shutdown_error() -> io::Error {
|
|
|
|
|
io::Error::new(
|
|
|
|
|
io::ErrorKind::ConnectionAborted,
|
|
|
|
|
"gateway shutdown deadline reached",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
#[path = "connection_tests.rs"]
|
|
|
|
|
mod tests;
|