feat(health): Add tonic-health server impl
This commit adds a new crate `tonic-health` which implements the [standard GRPC Health Checking][checking] protocol. Currently there is only a server implementation, though others have alluded in the discussion in #135 that client implementations exist which could also be imported as necessary. A example server has also been added - once the client work is done a client for this should be added also. [checking]: https://github.com/grpc/grpc/blob/master/doc/health-checking.md Fixes #135.
This commit is contained in:
@@ -2,9 +2,12 @@
|
|||||||
members = [
|
members = [
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
|
"tonic-health",
|
||||||
|
|
||||||
# Non-published crates
|
# Non-published crates
|
||||||
"examples",
|
"examples",
|
||||||
"interop",
|
"interop",
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
"tests/included_service",
|
"tests/included_service",
|
||||||
"tests/same_name",
|
"tests/same_name",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ contains the tools to build clients and servers from [`protobuf`] definitions.
|
|||||||
- Load balancing
|
- Load balancing
|
||||||
- Custom metadata
|
- Custom metadata
|
||||||
- Authentication
|
- Authentication
|
||||||
|
- Health Checking
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -67,6 +68,8 @@ question. If that doesn't work, try opening an [issue] with the question.
|
|||||||
- [`tonic`](https://github.com/hyperium/tonic/tree/master/tonic): Generic gRPC and HTTP/2 client/server
|
- [`tonic`](https://github.com/hyperium/tonic/tree/master/tonic): Generic gRPC and HTTP/2 client/server
|
||||||
implementation.
|
implementation.
|
||||||
- [`tonic-build`](https://github.com/hyperium/tonic/tree/master/tonic-build): [`prost`] based service codegen.
|
- [`tonic-build`](https://github.com/hyperium/tonic/tree/master/tonic-build): [`prost`] based service codegen.
|
||||||
|
- [`tonic-health`](https://github.com/hyperium/tonic/tree/master/tonic-health): Implementation of the standard [gRPC
|
||||||
|
health checking service][healthcheck]. Also serves as an example of both unary and response streaming.
|
||||||
- [`examples`](https://github.com/hyperium/tonic/tree/master/examples): Example gRPC implementations showing off
|
- [`examples`](https://github.com/hyperium/tonic/tree/master/examples): Example gRPC implementations showing off
|
||||||
tls, load balancing and bi-directional streaming.
|
tls, load balancing and bi-directional streaming.
|
||||||
- [`interop`](https://github.com/hyperium/tonic/tree/master/interop): Interop tests implementation.
|
- [`interop`](https://github.com/hyperium/tonic/tree/master/interop): Interop tests implementation.
|
||||||
@@ -105,3 +108,4 @@ terms or conditions.
|
|||||||
[Chat]: https://discord.gg/6yGkFeN
|
[Chat]: https://discord.gg/6yGkFeN
|
||||||
[routeguide-tutorial]: https://github.com/hyperium/tonic/blob/master/examples/routeguide-tutorial.md
|
[routeguide-tutorial]: https://github.com/hyperium/tonic/blob/master/examples/routeguide-tutorial.md
|
||||||
[helloworld-tutorial]: https://github.com/hyperium/tonic/blob/master/examples/helloworld-tutorial.md
|
[helloworld-tutorial]: https://github.com/hyperium/tonic/blob/master/examples/helloworld-tutorial.md
|
||||||
|
[healthcheck]: https://github.com/grpc/grpc/blob/master/doc/health-checking.md
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ path = "src/hyper_warp/client.rs"
|
|||||||
name = "hyper-warp-server"
|
name = "hyper-warp-server"
|
||||||
path = "src/hyper_warp/server.rs"
|
path = "src/hyper_warp/server.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "health-server"
|
||||||
|
path = "src/health/server.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tonic = { path = "../tonic", features = ["tls", "data-prost"] }
|
tonic = { path = "../tonic", features = ["tls", "data-prost"] }
|
||||||
prost = "0.6"
|
prost = "0.6"
|
||||||
@@ -126,6 +130,8 @@ warp = { version = "0.2", default-features = false }
|
|||||||
http = "0.2"
|
http = "0.2"
|
||||||
http-body = "0.3"
|
http-body = "0.3"
|
||||||
pin-project = "0.4"
|
pin-project = "0.4"
|
||||||
|
# Health example
|
||||||
|
tonic-health = { path = "../tonic-health" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-build = { path = "../tonic-build", features = ["prost"] }
|
tonic-build = { path = "../tonic-build", features = ["prost"] }
|
||||||
|
|||||||
@@ -72,6 +72,13 @@ $ cargo run --bin tls-client
|
|||||||
$ cargo run --bin tls-server
|
$ cargo run --bin tls-server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Health Checking
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo run --bin health-server
|
||||||
|
```
|
||||||
|
|
||||||
### Notes:
|
### Notes:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use tonic::{transport::Server, Request, Response, Status};
|
||||||
|
|
||||||
|
use hello_world::greeter_server::{Greeter, GreeterServer};
|
||||||
|
use hello_world::{HelloReply, HelloRequest};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::delay_for;
|
||||||
|
use tonic_health::server::HealthReporter;
|
||||||
|
|
||||||
|
pub mod hello_world {
|
||||||
|
tonic::include_proto!("helloworld");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MyGreeter {}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl Greeter for MyGreeter {
|
||||||
|
async fn say_hello(
|
||||||
|
&self,
|
||||||
|
request: Request<HelloRequest>,
|
||||||
|
) -> Result<Response<HelloReply>, Status> {
|
||||||
|
println!("Got a request from {:?}", request.remote_addr());
|
||||||
|
|
||||||
|
let reply = hello_world::HelloReply {
|
||||||
|
message: format!("Hello {}!", request.into_inner().name),
|
||||||
|
};
|
||||||
|
Ok(Response::new(reply))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function (somewhat improbably) flips the status of a service every second, in order
|
||||||
|
/// that the effect of `tonic_health::HealthReporter::watch` can be easily observed.
|
||||||
|
async fn twiddle_service_status(mut reporter: HealthReporter) {
|
||||||
|
let mut iter = 0u64;
|
||||||
|
loop {
|
||||||
|
iter += 1;
|
||||||
|
delay_for(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
if iter % 2 == 0 {
|
||||||
|
reporter.set_serving::<GreeterServer<MyGreeter>>().await;
|
||||||
|
} else {
|
||||||
|
reporter.set_not_serving::<GreeterServer<MyGreeter>>().await;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
|
||||||
|
health_reporter
|
||||||
|
.set_serving::<GreeterServer<MyGreeter>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
tokio::spawn(twiddle_service_status(health_reporter.clone()));
|
||||||
|
|
||||||
|
let addr = "[::1]:50051".parse().unwrap();
|
||||||
|
let greeter = MyGreeter::default();
|
||||||
|
|
||||||
|
println!("HealthServer + GreeterServer listening on {}", addr);
|
||||||
|
|
||||||
|
Server::builder()
|
||||||
|
.add_service(health_service)
|
||||||
|
.add_service(GreeterServer::new(greeter))
|
||||||
|
.serve(addr)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "tonic-health"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["James Nugent <[email protected]>"]
|
||||||
|
edition = "2018"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/hyperium/tonic"
|
||||||
|
homepage = "https://github.com/hyperium/tonic"
|
||||||
|
description = """
|
||||||
|
Health Checking module of `tonic` gRPC implementation.
|
||||||
|
"""
|
||||||
|
readme = "README.md"
|
||||||
|
categories = ["network-programming", "asynchronous"]
|
||||||
|
keywords = ["rpc", "grpc", "async", "healthcheck"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
transport = ["tonic/transport"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-stream = "0.2"
|
||||||
|
tokio = { version = "0.2", features = ["sync", "stream"] }
|
||||||
|
tonic = { path = "../tonic", features = ["codegen", "data-prost"] }
|
||||||
|
bytes = "0.5"
|
||||||
|
prost = "0.6"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "0.2", features = ["rt-core", "macros"]}
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tonic-build = { path = "../tonic-build" }
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tonic_build::configure()
|
||||||
|
.build_server(true)
|
||||||
|
.build_client(false)
|
||||||
|
.format(true)
|
||||||
|
.compile(&["proto/health.proto"], &["proto/"])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package grpc.health.v1;
|
||||||
|
|
||||||
|
message HealthCheckRequest {
|
||||||
|
string service = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HealthCheckResponse {
|
||||||
|
enum ServingStatus {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
SERVING = 1;
|
||||||
|
NOT_SERVING = 2;
|
||||||
|
}
|
||||||
|
ServingStatus status = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Health {
|
||||||
|
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||||
|
|
||||||
|
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
mod proto {
|
||||||
|
tonic::include_proto!("grpc.health.v1");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod server;
|
||||||
|
|
||||||
|
/// An enumeration of values representing gRPC service health.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
pub enum ServingStatus {
|
||||||
|
Unknown,
|
||||||
|
Serving,
|
||||||
|
NotServing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ServingStatus {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ServingStatus::Unknown => f.write_str("Unknown"),
|
||||||
|
ServingStatus::Serving => f.write_str("Serving"),
|
||||||
|
ServingStatus::NotServing => f.write_str("NotServing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ServingStatus> for proto::health_check_response::ServingStatus {
|
||||||
|
fn from(s: ServingStatus) -> Self {
|
||||||
|
match s {
|
||||||
|
ServingStatus::Unknown => proto::health_check_response::ServingStatus::Unknown,
|
||||||
|
ServingStatus::Serving => proto::health_check_response::ServingStatus::Serving,
|
||||||
|
ServingStatus::NotServing => proto::health_check_response::ServingStatus::NotServing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
use crate::proto::health_server::{Health, HealthServer};
|
||||||
|
use crate::proto::{HealthCheckRequest, HealthCheckResponse};
|
||||||
|
use crate::ServingStatus;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::stream::Stream;
|
||||||
|
use tokio::sync::{watch, RwLock};
|
||||||
|
#[cfg(feature = "transport")]
|
||||||
|
use tonic::transport::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<impl Health>) {
|
||||||
|
let reporter = HealthReporter::new();
|
||||||
|
let service = HealthService::new(reporter.statuses.clone());
|
||||||
|
let server = HealthServer::new(service);
|
||||||
|
|
||||||
|
(reporter, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusPair = (watch::Sender<ServingStatus>, watch::Receiver<ServingStatus>);
|
||||||
|
|
||||||
|
/// 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<RwLock<HashMap<String, StatusPair>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HealthReporter {
|
||||||
|
fn new() -> Self {
|
||||||
|
let statuses = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
|
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")]
|
||||||
|
pub async fn set_serving<S>(&mut self)
|
||||||
|
where
|
||||||
|
S: NamedService,
|
||||||
|
{
|
||||||
|
let service_name = <S as NamedService>::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")]
|
||||||
|
pub async fn set_not_serving<S>(&mut self)
|
||||||
|
where
|
||||||
|
S: NamedService,
|
||||||
|
{
|
||||||
|
let service_name = <S as NamedService>::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<S>(&mut self, service_name: S, status: ServingStatus)
|
||||||
|
where
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
|
let service_name = service_name.as_ref();
|
||||||
|
let mut writer = self.statuses.write().await;
|
||||||
|
match writer.get(service_name) {
|
||||||
|
None => {
|
||||||
|
let _ = writer.insert(service_name.to_string(), watch::channel(status));
|
||||||
|
}
|
||||||
|
Some((tx, rx)) => {
|
||||||
|
let mut rx = rx.clone();
|
||||||
|
if rx.recv().await == Some(status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.broadcast` should not fail, making use
|
||||||
|
// of `expect` here safe.
|
||||||
|
tx.broadcast(status).expect("channel should not be closed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HealthService {
|
||||||
|
statuses: Arc<RwLock<HashMap<String, StatusPair>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HealthService {
|
||||||
|
fn new(services: Arc<RwLock<HashMap<String, StatusPair>>>) -> Self {
|
||||||
|
HealthService { statuses: services }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn service_health(&self, service_name: &str) -> Option<ServingStatus> {
|
||||||
|
let reader = self.statuses.read().await;
|
||||||
|
match reader.get(service_name).map(|p| p.1.clone()) {
|
||||||
|
None => None,
|
||||||
|
Some(mut receiver) => receiver.recv().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl Health for HealthService {
|
||||||
|
async fn check(
|
||||||
|
&self,
|
||||||
|
request: Request<HealthCheckRequest>,
|
||||||
|
) -> Result<Response<HealthCheckResponse>, 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::proto::health_check_response::ServingStatus::from(status) as i32,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WatchStream =
|
||||||
|
Pin<Box<dyn Stream<Item = Result<HealthCheckResponse, Status>> + Send + Sync + 'static>>;
|
||||||
|
|
||||||
|
async fn watch(
|
||||||
|
&self,
|
||||||
|
request: Request<HealthCheckRequest>,
|
||||||
|
) -> Result<Response<Self::WatchStream>, 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! {
|
||||||
|
while let Some(status) = status_rx.recv().await {
|
||||||
|
yield HealthCheckResponse{
|
||||||
|
status: crate::proto::health_check_response::ServingStatus::from(status) as i32,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Response::new(Box::pin(output) as Self::WatchStream))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::proto::health_server::Health;
|
||||||
|
use crate::proto::HealthCheckRequest;
|
||||||
|
use crate::server::{HealthReporter, HealthService};
|
||||||
|
use crate::ServingStatus;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::stream::StreamExt;
|
||||||
|
use tokio::sync::{watch, RwLock};
|
||||||
|
use tonic::{Code, Request, Status};
|
||||||
|
|
||||||
|
fn assert_serving_status(wire: i32, expected: ServingStatus) {
|
||||||
|
let expected = crate::proto::health_check_response::ServingStatus::from(expected) as i32;
|
||||||
|
assert_eq!(wire, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_grpc_status(wire: Option<Status>, expected: Code) {
|
||||||
|
let wire = wire.expect("status is not None").code();
|
||||||
|
assert_eq!(wire, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn make_test_service() -> (HealthReporter, HealthService) {
|
||||||
|
let state = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
state.write().await.insert(
|
||||||
|
"TestService".to_string(),
|
||||||
|
watch::channel(ServingStatus::Unknown),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
HealthReporter {
|
||||||
|
statuses: state.clone(),
|
||||||
|
},
|
||||||
|
HealthService::new(state.clone()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_service_check() {
|
||||||
|
let (mut reporter, service) = make_test_service().await;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user