use std::future::Future;
use std::io;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use axum::Router;
use axum::extract::ConnectInfo;
use http::Request;
use hyper::body::Incoming;
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
use hyper_util::server::conn::auto::Builder;
use hyper_util::server::graceful::GracefulShutdown;
use hyper_util::service::TowerToHyperService;
use kerniq_core::{BoxError, ServerConfig, ServerError};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Semaphore;
use tokio_util::sync::CancellationToken;
use tower::ServiceExt;
use crate::Shutdown;
#[derive(Debug, Clone)]
pub struct ServeOptions {
pub max_connections: Option<usize>,
pub shutdown_timeout: Duration,
pub header_read_timeout: Duration,
pub tcp_nodelay: bool,
pub connect_info: bool,
}
impl Default for ServeOptions {
fn default() -> Self {
Self::from(&ServerConfig::default())
}
}
impl From<&ServerConfig> for ServeOptions {
fn from(config: &ServerConfig) -> Self {
Self {
max_connections: config.max_connections,
shutdown_timeout: config.shutdown_timeout,
header_read_timeout: config.header_read_timeout,
tcp_nodelay: config.tcp_nodelay,
connect_info: config.connect_info,
}
}
}
pub async fn bind(address: &str) -> Result<TcpListener, ServerError> {
if address.trim().is_empty() {
return Err(ServerError::InvalidAddress {
address: address.to_owned(),
reason: "address is empty".to_owned(),
});
}
TcpListener::bind(address)
.await
.map_err(|source| match source.kind() {
io::ErrorKind::InvalidInput => ServerError::InvalidAddress {
address: address.to_owned(),
reason: source.to_string(),
},
_ => ServerError::Bind {
address: address.to_owned(),
source,
},
})
}
pub async fn serve(
listener: TcpListener,
router: Router,
options: ServeOptions,
shutdown: Shutdown,
) -> Result<(), ServerError> {
let local_addr = listener.local_addr()?;
let limiter = options.max_connections.map(|n| Arc::new(Semaphore::new(n)));
let graceful = GracefulShutdown::new();
let force_close = CancellationToken::new();
let mut builder = Builder::new(TokioExecutor::new());
if !options.header_read_timeout.is_zero() {
builder
.http1()
.timer(TokioTimer::new())
.header_read_timeout(options.header_read_timeout);
builder.http2().timer(TokioTimer::new());
}
tracing::info!(address = %local_addr, "kerniq server listening");
loop {
let permit = match &limiter {
Some(semaphore) => tokio::select! {
biased;
() = shutdown.wait() => break,
permit = semaphore.clone().acquire_owned() => match permit {
Ok(permit) => Some(permit),
Err(_) => break, },
},
None => None,
};
let (stream, peer) = tokio::select! {
biased;
() = shutdown.wait() => break,
accepted = listener.accept() => match accepted {
Ok(pair) => pair,
Err(err) => {
handle_accept_error(err).await;
continue;
}
},
};
if options.tcp_nodelay
&& let Err(err) = stream.set_nodelay(true)
{
tracing::debug!(error = %err, "failed to set TCP_NODELAY");
}
let connection = serve_connection(
&builder,
&graceful,
stream,
peer,
router.clone(),
options.connect_info,
);
let force_close = force_close.clone();
tokio::spawn(async move {
tokio::select! {
result = connection => {
if let Err(err) = result {
tracing::trace!(peer = %peer, error = %err, "connection closed with error");
}
}
() = force_close.cancelled() => {
tracing::debug!(peer = %peer, "connection force-closed after shutdown timeout");
}
}
drop(permit);
});
}
drop(listener);
let open = graceful.count();
tracing::info!(
open_connections = open,
"stopped accepting connections, draining"
);
match tokio::time::timeout(options.shutdown_timeout, graceful.shutdown()).await {
Ok(()) => tracing::info!("all connections drained"),
Err(_) => {
tracing::warn!(
timeout = ?options.shutdown_timeout,
"shutdown timeout elapsed, closing remaining connections"
);
force_close.cancel();
}
}
Ok(())
}
fn serve_connection(
builder: &Builder<TokioExecutor>,
graceful: &GracefulShutdown,
stream: TcpStream,
peer: SocketAddr,
router: Router,
connect_info: bool,
) -> impl Future<Output = Result<(), BoxError>> + Send + 'static {
let service = router.map_request(move |mut request: Request<Incoming>| {
if connect_info {
request.extensions_mut().insert(ConnectInfo(peer));
}
request
});
let connection = builder
.serve_connection_with_upgrades(TokioIo::new(stream), TowerToHyperService::new(service))
.into_owned();
graceful.watch(connection)
}
async fn handle_accept_error(err: io::Error) {
if matches!(
err.kind(),
io::ErrorKind::ConnectionRefused
| io::ErrorKind::ConnectionAborted
| io::ErrorKind::ConnectionReset
) {
return;
}
tracing::error!(error = %err, "failed to accept connection");
tokio::time::sleep(Duration::from_millis(50)).await;
}