use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use super::bounded_incoming::BoundedIncoming;
use super::native::A2aServiceImpl;
use super::{A2aServiceServer, GrpcConfig};
use crate::handler::RequestHandler;
pub struct GrpcDispatcher {
pub(super) handler: Arc<RequestHandler>,
config: GrpcConfig,
keepalive: Option<(Duration, Duration)>,
max_connection_age: Option<Duration>,
pub(super) max_connections: Option<usize>,
pub(super) completion_grace: Duration,
pub(super) task_grace: Duration,
pub(super) drain_timeout: Duration,
#[cfg(feature = "grpc-tls")]
tls: Option<tonic::transport::ServerTlsConfig>,
}
impl GrpcDispatcher {
#[must_use]
pub const fn new(handler: Arc<RequestHandler>, config: GrpcConfig) -> Self {
Self {
handler,
config,
keepalive: None,
max_connection_age: None,
max_connections: None,
completion_grace: crate::serve::DEFAULT_COMPLETION_GRACE,
task_grace: crate::serve::DEFAULT_TASK_GRACE,
drain_timeout: crate::serve::DEFAULT_DRAIN_TIMEOUT,
#[cfg(feature = "grpc-tls")]
tls: None,
}
}
#[must_use]
pub const fn with_max_connections(mut self, max: usize) -> Self {
self.max_connections = Some(max);
self
}
#[cfg(feature = "grpc-tls")]
#[must_use]
pub fn with_tls(mut self, tls: tonic::transport::ServerTlsConfig) -> Self {
self.tls = Some(tls);
self
}
#[must_use]
pub const fn with_http2_keepalive(mut self, interval: Duration, timeout: Duration) -> Self {
self.keepalive = Some((interval, timeout));
self
}
#[must_use]
pub const fn with_max_connection_age(mut self, age: Duration) -> Self {
self.max_connection_age = Some(age);
self
}
#[must_use]
pub const fn with_completion_grace(mut self, grace: Duration) -> Self {
self.completion_grace = grace;
self
}
#[must_use]
pub const fn with_task_grace(mut self, grace: Duration) -> Self {
self.task_grace = grace;
self
}
#[must_use]
pub const fn with_drain_timeout(mut self, timeout: Duration) -> Self {
self.drain_timeout = timeout;
self
}
pub async fn serve(self, addr: impl tokio::net::ToSocketAddrs) -> std::io::Result<()> {
let addr = super::helpers::resolve_addr(addr).await?;
trace_info!(
addr = %addr,
"A2A gRPC server listening"
);
let listener = tokio::net::TcpListener::bind(addr).await?;
let incoming = BoundedIncoming::new(listener, self.max_connections);
let router = self.build_router()?;
router
.serve_with_incoming(incoming)
.await
.map_err(std::io::Error::other)
}
pub async fn serve_with_addr(
self,
addr: impl tokio::net::ToSocketAddrs,
) -> std::io::Result<SocketAddr> {
let listener = tokio::net::TcpListener::bind(addr).await?;
self.serve_with_listener(listener)
}
pub fn serve_with_listener(
self,
listener: tokio::net::TcpListener,
) -> std::io::Result<SocketAddr> {
let local_addr = listener.local_addr()?;
let incoming = BoundedIncoming::new(listener, self.max_connections);
trace_info!(
%local_addr,
"A2A gRPC server listening"
);
let router = self.build_router()?;
tokio::spawn(async move {
let _ = router.serve_with_incoming(incoming).await;
});
Ok(local_addr)
}
#[must_use]
pub fn into_service(&self) -> A2aServiceServer<A2aServiceImpl> {
let inner = A2aServiceImpl {
handler: Arc::clone(&self.handler),
config: self.config.clone(),
};
A2aServiceServer::new(inner)
.max_decoding_message_size(self.config.max_message_size)
.max_encoding_message_size(self.config.max_message_size)
}
#[cfg_attr(not(feature = "grpc-tls"), allow(clippy::unnecessary_wraps))]
pub(super) fn build_router(&self) -> std::io::Result<tonic::transport::server::Router> {
let mut server = tonic::transport::Server::builder()
.concurrency_limit_per_connection(self.config.concurrency_limit);
if let Some((interval, timeout)) = self.keepalive {
server = server
.http2_keepalive_interval(Some(interval))
.http2_keepalive_timeout(Some(timeout));
}
if let Some(age) = self.max_connection_age {
server = server.max_connection_age(age);
}
#[cfg(feature = "grpc-tls")]
if let Some(tls) = &self.tls {
ensure_crypto_provider();
server = server.tls_config(tls.clone()).map_err(|e| {
std::io::Error::other(format!("gRPC TLS configuration rejected: {e}"))
})?;
}
Ok(server.add_service(self.into_service()))
}
}
#[cfg(feature = "grpc-tls")]
fn ensure_crypto_provider() {
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
}
impl std::fmt::Debug for GrpcDispatcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("GrpcDispatcher");
s.field("handler", &"RequestHandler { .. }")
.field("config", &self.config)
.field("keepalive", &self.keepalive)
.field("max_connection_age", &self.max_connection_age)
.field("max_connections", &self.max_connections)
.field("completion_grace", &self.completion_grace)
.field("task_grace", &self.task_grace)
.field("drain_timeout", &self.drain_timeout);
#[cfg(feature = "grpc-tls")]
s.field("tls", &self.tls.is_some());
s.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn connection_knobs_are_off_by_default_and_carry_what_they_are_given() {
use crate::RequestHandlerBuilder;
use crate::agent_executor;
use std::sync::Arc;
struct DummyExec;
agent_executor!(DummyExec, |_ctx, _queue| async { Ok(()) });
let handler = Arc::new(RequestHandlerBuilder::new(DummyExec).build().unwrap());
let default = GrpcDispatcher::new(Arc::clone(&handler), GrpcConfig::default());
assert!(
default.keepalive.is_none(),
"HTTP/2 keepalive must be opt-in: enabling it by default would start \
pinging the clients of every deployment that upgrades"
);
assert!(
default.max_connection_age.is_none(),
"and so must connection ageing, which forces reconnects"
);
let tuned = GrpcDispatcher::new(handler, GrpcConfig::default())
.with_http2_keepalive(Duration::from_secs(30), Duration::from_secs(10))
.with_max_connection_age(Duration::from_secs(600));
assert_eq!(
tuned.keepalive,
Some((Duration::from_secs(30), Duration::from_secs(10)))
);
assert_eq!(tuned.max_connection_age, Some(Duration::from_secs(600)));
let _router = tuned.build_router();
}
#[test]
fn grpc_dispatcher_debug_does_not_panic() {
use crate::RequestHandlerBuilder;
use crate::agent_executor;
use std::sync::Arc;
struct DummyExec;
agent_executor!(DummyExec, |_ctx, _queue| async { Ok(()) });
let handler = Arc::new(RequestHandlerBuilder::new(DummyExec).build().unwrap());
let dispatcher = GrpcDispatcher::new(handler, GrpcConfig::default());
let _ = format!("{dispatcher:?}");
}
}