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 {
235 let serve_fns = Arc::new(self.serve_fns);
236
237 let start_fn = Arc::new(move |addr: SocketAddr, cancel: CancellationToken| {
238 let fns = serve_fns.clone();
239 tokio::spawn(async move {
240 let signal = Box::pin(cancel.clone().cancelled_owned());
243
244 if let Some(serve_fn) = fns.last() {
246 tracing::info!(addr = %addr, "gRPC server listening");
247 match serve_fn(addr, signal).await {
248 Ok(()) => tracing::info!(addr = %addr, "gRPC server stopped"),
249 Err(e) => tracing::error!(addr = %addr, error = %e, "gRPC server error"),
250 }
251 } else {
252 tracing::warn!(addr = %addr, "gRPC server has no services — waiting for cancel");
253 cancel.cancelled().await;
254 }
255 })
256 });
257
258 GrpcServer::new(self.name, self.config, start_fn)
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use std::convert::Infallible;
265 use std::future::{Ready, ready};
266 use std::task::{Context, Poll};
267
268 use http::{Request, Response};
269 use rskit_bootstrap::Component;
270 use rskit_errors::ErrorCode;
271 use tonic::body::Body;
272 use tower::Service;
273
274 use super::*;
275
276 #[tokio::test]
277 async fn builder_preserves_name_and_waits_for_cancel_without_services() {
278 let config = GrpcServerConfig::new("127.0.0.1", 50051);
279 let server = GrpcServerBuilder::new(config)
280 .with_name("api-grpc")
281 .with_reflection(b"not-a-descriptor")
282 .build();
283
284 assert_eq!(server.name(), "api-grpc");
285 assert!(!server.health().is_healthy());
286
287 server.start().await.expect("start no-service server");
288 assert!(server.health().is_healthy());
289 server.stop().await.expect("stop no-service server");
290 assert!(!server.health().is_healthy());
291 }
292
293 #[tokio::test]
294 async fn start_rejects_invalid_bind_address_before_spawning() {
295 let config = GrpcServerConfig::new("not a host", 50051);
296 let server = GrpcServerBuilder::new(config).build();
297
298 let error = server.start().await.expect_err("invalid address");
299
300 assert_eq!(error.code(), ErrorCode::Internal);
301 assert!(error.message().contains("invalid gRPC address"));
302 assert!(!server.health().is_healthy());
303 }
304
305 #[tokio::test]
306 async fn builder_runs_added_service_on_local_listener_until_cancelled() {
307 let config = GrpcServerConfig::new("127.0.0.1", 0);
308 let server = GrpcServerBuilder::new(config)
309 .with_name("local-grpc")
310 .add_service(EmptyGrpcService)
311 .build();
312
313 server.start().await.expect("start service server");
314 assert!(server.health().is_healthy());
315 tokio::time::sleep(Duration::from_millis(20)).await;
316 server.stop().await.expect("stop service server");
317 tokio::time::sleep(Duration::from_millis(20)).await;
318 }
319
320 #[tokio::test]
321 async fn builder_falls_back_when_reflection_descriptor_is_invalid() {
322 let config = GrpcServerConfig::new("127.0.0.1", 0);
323 let server = GrpcServerBuilder::new(config)
324 .with_reflection(b"not a protobuf file descriptor set")
325 .add_service(EmptyGrpcService)
326 .build();
327
328 server.start().await.expect("start service server");
329 tokio::time::sleep(Duration::from_millis(20)).await;
330 server.stop().await.expect("stop service server");
331 tokio::time::sleep(Duration::from_millis(20)).await;
332 }
333
334 #[tokio::test]
335 async fn builder_reports_missing_tls_files_from_service_task_without_panicking() {
336 let config = GrpcServerConfig {
337 host: "127.0.0.1".to_string(),
338 port: 0,
339 tls: Some(crate::config::TlsConfig {
340 cert_path: "missing-cert.pem".to_string(),
341 key_path: "missing-key.pem".to_string(),
342 }),
343 ..GrpcServerConfig::default()
344 };
345 let server = GrpcServerBuilder::new(config)
346 .add_service(EmptyGrpcService)
347 .build();
348
349 server.start().await.expect("spawn service task");
350 tokio::time::sleep(Duration::from_millis(20)).await;
351 server.stop().await.expect("stop service server");
352 }
353
354 fn testdata(name: &str) -> String {
355 format!("{}/testdata/{name}", env!("CARGO_MANIFEST_DIR"))
356 }
357
358 #[tokio::test]
359 async fn builder_serves_with_valid_tls_material() {
360 let config = GrpcServerConfig {
361 host: "127.0.0.1".to_string(),
362 port: 0,
363 tls: Some(crate::config::TlsConfig {
364 cert_path: testdata("cert.pem"),
365 key_path: testdata("key.pem"),
366 }),
367 ..GrpcServerConfig::default()
368 };
369 let server = GrpcServerBuilder::new(config)
370 .add_service(EmptyGrpcService)
371 .build();
372
373 server.start().await.expect("start tls service server");
374 tokio::time::sleep(Duration::from_millis(20)).await;
375 server.stop().await.expect("stop tls service server");
376 tokio::time::sleep(Duration::from_millis(20)).await;
377 }
378
379 #[tokio::test]
380 async fn builder_serves_with_valid_reflection_descriptor() {
381 let config = GrpcServerConfig::new("127.0.0.1", 0);
382 let server = GrpcServerBuilder::new(config)
383 .with_reflection(&[])
384 .add_service(EmptyGrpcService)
385 .build();
386
387 server
388 .start()
389 .await
390 .expect("start reflection service server");
391 tokio::time::sleep(Duration::from_millis(20)).await;
392 server.stop().await.expect("stop reflection service server");
393 tokio::time::sleep(Duration::from_millis(20)).await;
394 }
395
396 #[derive(Clone)]
397 struct EmptyGrpcService;
398
399 impl tonic::server::NamedService for EmptyGrpcService {
400 const NAME: &'static str = "test.Empty";
401 }
402
403 impl tonic::codegen::Service<Request<Body>> for EmptyGrpcService {
404 type Response = Response<Body>;
405 type Error = Infallible;
406 type Future = Ready<Result<Self::Response, Self::Error>>;
407
408 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
409 Poll::Ready(Ok(()))
410 }
411
412 fn call(&mut self, _req: Request<Body>) -> Self::Future {
413 ready(Ok(Response::new(Body::default())))
414 }
415 }
416
417 #[test]
418 fn empty_test_service_is_ready_and_returns_empty_body() {
419 let mut service = EmptyGrpcService;
420 let waker = std::task::Waker::noop();
421 let mut cx = Context::from_waker(waker);
422
423 assert!(matches!(service.poll_ready(&mut cx), Poll::Ready(Ok(()))));
424 let response = service
425 .call(Request::new(Body::default()))
426 .into_inner()
427 .unwrap();
428 assert_eq!(response.status(), http::StatusCode::OK);
429 }
430}