use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use rskit_errors::{AppError, AppResult, ErrorCode};
use tokio_util::sync::CancellationToken;
use tonic::transport::{Identity, Server, ServerTlsConfig};
use tonic_reflection::server::Builder as ReflectionBuilder;
use tower::Layer;
use crate::component::GrpcServer;
use crate::config::GrpcServerConfig;
use crate::error_layer::ErrorLayer;
pub(crate) type ServeFn = Arc<
dyn Fn(
SocketAddr,
std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = AppResult<()>> + Send>>
+ Send
+ Sync,
>;
pub struct GrpcServerBuilder {
name: String,
config: GrpcServerConfig,
serve_fns: Vec<ServeFn>,
reflection_descriptor: Option<Vec<u8>>,
}
impl GrpcServerBuilder {
pub fn new(config: GrpcServerConfig) -> Self {
Self {
name: "grpc-server".into(),
config,
serve_fns: Vec::new(),
reflection_descriptor: None,
}
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
#[must_use]
pub fn with_reflection(mut self, file_descriptor_set: &[u8]) -> Self {
self.reflection_descriptor = Some(file_descriptor_set.to_vec());
self
}
#[must_use]
pub fn add_service<S>(mut self, svc: S) -> Self
where
S: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<tonic::body::Body>,
Error = std::convert::Infallible,
> + tonic::server::NamedService
+ Clone
+ Send
+ Sync
+ 'static,
S::Future: Send + 'static,
{
let descriptor = self.reflection_descriptor.clone();
let tls = self.config.tls.clone();
let wrapped_svc = ErrorLayer::new().layer(svc);
let serve_fn: ServeFn = Arc::new(move |addr, signal| {
let s = wrapped_svc.clone();
let desc = descriptor.clone();
let tls = tls.clone();
Box::pin(async move {
let mut builder = Server::builder();
if let Some(tls) = &tls {
let cert = rskit_fs::async_io::file::read(Path::new(&tls.cert_path))
.await
.map_err(|error| {
AppError::new(
ErrorCode::InvalidInput,
format!(
"failed to read TLS certificate '{}': {error}",
tls.cert_path
),
)
.with_cause(error)
})?;
let key = rskit_fs::async_io::file::read(Path::new(&tls.key_path))
.await
.map_err(|error| {
AppError::new(
ErrorCode::InvalidInput,
format!(
"failed to read TLS private key '{}': {error}",
tls.key_path
),
)
.with_cause(error)
})?;
let tls_config = ServerTlsConfig::new()
.identity(Identity::from_pem(cert, key))
.ignore_client_order(true)
.timeout(Duration::from_secs(10));
builder = builder.tls_config(tls_config).map_err(|error| {
AppError::new(
ErrorCode::InvalidInput,
format!("invalid gRPC TLS configuration: {error}"),
)
.with_cause(error)
})?;
}
let router = builder.add_service(s);
if let Some(desc_bytes) = desc {
match ReflectionBuilder::configure()
.register_encoded_file_descriptor_set(&desc_bytes)
.build_v1()
{
Ok(refl_svc) => router
.add_service(refl_svc)
.serve_with_shutdown(addr, signal)
.await
.map_err(|error| {
AppError::new(
ErrorCode::Internal,
format!("gRPC server transport failed: {error}"),
)
.with_cause(error)
}),
Err(e) => {
tracing::error!(error = %e, "failed to build reflection service; serving without reflection");
router
.serve_with_shutdown(addr, signal)
.await
.map_err(|error| {
AppError::new(
ErrorCode::Internal,
format!("gRPC server transport failed: {error}"),
)
.with_cause(error)
})
}
}
} else {
router
.serve_with_shutdown(addr, signal)
.await
.map_err(|error| {
AppError::new(
ErrorCode::Internal,
format!("gRPC server transport failed: {error}"),
)
.with_cause(error)
})
}
})
});
self.serve_fns.push(serve_fn);
self
}
pub fn build(self) -> GrpcServer {
let serve_fns = Arc::new(self.serve_fns);
let start_fn = Arc::new(move |addr: SocketAddr, cancel: CancellationToken| {
let fns = serve_fns.clone();
tokio::spawn(async move {
let signal = Box::pin(cancel.clone().cancelled_owned());
if let Some(serve_fn) = fns.last() {
tracing::info!(addr = %addr, "gRPC server listening");
match serve_fn(addr, signal).await {
Ok(()) => tracing::info!(addr = %addr, "gRPC server stopped"),
Err(e) => tracing::error!(addr = %addr, error = %e, "gRPC server error"),
}
} else {
tracing::warn!(addr = %addr, "gRPC server has no services — waiting for cancel");
cancel.cancelled().await;
}
})
});
GrpcServer::new(self.name, self.config, start_fn)
}
}
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use std::future::{Ready, ready};
use std::task::{Context, Poll};
use http::{Request, Response};
use rskit_bootstrap::Component;
use rskit_errors::ErrorCode;
use tonic::body::Body;
use tower::Service;
use super::*;
#[tokio::test]
async fn builder_preserves_name_and_waits_for_cancel_without_services() {
let config = GrpcServerConfig::new("127.0.0.1", 50051);
let server = GrpcServerBuilder::new(config)
.with_name("api-grpc")
.with_reflection(b"not-a-descriptor")
.build();
assert_eq!(server.name(), "api-grpc");
assert!(!server.health().is_healthy());
server.start().await.expect("start no-service server");
assert!(server.health().is_healthy());
server.stop().await.expect("stop no-service server");
assert!(!server.health().is_healthy());
}
#[tokio::test]
async fn start_rejects_invalid_bind_address_before_spawning() {
let config = GrpcServerConfig::new("not a host", 50051);
let server = GrpcServerBuilder::new(config).build();
let error = server.start().await.expect_err("invalid address");
assert_eq!(error.code(), ErrorCode::Internal);
assert!(error.message().contains("invalid gRPC address"));
assert!(!server.health().is_healthy());
}
#[tokio::test]
async fn builder_runs_added_service_on_local_listener_until_cancelled() {
let config = GrpcServerConfig::new("127.0.0.1", 0);
let server = GrpcServerBuilder::new(config)
.with_name("local-grpc")
.add_service(EmptyGrpcService)
.build();
server.start().await.expect("start service server");
assert!(server.health().is_healthy());
tokio::time::sleep(Duration::from_millis(20)).await;
server.stop().await.expect("stop service server");
tokio::time::sleep(Duration::from_millis(20)).await;
}
#[tokio::test]
async fn builder_falls_back_when_reflection_descriptor_is_invalid() {
let config = GrpcServerConfig::new("127.0.0.1", 0);
let server = GrpcServerBuilder::new(config)
.with_reflection(b"not a protobuf file descriptor set")
.add_service(EmptyGrpcService)
.build();
server.start().await.expect("start service server");
tokio::time::sleep(Duration::from_millis(20)).await;
server.stop().await.expect("stop service server");
tokio::time::sleep(Duration::from_millis(20)).await;
}
#[tokio::test]
async fn builder_reports_missing_tls_files_from_service_task_without_panicking() {
let config = GrpcServerConfig {
host: "127.0.0.1".to_string(),
port: 0,
tls: Some(crate::config::TlsConfig {
cert_path: "missing-cert.pem".to_string(),
key_path: "missing-key.pem".to_string(),
}),
..GrpcServerConfig::default()
};
let server = GrpcServerBuilder::new(config)
.add_service(EmptyGrpcService)
.build();
server.start().await.expect("spawn service task");
tokio::time::sleep(Duration::from_millis(20)).await;
server.stop().await.expect("stop service server");
}
#[derive(Clone)]
struct EmptyGrpcService;
impl tonic::server::NamedService for EmptyGrpcService {
const NAME: &'static str = "test.Empty";
}
impl tonic::codegen::Service<Request<Body>> for EmptyGrpcService {
type Response = Response<Body>;
type Error = Infallible;
type Future = Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<Body>) -> Self::Future {
ready(Ok(Response::new(Body::default())))
}
}
#[test]
fn empty_test_service_is_ready_and_returns_empty_body() {
let mut service = EmptyGrpcService;
let waker = std::task::Waker::noop();
let mut cx = Context::from_waker(waker);
assert!(matches!(service.poll_ready(&mut cx), Poll::Ready(Ok(()))));
let response = service
.call(Request::new(Body::default()))
.into_inner()
.unwrap();
assert_eq!(response.status(), http::StatusCode::OK);
}
}