1use std::net::SocketAddr;
2use std::path::Path;
3use std::sync::Arc;
4use std::time::Duration;
5
6use rskit_errors::{AppError, AppResult, ErrorCode};
7use tokio_util::sync::CancellationToken;
8use tonic::transport::{Identity, Server, ServerTlsConfig};
9use tonic_reflection::server::Builder as ReflectionBuilder;
10use tower::Layer;
11
12use crate::component::GrpcServer;
13use crate::config::GrpcServerConfig;
14use crate::error_layer::ErrorLayer;
15
16pub(crate) type ServeFn = Arc<
31 dyn Fn(
32 SocketAddr,
33 std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
34 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = AppResult<()>> + Send>>
35 + Send
36 + Sync,
37>;
38
39pub struct GrpcServerBuilder {
65 name: String,
66 config: GrpcServerConfig,
67 serve_fns: Vec<ServeFn>,
69 reflection_descriptor: Option<Vec<u8>>,
71}
72
73impl GrpcServerBuilder {
74 pub fn new(config: GrpcServerConfig) -> Self {
76 Self {
77 name: "grpc-server".into(),
78 config,
79 serve_fns: Vec::new(),
80 reflection_descriptor: None,
81 }
82 }
83
84 #[must_use]
86 pub fn with_name(mut self, name: impl Into<String>) -> Self {
87 self.name = name.into();
88 self
89 }
90
91 #[must_use]
108 pub fn with_reflection(mut self, file_descriptor_set: &[u8]) -> Self {
109 self.reflection_descriptor = Some(file_descriptor_set.to_vec());
110 self
111 }
112
113 #[must_use]
120 pub fn add_service<S>(mut self, svc: S) -> Self
121 where
122 S: tonic::codegen::Service<
123 http::Request<tonic::body::Body>,
124 Response = http::Response<tonic::body::Body>,
125 Error = std::convert::Infallible,
126 > + tonic::server::NamedService
127 + Clone
128 + Send
129 + Sync
130 + 'static,
131 S::Future: Send + 'static,
132 {
133 let descriptor = self.reflection_descriptor.clone();
134 let tls = self.config.tls.clone();
135 let wrapped_svc = ErrorLayer::new().layer(svc);
137 let serve_fn: ServeFn = Arc::new(move |addr, signal| {
138 let s = wrapped_svc.clone();
139 let desc = descriptor.clone();
140 let tls = tls.clone();
141 Box::pin(async move {
142 let mut builder = Server::builder();
143 if let Some(tls) = &tls {
144 let cert = rskit_fs::async_io::file::read(Path::new(&tls.cert_path))
145 .await
146 .map_err(|error| {
147 AppError::new(
148 ErrorCode::InvalidInput,
149 format!(
150 "failed to read TLS certificate '{}': {error}",
151 tls.cert_path
152 ),
153 )
154 .with_cause(error)
155 })?;
156 let key = rskit_fs::async_io::file::read(Path::new(&tls.key_path))
157 .await
158 .map_err(|error| {
159 AppError::new(
160 ErrorCode::InvalidInput,
161 format!(
162 "failed to read TLS private key '{}': {error}",
163 tls.key_path
164 ),
165 )
166 .with_cause(error)
167 })?;
168 let tls_config = ServerTlsConfig::new()
169 .identity(Identity::from_pem(cert, key))
170 .ignore_client_order(true)
171 .timeout(Duration::from_secs(10));
172 builder = builder.tls_config(tls_config).map_err(|error| {
173 AppError::new(
174 ErrorCode::InvalidInput,
175 format!("invalid gRPC TLS configuration: {error}"),
176 )
177 .with_cause(error)
178 })?;
179 }
180 let router = builder.add_service(s);
181
182 if let Some(desc_bytes) = desc {
183 match ReflectionBuilder::configure()
184 .register_encoded_file_descriptor_set(&desc_bytes)
185 .build_v1()
186 {
187 Ok(refl_svc) => router
188 .add_service(refl_svc)
189 .serve_with_shutdown(addr, signal)
190 .await
191 .map_err(|error| {
192 AppError::new(
193 ErrorCode::Internal,
194 format!("gRPC server transport failed: {error}"),
195 )
196 .with_cause(error)
197 }),
198 Err(e) => {
199 tracing::error!(error = %e, "failed to build reflection service; serving without reflection");
200 router
201 .serve_with_shutdown(addr, signal)
202 .await
203 .map_err(|error| {
204 AppError::new(
205 ErrorCode::Internal,
206 format!("gRPC server transport failed: {error}"),
207 )
208 .with_cause(error)
209 })
210 }
211 }
212 } else {
213 router
214 .serve_with_shutdown(addr, signal)
215 .await
216 .map_err(|error| {
217 AppError::new(
218 ErrorCode::Internal,
219 format!("gRPC server transport failed: {error}"),
220 )
221 .with_cause(error)
222 })
223 }
224 })
225 });
226 self.serve_fns.push(serve_fn);
227 self
228 }
229
230 pub fn build(self) -> GrpcServer {
237 let serve_fns = Arc::new(self.serve_fns);
238
239 let start_fn = Arc::new(move |addr: SocketAddr, cancel: CancellationToken| {
240 let fns = serve_fns.clone();
241 tokio::spawn(async move {
242 let signal = Box::pin(cancel.clone().cancelled_owned());
245
246 if let Some(serve_fn) = fns.last() {
248 tracing::info!(addr = %addr, "gRPC server listening");
249 match serve_fn(addr, signal).await {
250 Ok(()) => tracing::info!(addr = %addr, "gRPC server stopped"),
251 Err(e) => tracing::error!(addr = %addr, error = %e, "gRPC server error"),
252 }
253 } else {
254 tracing::warn!(addr = %addr, "gRPC server has no services — waiting for cancel");
255 cancel.cancelled().await;
256 }
257 })
258 });
259
260 GrpcServer::new(self.name, self.config, start_fn)
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use std::convert::Infallible;
267 use std::future::{Ready, ready};
268 use std::task::{Context, Poll};
269
270 use http::{Request, Response};
271 use rskit_bootstrap::Component;
272 use rskit_errors::ErrorCode;
273 use tonic::body::Body;
274 use tower::Service;
275
276 use super::*;
277
278 #[tokio::test]
279 async fn builder_preserves_name_and_waits_for_cancel_without_services() {
280 let config = GrpcServerConfig::new("127.0.0.1", 50051);
281 let server = GrpcServerBuilder::new(config)
282 .with_name("api-grpc")
283 .with_reflection(b"not-a-descriptor")
284 .build();
285
286 assert_eq!(server.name(), "api-grpc");
287 assert!(!server.health().is_healthy());
288
289 server.start().await.expect("start no-service server");
290 assert!(server.health().is_healthy());
291 server.stop().await.expect("stop no-service server");
292 assert!(!server.health().is_healthy());
293 }
294
295 #[tokio::test]
296 async fn start_rejects_invalid_bind_address_before_spawning() {
297 let config = GrpcServerConfig::new("not a host", 50051);
298 let server = GrpcServerBuilder::new(config).build();
299
300 let error = server.start().await.expect_err("invalid address");
301
302 assert_eq!(error.code(), ErrorCode::Internal);
303 assert!(error.message().contains("invalid gRPC address"));
304 assert!(!server.health().is_healthy());
305 }
306
307 #[tokio::test]
308 async fn builder_runs_added_service_on_local_listener_until_cancelled() {
309 let config = GrpcServerConfig::new("127.0.0.1", 0);
310 let server = GrpcServerBuilder::new(config)
311 .with_name("local-grpc")
312 .add_service(EmptyGrpcService)
313 .build();
314
315 server.start().await.expect("start service server");
316 assert!(server.health().is_healthy());
317 tokio::time::sleep(Duration::from_millis(20)).await;
318 server.stop().await.expect("stop service server");
319 tokio::time::sleep(Duration::from_millis(20)).await;
320 }
321
322 #[tokio::test]
323 async fn builder_falls_back_when_reflection_descriptor_is_invalid() {
324 let config = GrpcServerConfig::new("127.0.0.1", 0);
325 let server = GrpcServerBuilder::new(config)
326 .with_reflection(b"not a protobuf file descriptor set")
327 .add_service(EmptyGrpcService)
328 .build();
329
330 server.start().await.expect("start service server");
331 tokio::time::sleep(Duration::from_millis(20)).await;
332 server.stop().await.expect("stop service server");
333 tokio::time::sleep(Duration::from_millis(20)).await;
334 }
335
336 #[tokio::test]
337 async fn builder_reports_missing_tls_files_from_service_task_without_panicking() {
338 let config = GrpcServerConfig {
339 host: "127.0.0.1".to_string(),
340 port: 0,
341 tls: Some(crate::config::TlsConfig {
342 cert_path: "missing-cert.pem".to_string(),
343 key_path: "missing-key.pem".to_string(),
344 }),
345 ..GrpcServerConfig::default()
346 };
347 let server = GrpcServerBuilder::new(config)
348 .add_service(EmptyGrpcService)
349 .build();
350
351 server.start().await.expect("spawn service task");
352 tokio::time::sleep(Duration::from_millis(20)).await;
353 server.stop().await.expect("stop service server");
354 }
355
356 #[derive(Clone)]
357 struct EmptyGrpcService;
358
359 impl tonic::server::NamedService for EmptyGrpcService {
360 const NAME: &'static str = "test.Empty";
361 }
362
363 impl tonic::codegen::Service<Request<Body>> for EmptyGrpcService {
364 type Response = Response<Body>;
365 type Error = Infallible;
366 type Future = Ready<Result<Self::Response, Self::Error>>;
367
368 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
369 Poll::Ready(Ok(()))
370 }
371
372 fn call(&mut self, _req: Request<Body>) -> Self::Future {
373 ready(Ok(Response::new(Body::default())))
374 }
375 }
376
377 #[test]
378 fn empty_test_service_is_ready_and_returns_empty_body() {
379 let mut service = EmptyGrpcService;
380 let waker = std::task::Waker::noop();
381 let mut cx = Context::from_waker(waker);
382
383 assert!(matches!(service.poll_ready(&mut cx), Poll::Ready(Ok(()))));
384 let response = service
385 .call(Request::new(Body::default()))
386 .into_inner()
387 .unwrap();
388 assert_eq!(response.status(), http::StatusCode::OK);
389 }
390}