//! Contains all healthcheck based server utilities. use crate::pb::health_server::{Health, HealthServer}; use crate::pb::{HealthCheckRequest, HealthCheckResponse}; use crate::ServingStatus; use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; use tokio::sync::{watch, RwLock}; use tokio_stream::Stream; #[cfg(feature = "transport")] use tonic::server::NamedService; use tonic::{Request, Response, Status}; /// Creates a `HealthReporter` and a linked `HealthServer` pair. Together, /// these types can be used to serve the gRPC Health Checking service. /// /// A `HealthReporter` is used to update the state of gRPC services. /// /// A `HealthServer` is a Tonic gRPC server for the `grpc.health.v1.Health`, /// which can be added to a Tonic runtime using `add_service` on the runtime /// builder. pub fn health_reporter() -> (HealthReporter, HealthServer) { let reporter = HealthReporter::new(); let service = HealthService::new(reporter.statuses.clone()); let server = HealthServer::new(service); (reporter, server) } type StatusPair = (watch::Sender, watch::Receiver); /// A handle providing methods to update the health status of gRPC services. A /// `HealthReporter` is connected to a `HealthServer` which serves the statuses /// over the `grpc.health.v1.Health` service. #[derive(Clone, Debug)] pub struct HealthReporter { statuses: Arc>>, } impl HealthReporter { fn new() -> Self { // According to the gRPC Health Check specification, the empty service "" corresponds to the overall server health let server_status = ("".to_string(), watch::channel(ServingStatus::Serving)); let statuses = Arc::new(RwLock::new(HashMap::from([server_status]))); HealthReporter { statuses } } /// Sets the status of the service implemented by `S` to `Serving`. This notifies any watchers /// if there is a change in status. #[cfg(feature = "transport")] #[cfg_attr(docsrs, doc(cfg(feature = "transport")))] pub async fn set_serving(&mut self) where S: NamedService, { let service_name = ::NAME; self.set_service_status(service_name, ServingStatus::Serving) .await; } /// Sets the status of the service implemented by `S` to `NotServing`. This notifies any watchers /// if there is a change in status. #[cfg(feature = "transport")] #[cfg_attr(docsrs, doc(cfg(feature = "transport")))] pub async fn set_not_serving(&mut self) where S: NamedService, { let service_name = ::NAME; self.set_service_status(service_name, ServingStatus::NotServing) .await; } /// Sets the status of the service with `service_name` to `status`. This notifies any watchers /// if there is a change in status. pub async fn set_service_status(&mut self, service_name: S, status: ServingStatus) where S: AsRef, { let service_name = service_name.as_ref(); let mut writer = self.statuses.write().await; match writer.get(service_name) { Some((tx, _)) => { // We only ever hand out clones of the receiver, so the originally-created // receiver should always be present, only being dropped when clearing the // service status. Consequently, `tx.send` should not fail, making use // of `expect` here safe. tx.send(status).expect("channel should not be closed"); } None => { writer.insert(service_name.to_string(), watch::channel(status)); } }; } /// Clear the status of the given service. pub async fn clear_service_status(&mut self, service_name: &str) { let mut writer = self.statuses.write().await; let _ = writer.remove(service_name); } } /// A service providing implementations of gRPC health checking protocol. #[derive(Debug)] pub struct HealthService { statuses: Arc>>, } impl HealthService { fn new(services: Arc>>) -> Self { HealthService { statuses: services } } async fn service_health(&self, service_name: &str) -> Option { let reader = self.statuses.read().await; reader.get(service_name).map(|p| *p.1.borrow()) } } #[tonic::async_trait] impl Health for HealthService { async fn check( &self, request: Request, ) -> Result, Status> { let service_name = request.get_ref().service.as_str(); let status = self.service_health(service_name).await; match status { None => Err(Status::not_found("service not registered")), Some(status) => Ok(Response::new(HealthCheckResponse { status: crate::pb::health_check_response::ServingStatus::from(status) as i32, })), } } type WatchStream = Pin> + Send + 'static>>; async fn watch( &self, request: Request, ) -> Result, Status> { let service_name = request.get_ref().service.as_str(); let mut status_rx = match self.statuses.read().await.get(service_name) { None => return Err(Status::not_found("service not registered")), Some(pair) => pair.1.clone(), }; let output = async_stream::try_stream! { // yield the current value let status = crate::pb::health_check_response::ServingStatus::from(*status_rx.borrow()) as i32; yield HealthCheckResponse { status }; while let Ok(_) = status_rx.changed().await { let status = crate::pb::health_check_response::ServingStatus::from(*status_rx.borrow()) as i32; yield HealthCheckResponse { status }; } }; Ok(Response::new(Box::pin(output) as Self::WatchStream)) } } #[cfg(test)] mod tests { use crate::pb::health_server::Health; use crate::pb::HealthCheckRequest; use crate::server::{HealthReporter, HealthService}; use crate::ServingStatus; use tokio::sync::watch; use tokio_stream::StreamExt; use tonic::{Code, Request, Status}; fn assert_serving_status(wire: i32, expected: ServingStatus) { let expected = crate::pb::health_check_response::ServingStatus::from(expected) as i32; assert_eq!(wire, expected); } fn assert_grpc_status(wire: Option, expected: Code) { let wire = wire.expect("status is not None").code(); assert_eq!(wire, expected); } async fn make_test_service() -> (HealthReporter, HealthService) { let health_reporter = HealthReporter::new(); // insert test value { let mut statuses = health_reporter.statuses.write().await; statuses.insert( "TestService".to_string(), watch::channel(ServingStatus::Unknown), ); } let health_service = HealthService::new(health_reporter.statuses.clone()); (health_reporter, health_service) } #[tokio::test] async fn test_service_check() { let (mut reporter, service) = make_test_service().await; // Overall server health let resp = service .check(Request::new(HealthCheckRequest { service: "".to_string(), })) .await; assert!(resp.is_ok()); let resp = resp.unwrap().into_inner(); assert_serving_status(resp.status, ServingStatus::Serving); // Unregistered service let resp = service .check(Request::new(HealthCheckRequest { service: "Unregistered".to_string(), })) .await; assert!(resp.is_err()); assert_grpc_status(resp.err(), Code::NotFound); // Registered service - initial state let resp = service .check(Request::new(HealthCheckRequest { service: "TestService".to_string(), })) .await; assert!(resp.is_ok()); let resp = resp.unwrap().into_inner(); assert_serving_status(resp.status, ServingStatus::Unknown); // Registered service - updated state reporter .set_service_status("TestService", ServingStatus::Serving) .await; let resp = service .check(Request::new(HealthCheckRequest { service: "TestService".to_string(), })) .await; assert!(resp.is_ok()); let resp = resp.unwrap().into_inner(); assert_serving_status(resp.status, ServingStatus::Serving); } #[tokio::test] async fn test_service_watch() { let (mut reporter, service) = make_test_service().await; // Overall server health let resp = service .watch(Request::new(HealthCheckRequest { service: "".to_string(), })) .await; assert!(resp.is_ok()); let mut resp = resp.unwrap().into_inner(); let item = resp .next() .await .expect("streamed response is Some") .expect("response is ok"); assert_serving_status(item.status, ServingStatus::Serving); // Unregistered service let resp = service .watch(Request::new(HealthCheckRequest { service: "Unregistered".to_string(), })) .await; assert!(resp.is_err()); assert_grpc_status(resp.err(), Code::NotFound); // Registered service let resp = service .watch(Request::new(HealthCheckRequest { service: "TestService".to_string(), })) .await; assert!(resp.is_ok()); let mut resp = resp.unwrap().into_inner(); // Registered service - initial state let item = resp .next() .await .expect("streamed response is Some") .expect("response is ok"); assert_serving_status(item.status, ServingStatus::Unknown); // Registered service - updated state reporter .set_service_status("TestService", ServingStatus::NotServing) .await; let item = resp .next() .await .expect("streamed response is Some") .expect("response is ok"); assert_serving_status(item.status, ServingStatus::NotServing); // Registered service - updated state reporter .set_service_status("TestService", ServingStatus::Serving) .await; let item = resp .next() .await .expect("streamed response is Some") .expect("response is ok"); assert_serving_status(item.status, ServingStatus::Serving); // De-registered service reporter.clear_service_status("TestService").await; let item = resp.next().await; assert!(item.is_none()); } }