use std::future::Future;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use hyper::header::HeaderValue;
use hyper::service::service_fn;
use hyper_util::rt::{TokioExecutor, TokioIo};
use plecto_control::Control;
use tokio::net::TcpListener;
use tokio::sync::{Semaphore, watch};
use tokio::task::JoinSet;
use tokio_rustls::TlsAcceptor;
use crate::body::MAX_INFLIGHT_BODY_BUFFERS;
use crate::conn_limit::PerIpConnLimit;
use crate::dispatch::handle;
use crate::error::ServerError;
use crate::h3::{build_h3_endpoint, serve_h3};
use crate::health::serve_health_checks;
use crate::metrics::ServerMetrics;
use crate::upstream_client::UpstreamClients;
use crate::{MAX_CONCURRENT_STREAMS, MAX_CONNECTIONS, MAX_CONNECTIONS_PER_IP, ServerState, admin};
const MAX_HEADERS: usize = 100;
const INBOUND_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(15);
const H2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(20);
const H2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(20);
pub const DEFAULT_DRAIN_DEADLINE: Duration = Duration::from_secs(30);
pub async fn serve(control: Arc<Control>, listener: TcpListener) -> anyhow::Result<()> {
serve_inner(control, listener, std::future::pending::<()>())
.await
.map_err(Into::into)
}
pub async fn serve_with_shutdown(
control: Arc<Control>,
listener: TcpListener,
shutdown: impl Future<Output = ()> + Send,
) -> anyhow::Result<()> {
serve_inner(control, listener, shutdown)
.await
.map_err(Into::into)
}
async fn serve_inner(
control: Arc<Control>,
listener: TcpListener,
shutdown: impl Future<Output = ()> + Send,
) -> Result<(), ServerError> {
let tcp_addr = listener.local_addr().map_err(ServerError::Bind)?;
let quic_cfg = control.quic_tls_config();
let advertised_port = control.advertised_port().unwrap_or_else(|| tcp_addr.port());
let alt_svc = quic_cfg
.as_ref()
.and_then(|_| HeaderValue::from_str(&format!("h3=\":{advertised_port}\"; ma=86400")).ok());
let otlp_export = control
.otlp_endpoint()
.map(str::to_string)
.zip(control.otlp_buffer());
let (drain_tx, drain_rx) = watch::channel(false);
let (ready_tx, ready_rx) = watch::channel(true);
let state = Arc::new(ServerState {
control,
clients: UpstreamClients::new(),
alt_svc,
conn_limit: Arc::new(Semaphore::new(MAX_CONNECTIONS)),
per_ip_conn_limit: Arc::new(PerIpConnLimit::new(MAX_CONNECTIONS_PER_IP)),
body_buffer_limit: Arc::new(Semaphore::new(MAX_INFLIGHT_BODY_BUFFERS)),
metrics: Arc::new(ServerMetrics::new()),
otlp: otlp_export.as_ref().map(|(_, buffer)| buffer.clone()),
drain: drain_rx.clone(),
ready: ready_rx,
});
let drain_window = state
.control
.drain_window()
.unwrap_or(DEFAULT_DRAIN_DEADLINE);
let readiness_grace = state.control.readiness_grace();
if let Some(admin_addr) = state.control.admin_addr() {
match admin_addr.parse::<SocketAddr>() {
Ok(addr) => {
tokio::spawn(admin::serve_admin(state.clone(), addr, drain_rx.clone()));
}
Err(e) => {
tracing::error!(addr = admin_addr, error = %e, "invalid observability.admin_addr; admin endpoint disabled");
}
}
}
tokio::spawn(serve_health_checks(state.control.clone(), drain_rx.clone()));
tokio::spawn(crate::dns::serve_dns_refresh(
state.control.clone(),
drain_rx.clone(),
));
let otlp_pump = otlp_export.map(|(endpoint, buffer)| {
tokio::spawn(crate::otlp::serve_otlp_export(
buffer,
endpoint,
drain_rx.clone(),
))
});
let h3_task = quic_cfg.and_then(|cfg| match build_h3_endpoint(cfg, tcp_addr) {
Ok(endpoint) => {
tracing::info!(port = tcp_addr.port(), "HTTP/3 (QUIC) listener bound");
Some(tokio::spawn(serve_h3(
state.clone(),
endpoint,
drain_rx.clone(),
drain_window,
)))
}
Err(e) => {
tracing::error!(error = %e, "failed to bind HTTP/3 listener; serving TCP only");
None
}
});
let proxy_trust = state.control.proxy_protocol_trust().map(Arc::new);
if proxy_trust.is_some() {
tracing::info!(
"PROXY protocol v2 enabled on the TCP listener (the h3/UDP listener is not covered — ADR 000057)"
);
}
let shutdown = async move {
shutdown.await;
let _ = ready_tx.send(false);
if !readiness_grace.is_zero() {
tracing::info!(
grace_ms = readiness_grace.as_millis() as u64,
"shutdown signal: /readyz is not-ready; accepting through the readiness grace"
);
tokio::time::sleep(readiness_grace).await;
}
};
let mut conns = JoinSet::new();
tokio::pin!(shutdown);
loop {
let permit = tokio::select! {
_ = &mut shutdown => break,
Some(_) = conns.join_next() => continue,
permit = state.conn_limit.clone().acquire_owned() => match permit {
Ok(p) => p,
Err(_) => return Ok(()), },
};
let (mut stream, peer) = tokio::select! {
_ = &mut shutdown => break,
Some(_) = conns.join_next() => continue, accepted = listener.accept() => match accepted {
Ok(pair) => pair,
Err(e) => {
tracing::warn!(error = %e, "accept failed");
continue;
}
},
};
if let Err(e) = stream.set_nodelay(true) {
tracing::debug!(error = %e, "set_nodelay failed; Nagle stays on for this connection");
}
let state = state.clone();
let tls = state.control.tls_config();
let drain = drain_rx.clone();
let proxy_trust = proxy_trust.clone();
conns.spawn(async move {
let _permit = permit; let peer = match &proxy_trust {
Some(trust) => {
match crate::proxy_protocol::resolve_peer(
&mut stream,
peer,
trust,
INBOUND_HEADER_READ_TIMEOUT,
)
.await
{
Ok(resolved) => resolved,
Err(fault) => {
tracing::warn!(peer = %peer, fault = %fault, "proxy-protocol: connection rejected");
return;
}
}
}
None => peer,
};
let Some(_ip_guard) = PerIpConnLimit::try_acquire(&state.per_ip_conn_limit, peer.ip())
else {
tracing::debug!(peer = %peer, "per-IP connection limit reached; refusing connection");
return;
};
match tls {
Some(cfg) => match tokio::time::timeout(
INBOUND_HEADER_READ_TIMEOUT,
TlsAcceptor::from(cfg).accept(stream),
)
.await
{
Err(_elapsed) => {
tracing::debug!(peer = %peer, "TLS handshake timed out");
}
Ok(Ok(tls_stream)) => {
let h2 = tls_stream.get_ref().1.alpn_protocol() == Some(b"h2".as_ref());
serve_conn(state, TokioIo::new(tls_stream), "https", h2, peer, drain).await;
}
Ok(Err(e)) => tracing::debug!(error = %e, "TLS handshake failed"),
},
None => serve_conn(state, TokioIo::new(stream), "http", false, peer, drain).await,
}
});
}
drop(listener);
let _ = drain_tx.send(true);
let all_drained = state.conn_limit.acquire_many(MAX_CONNECTIONS as u32);
if tokio::time::timeout(drain_window, all_drained)
.await
.is_ok()
{
tracing::info!("graceful shutdown: all connections drained");
} else {
tracing::warn!(
deadline_ms = drain_window.as_millis() as u64,
"graceful shutdown: drain window expired; cutting remaining connections"
);
conns.abort_all();
}
if let Some(h3) = h3_task {
let _ = tokio::time::timeout(drain_window + Duration::from_secs(1), h3).await;
}
if let Some(pump) = otlp_pump {
let _ = tokio::time::timeout(
crate::otlp::SHUTDOWN_FLUSH_DEADLINE + Duration::from_secs(1),
pump,
)
.await;
}
Ok(())
}
pub(crate) async fn drained(drain: &mut watch::Receiver<bool>) {
let _ = drain.wait_for(|d| *d).await;
}
async fn serve_conn<I>(
state: Arc<ServerState>,
io: I,
scheme: &'static str,
h2: bool,
peer: SocketAddr,
mut drain: watch::Receiver<bool>,
) where
I: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
{
let service = service_fn(move |req| handle(state.clone(), scheme, peer, req));
let result = if h2 {
let conn = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
.max_concurrent_streams(MAX_CONCURRENT_STREAMS)
.timer(hyper_util::rt::TokioTimer::new())
.keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL))
.keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT)
.serve_connection(io, service);
tokio::pin!(conn);
tokio::select! {
res = conn.as_mut() => res,
_ = drained(&mut drain) => {
conn.as_mut().graceful_shutdown();
conn.await
}
}
} else {
let conn = hyper::server::conn::http1::Builder::new()
.timer(hyper_util::rt::TokioTimer::new())
.header_read_timeout(INBOUND_HEADER_READ_TIMEOUT)
.max_headers(MAX_HEADERS)
.serve_connection(io, service)
.with_upgrades();
tokio::pin!(conn);
tokio::select! {
res = conn.as_mut() => res,
_ = drained(&mut drain) => {
conn.as_mut().graceful_shutdown();
conn.await
}
}
};
if let Err(e) = result {
tracing::debug!(error = %e, "connection closed with error");
}
}