use std::future::Future;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::Semaphore;
use super::{Dispatcher, connections, pause_after_accept_error};
mod idle;
use idle::IdleTimeout;
mod config;
pub use config::{
DEFAULT_COMPLETION_GRACE, DEFAULT_DRAIN_TIMEOUT, DEFAULT_HEADER_READ_TIMEOUT,
DEFAULT_IDLE_TIMEOUT, DEFAULT_TASK_GRACE, ServeConfig,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ServeReport {
pub accepted: u64,
pub drained: bool,
pub abandoned: usize,
pub tasks: Option<crate::handler::InFlightReport>,
}
#[derive(Debug)]
pub struct Server {
listener: TcpListener,
config: ServeConfig,
}
impl Server {
pub async fn bind(addr: impl tokio::net::ToSocketAddrs) -> std::io::Result<Self> {
Ok(Self {
listener: TcpListener::bind(addr).await?,
config: ServeConfig::default(),
})
}
#[must_use]
pub const fn with_config(mut self, config: ServeConfig) -> Self {
self.config = config;
self
}
pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
self.listener.local_addr()
}
#[allow(clippy::too_many_lines)]
pub async fn serve_with_shutdown(
self,
dispatcher: impl Dispatcher,
shutdown: impl Future<Output = ()> + Send,
) -> ServeReport {
let Self { listener, config } = self;
let connections = connections::Connections::for_dispatcher(&dispatcher);
let dispatcher = Arc::new(dispatcher);
let graceful = hyper_util::server::graceful::GracefulShutdown::new();
let accepted = AtomicU64::new(0);
let permits = Arc::new(Semaphore::new(
config.max_connections.unwrap_or(Semaphore::MAX_PERMITS),
));
trace_info!(
addr = %listener.local_addr().unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0))),
max_connections = ?config.max_connections,
"A2A server listening (graceful)"
);
let mut shutdown = std::pin::pin!(shutdown);
loop {
let (Ok(permit), accept) = (tokio::select! {
biased;
() = &mut shutdown => break,
next = async {
let permit = Arc::clone(&permits).acquire_owned().await;
(permit, listener.accept().await)
} => next,
}) else {
break;
};
let (stream, _peer) = match accept {
Ok(pair) => pair,
Err(e) => {
trace_warn!(error = %e, "accept() failed; retrying");
pause_after_accept_error(&e).await;
continue;
}
};
accepted.fetch_add(1, Ordering::Relaxed);
spawn_connection(
stream,
Arc::clone(&dispatcher),
graceful.watcher(),
permit,
&config,
connections.as_ref().map(connections::Connections::opened),
);
}
drop(listener);
let tasks = match dispatcher.request_handler() {
Some(handler) => Some(
handler
.finish_in_flight(config.completion_grace, config.task_grace)
.await,
),
None => None,
};
let mut report = drain(
graceful,
accepted.load(Ordering::Relaxed),
config.drain_timeout,
)
.await;
report.tasks = tasks;
report
}
}
fn spawn_connection(
stream: tokio::net::TcpStream,
dispatcher: Arc<impl Dispatcher>,
watcher: hyper_util::server::graceful::Watcher,
permit: tokio::sync::OwnedSemaphorePermit,
config: &ServeConfig,
connection: Option<connections::Connection>,
) {
let _ = stream.set_nodelay(true);
let io = hyper_util::rt::TokioIo::new(IdleTimeout::new(stream, config.idle_timeout));
let header_read_timeout = config.header_read_timeout;
tokio::spawn(async move {
let service = super::service(
dispatcher,
connection.as_ref().map(connections::Connection::requests),
);
let mut builder =
hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new());
builder
.http1()
.timer(hyper_util::rt::TokioTimer::new())
.header_read_timeout(header_read_timeout);
builder.http2().timer(hyper_util::rt::TokioTimer::new());
let conn = builder.serve_connection(io, service);
let result = watcher.watch(conn).await;
if let Err(_e) = &result {
trace_warn!(error = %_e, "connection error");
}
if let Some(connection) = connection {
connection.close(result.is_err());
}
drop(permit);
});
}
async fn drain(
graceful: hyper_util::server::graceful::GracefulShutdown,
accepted: u64,
timeout: Duration,
) -> ServeReport {
let in_flight = graceful.count();
trace_info!(
accepted,
in_flight,
"shutdown signalled; draining connections"
);
if tokio::time::timeout(timeout, graceful.shutdown())
.await
.is_ok()
{
ServeReport {
accepted,
drained: true,
abandoned: 0,
tasks: None,
}
} else {
trace_warn!(
abandoned = in_flight,
"drain timeout expired with connections still open"
);
ServeReport {
accepted,
drained: false,
abandoned: in_flight,
tasks: None,
}
}
}
#[cfg(test)]
mod tests;