1 //! Contains all healthcheck based server utilities.
2
3 use crate::pb::health_server::{Health, HealthServer};
4 use crate::pb::{HealthCheckRequest, HealthCheckResponse};
5 use crate::ServingStatus;
6 use std::collections::HashMap;
7 use std::fmt;
8 use std::sync::Arc;
9 use tokio::sync::{watch, RwLock};
10 use tokio_stream::Stream;
11 use tonic::{server::NamedService, Request, Response, Status};
12
13 /// Creates a `HealthReporter` and a linked `HealthServer` pair. Together,
14 /// these types can be used to serve the gRPC Health Checking service.
15 ///
16 /// A `HealthReporter` is used to update the state of gRPC services.
17 ///
18 /// A `HealthServer` is a Tonic gRPC server for the `grpc.health.v1.Health`,
19 /// which can be added to a Tonic runtime using `add_service` on the runtime
20 /// builder.
health_reporter() -> (HealthReporter, HealthServer<impl Health>)21 pub fn health_reporter() -> (HealthReporter, HealthServer<impl Health>) {
22 let reporter = HealthReporter::new();
23 let service = HealthService::new(reporter.statuses.clone());
24 let server = HealthServer::new(service);
25
26 (reporter, server)
27 }
28
29 type StatusPair = (watch::Sender<ServingStatus>, watch::Receiver<ServingStatus>);
30
31 /// A handle providing methods to update the health status of gRPC services. A
32 /// `HealthReporter` is connected to a `HealthServer` which serves the statuses
33 /// over the `grpc.health.v1.Health` service.
34 #[derive(Clone, Debug)]
35 pub struct HealthReporter {
36 statuses: Arc<RwLock<HashMap<String, StatusPair>>>,
37 }
38
39 impl HealthReporter {
new() -> Self40 fn new() -> Self {
41 // According to the gRPC Health Check specification, the empty service "" corresponds to the overall server health
42 let server_status = ("".to_string(), watch::channel(ServingStatus::Serving));
43
44 let statuses = Arc::new(RwLock::new(HashMap::from([server_status])));
45
46 HealthReporter { statuses }
47 }
48
49 /// Sets the status of the service implemented by `S` to `Serving`. This notifies any watchers
50 /// if there is a change in status.
set_serving<S>(&mut self) where S: NamedService,51 pub async fn set_serving<S>(&mut self)
52 where
53 S: NamedService,
54 {
55 let service_name = <S as NamedService>::NAME;
56 self.set_service_status(service_name, ServingStatus::Serving)
57 .await;
58 }
59
60 /// Sets the status of the service implemented by `S` to `NotServing`. This notifies any watchers
61 /// if there is a change in status.
set_not_serving<S>(&mut self) where S: NamedService,62 pub async fn set_not_serving<S>(&mut self)
63 where
64 S: NamedService,
65 {
66 let service_name = <S as NamedService>::NAME;
67 self.set_service_status(service_name, ServingStatus::NotServing)
68 .await;
69 }
70
71 /// Sets the status of the service with `service_name` to `status`. This notifies any watchers
72 /// if there is a change in status.
set_service_status<S>(&mut self, service_name: S, status: ServingStatus) where S: AsRef<str>,73 pub async fn set_service_status<S>(&mut self, service_name: S, status: ServingStatus)
74 where
75 S: AsRef<str>,
76 {
77 let service_name = service_name.as_ref();
78 let mut writer = self.statuses.write().await;
79 match writer.get(service_name) {
80 Some((tx, _)) => {
81 // We only ever hand out clones of the receiver, so the originally-created
82 // receiver should always be present, only being dropped when clearing the
83 // service status. Consequently, `tx.send` should not fail, making use
84 // of `expect` here safe.
85 tx.send(status).expect("channel should not be closed");
86 }
87 None => {
88 writer.insert(service_name.to_string(), watch::channel(status));
89 }
90 };
91 }
92
93 /// Clear the status of the given service.
clear_service_status(&mut self, service_name: &str)94 pub async fn clear_service_status(&mut self, service_name: &str) {
95 let mut writer = self.statuses.write().await;
96 let _ = writer.remove(service_name);
97 }
98 }
99
100 /// A service providing implementations of gRPC health checking protocol.
101 #[derive(Debug)]
102 pub struct HealthService {
103 statuses: Arc<RwLock<HashMap<String, StatusPair>>>,
104 }
105
106 impl HealthService {
new(services: Arc<RwLock<HashMap<String, StatusPair>>>) -> Self107 fn new(services: Arc<RwLock<HashMap<String, StatusPair>>>) -> Self {
108 HealthService { statuses: services }
109 }
110
service_health(&self, service_name: &str) -> Option<ServingStatus>111 async fn service_health(&self, service_name: &str) -> Option<ServingStatus> {
112 let reader = self.statuses.read().await;
113 reader.get(service_name).map(|p| *p.1.borrow())
114 }
115 }
116
117 #[tonic::async_trait]
118 impl Health for HealthService {
check( &self, request: Request<HealthCheckRequest>, ) -> Result<Response<HealthCheckResponse>, Status>119 async fn check(
120 &self,
121 request: Request<HealthCheckRequest>,
122 ) -> Result<Response<HealthCheckResponse>, Status> {
123 let service_name = request.get_ref().service.as_str();
124 let Some(status) = self.service_health(service_name).await else {
125 return Err(Status::not_found("service not registered"));
126 };
127
128 Ok(Response::new(HealthCheckResponse::new(status)))
129 }
130
131 type WatchStream = WatchStream;
132
watch( &self, request: Request<HealthCheckRequest>, ) -> Result<Response<Self::WatchStream>, Status>133 async fn watch(
134 &self,
135 request: Request<HealthCheckRequest>,
136 ) -> Result<Response<Self::WatchStream>, Status> {
137 let service_name = request.get_ref().service.as_str();
138 let status_rx = match self.statuses.read().await.get(service_name) {
139 Some((_tx, rx)) => rx.clone(),
140 None => return Err(Status::not_found("service not registered")),
141 };
142
143 Ok(Response::new(WatchStream::new(status_rx)))
144 }
145 }
146
147 /// A watch stream for the health service.
148 pub struct WatchStream {
149 inner: tokio_stream::wrappers::WatchStream<ServingStatus>,
150 }
151
152 impl WatchStream {
new(status_rx: watch::Receiver<ServingStatus>) -> Self153 fn new(status_rx: watch::Receiver<ServingStatus>) -> Self {
154 let inner = tokio_stream::wrappers::WatchStream::new(status_rx);
155 Self { inner }
156 }
157 }
158
159 impl Stream for WatchStream {
160 type Item = Result<HealthCheckResponse, Status>;
161
poll_next( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll<Option<Self::Item>>162 fn poll_next(
163 mut self: std::pin::Pin<&mut Self>,
164 cx: &mut std::task::Context<'_>,
165 ) -> std::task::Poll<Option<Self::Item>> {
166 std::pin::Pin::new(&mut self.inner)
167 .poll_next(cx)
168 .map(|opt| opt.map(|status| Ok(HealthCheckResponse::new(status))))
169 }
170 }
171
172 impl fmt::Debug for WatchStream {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 f.debug_struct("WatchStream").finish()
175 }
176 }
177
178 impl HealthCheckResponse {
new(status: ServingStatus) -> Self179 fn new(status: ServingStatus) -> Self {
180 let status = crate::pb::health_check_response::ServingStatus::from(status) as i32;
181 Self { status }
182 }
183 }
184
185 #[cfg(test)]
186 mod tests {
187 use crate::pb::health_server::Health;
188 use crate::pb::HealthCheckRequest;
189 use crate::server::{HealthReporter, HealthService};
190 use crate::ServingStatus;
191 use tokio::sync::watch;
192 use tokio_stream::StreamExt;
193 use tonic::{Code, Request, Status};
194
assert_serving_status(wire: i32, expected: ServingStatus)195 fn assert_serving_status(wire: i32, expected: ServingStatus) {
196 let expected = crate::pb::health_check_response::ServingStatus::from(expected) as i32;
197 assert_eq!(wire, expected);
198 }
199
assert_grpc_status(wire: Option<Status>, expected: Code)200 fn assert_grpc_status(wire: Option<Status>, expected: Code) {
201 let wire = wire.expect("status is not None").code();
202 assert_eq!(wire, expected);
203 }
204
make_test_service() -> (HealthReporter, HealthService)205 async fn make_test_service() -> (HealthReporter, HealthService) {
206 let health_reporter = HealthReporter::new();
207
208 // insert test value
209 {
210 let mut statuses = health_reporter.statuses.write().await;
211 statuses.insert(
212 "TestService".to_string(),
213 watch::channel(ServingStatus::Unknown),
214 );
215 }
216
217 let health_service = HealthService::new(health_reporter.statuses.clone());
218 (health_reporter, health_service)
219 }
220
221 #[tokio::test]
test_service_check()222 async fn test_service_check() {
223 let (mut reporter, service) = make_test_service().await;
224
225 // Overall server health
226 let resp = service
227 .check(Request::new(HealthCheckRequest {
228 service: "".to_string(),
229 }))
230 .await;
231 assert!(resp.is_ok());
232 let resp = resp.unwrap().into_inner();
233 assert_serving_status(resp.status, ServingStatus::Serving);
234
235 // Unregistered service
236 let resp = service
237 .check(Request::new(HealthCheckRequest {
238 service: "Unregistered".to_string(),
239 }))
240 .await;
241 assert!(resp.is_err());
242 assert_grpc_status(resp.err(), Code::NotFound);
243
244 // Registered service - initial state
245 let resp = service
246 .check(Request::new(HealthCheckRequest {
247 service: "TestService".to_string(),
248 }))
249 .await;
250 assert!(resp.is_ok());
251 let resp = resp.unwrap().into_inner();
252 assert_serving_status(resp.status, ServingStatus::Unknown);
253
254 // Registered service - updated state
255 reporter
256 .set_service_status("TestService", ServingStatus::Serving)
257 .await;
258 let resp = service
259 .check(Request::new(HealthCheckRequest {
260 service: "TestService".to_string(),
261 }))
262 .await;
263 assert!(resp.is_ok());
264 let resp = resp.unwrap().into_inner();
265 assert_serving_status(resp.status, ServingStatus::Serving);
266 }
267
268 #[tokio::test]
test_service_watch()269 async fn test_service_watch() {
270 let (mut reporter, service) = make_test_service().await;
271
272 // Overall server health
273 let resp = service
274 .watch(Request::new(HealthCheckRequest {
275 service: "".to_string(),
276 }))
277 .await;
278 assert!(resp.is_ok());
279 let mut resp = resp.unwrap().into_inner();
280 let item = resp
281 .next()
282 .await
283 .expect("streamed response is Some")
284 .expect("response is ok");
285 assert_serving_status(item.status, ServingStatus::Serving);
286
287 // Unregistered service
288 let resp = service
289 .watch(Request::new(HealthCheckRequest {
290 service: "Unregistered".to_string(),
291 }))
292 .await;
293 assert!(resp.is_err());
294 assert_grpc_status(resp.err(), Code::NotFound);
295
296 // Registered service
297 let resp = service
298 .watch(Request::new(HealthCheckRequest {
299 service: "TestService".to_string(),
300 }))
301 .await;
302 assert!(resp.is_ok());
303 let mut resp = resp.unwrap().into_inner();
304
305 // Registered service - initial state
306 let item = resp
307 .next()
308 .await
309 .expect("streamed response is Some")
310 .expect("response is ok");
311 assert_serving_status(item.status, ServingStatus::Unknown);
312
313 // Registered service - updated state
314 reporter
315 .set_service_status("TestService", ServingStatus::NotServing)
316 .await;
317
318 let item = resp
319 .next()
320 .await
321 .expect("streamed response is Some")
322 .expect("response is ok");
323 assert_serving_status(item.status, ServingStatus::NotServing);
324
325 // Registered service - updated state
326 reporter
327 .set_service_status("TestService", ServingStatus::Serving)
328 .await;
329 let item = resp
330 .next()
331 .await
332 .expect("streamed response is Some")
333 .expect("response is ok");
334 assert_serving_status(item.status, ServingStatus::Serving);
335
336 // De-registered service
337 reporter.clear_service_status("TestService").await;
338 let item = resp.next().await;
339 assert!(item.is_none());
340 }
341 }
342