From bb37fe8d4fa6b8d333929ac981b66f663ce25177 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Thu, 4 Aug 2022 15:47:40 +0100 Subject: [PATCH 1/3] tonic-health: commit generated code rather than generating at build time This saves users from needing protoc available on their path if they're not generating other tonic code at build time, either. The generated modules can be regenerated by enabling the `gen-proto` feature, which will trigger the relevant part of the build script. --- tonic-health/Cargo.toml | 6 +- tonic-health/build.rs | 14 +- tonic-health/src/generated/grpc.health.v1.rs | 379 ++++++++++++++++++ tonic-health/src/generated/grpc_health_v1.bin | Bin 0 -> 2875 bytes tonic-health/src/lib.rs | 6 +- 5 files changed, 395 insertions(+), 10 deletions(-) create mode 100644 tonic-health/src/generated/grpc.health.v1.rs create mode 100644 tonic-health/src/generated/grpc_health_v1.bin diff --git a/tonic-health/Cargo.toml b/tonic-health/Cargo.toml index 5b89de287..2041e43d1 100644 --- a/tonic-health/Cargo.toml +++ b/tonic-health/Cargo.toml @@ -16,7 +16,9 @@ version = "0.7.0" [features] default = ["transport"] -transport = ["tonic/transport", "tonic-build/transport"] +transport = ["tonic/transport"] +# generate gRPC code from `.proto`(s) +gen-proto = ["dep:tonic-build"] [dependencies] async-stream = "0.3" @@ -30,4 +32,4 @@ tonic = {version = "0.8", path = "../tonic", features = ["codegen", "prost"]} tokio = {version = "1.0", features = ["rt-multi-thread", "macros"]} [build-dependencies] -tonic-build = {version = "0.8", path = "../tonic-build", features = ["prost"]} +tonic-build = {version = "0.8", path = "../tonic-build", features = ["prost"], optional = true} diff --git a/tonic-health/build.rs b/tonic-health/build.rs index 5b04330ef..1b9933429 100644 --- a/tonic-health/build.rs +++ b/tonic-health/build.rs @@ -1,11 +1,15 @@ -use std::env; -use std::path::PathBuf; +// This build file is used to generate the code as a one-off, +// but is only rerun with the `gen-proto` feature enabled. +// This simplifies the build process for this crate by not requiring +// users to have protoc available. fn main() -> Result<(), Box> { - let grpc_health_v1_descriptor_set_path: PathBuf = - PathBuf::from(env::var("OUT_DIR").unwrap()).join("grpc_health_v1.bin"); + #[cfg(feature = "gen-proto")] tonic_build::configure() - .file_descriptor_set_path(grpc_health_v1_descriptor_set_path) + .file_descriptor_set_path( + std::path::PathBuf::from("src/generated").join("grpc_health_v1.bin"), + ) + .out_dir("src/generated") .build_server(true) .build_client(true) .compile(&["proto/health.proto"], &["proto/"])?; diff --git a/tonic-health/src/generated/grpc.health.v1.rs b/tonic-health/src/generated/grpc.health.v1.rs new file mode 100644 index 000000000..634201e52 --- /dev/null +++ b/tonic-health/src/generated/grpc.health.v1.rs @@ -0,0 +1,379 @@ +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct HealthCheckRequest { + #[prost(string, tag="1")] + pub service: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct HealthCheckResponse { + #[prost(enumeration="health_check_response::ServingStatus", tag="1")] + pub status: i32, +} +/// Nested message and enum types in `HealthCheckResponse`. +pub mod health_check_response { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum ServingStatus { + Unknown = 0, + Serving = 1, + NotServing = 2, + /// Used only by the Watch method. + ServiceUnknown = 3, + } + impl ServingStatus { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + ServingStatus::Unknown => "UNKNOWN", + ServingStatus::Serving => "SERVING", + ServingStatus::NotServing => "NOT_SERVING", + ServingStatus::ServiceUnknown => "SERVICE_UNKNOWN", + } + } + } +} +/// Generated client implementations. +pub mod health_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct HealthClient { + inner: tonic::client::Grpc, + } + impl HealthClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: std::convert::TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl HealthClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> HealthClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + HealthClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// If the requested service is unknown, the call will fail with status + /// NOT_FOUND. + pub async fn check( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/grpc.health.v1.Health/Check", + ); + self.inner.unary(request.into_request(), path, codec).await + } + /// Performs a watch for the serving status of the requested service. + /// The server will immediately send back a message indicating the current + /// serving status. It will then subsequently send a new message whenever + /// the service's serving status changes. + /// + /// If the requested service is unknown when the call is received, the + /// server will send a message setting the serving status to + /// SERVICE_UNKNOWN but will *not* terminate the call. If at some + /// future point, the serving status of the service becomes known, the + /// server will send a new message with the service's serving status. + /// + /// If the call terminates with status UNIMPLEMENTED, then clients + /// should assume this method is not supported and should not retry the + /// call. If the call terminates with any other status (including OK), + /// clients should retry the call with appropriate exponential backoff. + pub async fn watch( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/grpc.health.v1.Health/Watch", + ); + self.inner.server_streaming(request.into_request(), path, codec).await + } + } +} +/// Generated server implementations. +pub mod health_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + ///Generated trait containing gRPC methods that should be implemented for use with HealthServer. + #[async_trait] + pub trait Health: Send + Sync + 'static { + /// If the requested service is unknown, the call will fail with status + /// NOT_FOUND. + async fn check( + &self, + request: tonic::Request, + ) -> Result, tonic::Status>; + ///Server streaming response type for the Watch method. + type WatchStream: futures_core::Stream< + Item = Result, + > + + Send + + 'static; + /// Performs a watch for the serving status of the requested service. + /// The server will immediately send back a message indicating the current + /// serving status. It will then subsequently send a new message whenever + /// the service's serving status changes. + /// + /// If the requested service is unknown when the call is received, the + /// server will send a message setting the serving status to + /// SERVICE_UNKNOWN but will *not* terminate the call. If at some + /// future point, the serving status of the service becomes known, the + /// server will send a new message with the service's serving status. + /// + /// If the call terminates with status UNIMPLEMENTED, then clients + /// should assume this method is not supported and should not retry the + /// call. If the call terminates with any other status (including OK), + /// clients should retry the call with appropriate exponential backoff. + async fn watch( + &self, + request: tonic::Request, + ) -> Result, tonic::Status>; + } + #[derive(Debug)] + pub struct HealthServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + } + struct _Inner(Arc); + impl HealthServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + } + impl tonic::codegen::Service> for HealthServer + where + T: Health, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/grpc.health.v1.Health/Check" => { + #[allow(non_camel_case_types)] + struct CheckSvc(pub Arc); + impl< + T: Health, + > tonic::server::UnaryService + for CheckSvc { + type Response = super::HealthCheckResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = self.0.clone(); + let fut = async move { (*inner).check(request).await }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = CheckSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/grpc.health.v1.Health/Watch" => { + #[allow(non_camel_case_types)] + struct WatchSvc(pub Arc); + impl< + T: Health, + > tonic::server::ServerStreamingService + for WatchSvc { + type Response = super::HealthCheckResponse; + type ResponseStream = T::WatchStream; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = self.0.clone(); + let fut = async move { (*inner).watch(request).await }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = WatchSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ); + let res = grpc.server_streaming(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for HealthServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(self.0.clone()) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService for HealthServer { + const NAME: &'static str = "grpc.health.v1.Health"; + } +} diff --git a/tonic-health/src/generated/grpc_health_v1.bin b/tonic-health/src/generated/grpc_health_v1.bin new file mode 100644 index 0000000000000000000000000000000000000000..52efb312248858eba8a5024742a401fbc68b32fb GIT binary patch literal 2875 zcmb_e&2ke*5Y|d-*^G^C*8CVlLL+`ngk(FFOA=FMga8GQ97)DS4soqkBWa7(&SqxT z!pA&C-XVun<&-=@&Uu17OS)%wB^xLzhxi~*PxthHf4%I_%WP@H-9V2jkrY}u%R?D? zmGtU(yHsJ$bHlHVxc9cre~P)%&N`b{Tps%#U$?-zf3BU8rQa>KcIJVKM5wsaU`3@} z9V_!-d1t$F)==dmk18Fq5e_@%VLMe&YO(WY7|tS_KWM&awho)CIU+js_UlIT*{bEN zu*GJpdz4+|oi(;%h+6$9ds5i>&0@vGytB{dOrTEblm3}3Y~1>!Od5Eb%^kYh8~tw@ zx2(r5yWorWj6YsXLfEG=?pr^Uhe8YkUKxtO4TlvWhgHgWb!4h(&PU1l=y?0r{PHuZ zd!nN9di%Ry99DGf3%N%Y`)iI_OHScJ&e{3HhMI^@q(2;K*xB0t3c4c>!}fj+_F_E} zQZWWEu`RIVeTc(8m!MH%FLFKnmvYMRnoH%25OykCaGwN9DXX;k4TBRA!`MB6P-uu1 zM-9G$fgf<-?>ru9@I&y#I0}3>^f*j>Ju+5O9Tf&|QVpV~T@-dvJURg}I4uUQMrY}7 zq;>RlwK|zhDz0(VWGhI*P}P@>TD{q+KSXEo!9f^sr9k2&_!5Kcoq!vmE1ug!2LpEk zLV`P#9C=!hs}t#K?BX&gG0+oNat3{0Y3cW3eI`q(|A5h+7Qj5YA(Zwy(CC!l@m{CV zDKj{1bf32lx^TGHZtpd_jd}-KZK$=HPa54utBKT8*lWIl7menVGH}ch)`Gu_Bzcc6 z`jkK3$7FRlrFdE$7$BEu6>-lW_#Oro4r6zSg%-zLh8PP(T#kK3HB{(apF!Y{eH>GB zF{}HuB0E1@rd2#Q6d~p+0Muk!z!U`|Ht++;haN{4PzM#r)J9=(h8RHHt6zNf#_AouF_ndY4PLjwAmAAV{Cz0d8e?JyF#Z0r*J9v1v|&`3wM%Za(OvdGnb1_ zfsV=Lm0ZbOGN(Yt+ZK+yirIIR!d0mbY_`eJ%Hyh3A5u!~1Is8*Gu; zb}pZD3fJ?O*bEm2^B389RwN!7yJ@eoWj3FX$i8VW&S*@{&E=J;II>Hp#K}_WlsILwbV?k_TlU4N zwmgd8vR7s_@ezHisWTYQZF{{0a6mvI!r+7kiMq~!C5$-!sw%2AfD!hI9 zibn8VO?1u;bc8Pt&bOBN~TqLoo@<#%t{80D%a92Ch#hgrsEOF`y0fv~|#Yg5jVS zq#v!7i;EeBp_G?C&nWb;y!kErr)XS3$=!T?7Vt8O zumaG~i9#%32rBL=N?52fwJwBwGSxakA&x$<1Z9%%hBFicDTZ%{g*NTX`Ph< z>m#|x{bSxY1xkF*;x@6)!c<&OV>}xgA~@nBeNN0i7yw~AiS_|5s0V}fImjCAH+JA93tU|I_Z}8jHG+X+~Pj#aYQ+;VAftA_5MssvlW=?_#m67vS?a$ z>a;rDt8mb4yxM$o*cIXzqxp;qX_NW=l6vQqIlUCPOr(lpSnR&f*wNn?U5 zArmKt>kmQGk{iD9PA+BoV%B-XB`=J-aeQLwji%r~psY{uOsrp2p89v;} zQbdxPo3@?^F$N#k4t2N~45p( Date: Sun, 21 Aug 2022 12:12:53 +0100 Subject: [PATCH 2/3] Check generated code for tonic-health matches in CI --- .github/workflows/CI.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ce2a9ce59..29b3f27a8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -37,6 +37,10 @@ jobs: run: cargo hack check --all --ignore-private --each-feature --no-dev-deps - name: Check all targets run: cargo check --all --all-targets --all-features + - name: Ensure generated code matches + run: > + echo 'If this fails, run `cargo build --features gen-proto` and commit the changes'; + git diff --exit-code --name-only -- tonic-health/src/generated/ # deny-check: # name: cargo-deny check From cb2eedeca2b43c2e04aa9e5d713adf505cb20658 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Mon, 22 Aug 2022 22:45:03 +0100 Subject: [PATCH 3/3] Use bootstrap test to generate/check validity of generated code As suggested by @LucioFranco in https://github.com/hyperium/tonic/pull/1065\#discussion_r951580189. This avoids the need for a feature which would show up in docs.rs, and achieves the same goals via CI. --- .github/workflows/CI.yml | 4 ---- tonic-health/Cargo.toml | 6 +----- tonic-health/build.rs | 18 ------------------ tonic-health/tests/bootstrap.rs | 30 ++++++++++++++++++++++++++++++ 4 files changed, 31 insertions(+), 27 deletions(-) delete mode 100644 tonic-health/build.rs create mode 100644 tonic-health/tests/bootstrap.rs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 29b3f27a8..ce2a9ce59 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -37,10 +37,6 @@ jobs: run: cargo hack check --all --ignore-private --each-feature --no-dev-deps - name: Check all targets run: cargo check --all --all-targets --all-features - - name: Ensure generated code matches - run: > - echo 'If this fails, run `cargo build --features gen-proto` and commit the changes'; - git diff --exit-code --name-only -- tonic-health/src/generated/ # deny-check: # name: cargo-deny check diff --git a/tonic-health/Cargo.toml b/tonic-health/Cargo.toml index 2041e43d1..c3bc5236a 100644 --- a/tonic-health/Cargo.toml +++ b/tonic-health/Cargo.toml @@ -17,8 +17,6 @@ version = "0.7.0" [features] default = ["transport"] transport = ["tonic/transport"] -# generate gRPC code from `.proto`(s) -gen-proto = ["dep:tonic-build"] [dependencies] async-stream = "0.3" @@ -30,6 +28,4 @@ tonic = {version = "0.8", path = "../tonic", features = ["codegen", "prost"]} [dev-dependencies] tokio = {version = "1.0", features = ["rt-multi-thread", "macros"]} - -[build-dependencies] -tonic-build = {version = "0.8", path = "../tonic-build", features = ["prost"], optional = true} +tonic-build = {version = "0.8", path = "../tonic-build", features = ["prost"]} diff --git a/tonic-health/build.rs b/tonic-health/build.rs deleted file mode 100644 index 1b9933429..000000000 --- a/tonic-health/build.rs +++ /dev/null @@ -1,18 +0,0 @@ -// This build file is used to generate the code as a one-off, -// but is only rerun with the `gen-proto` feature enabled. -// This simplifies the build process for this crate by not requiring -// users to have protoc available. - -fn main() -> Result<(), Box> { - #[cfg(feature = "gen-proto")] - tonic_build::configure() - .file_descriptor_set_path( - std::path::PathBuf::from("src/generated").join("grpc_health_v1.bin"), - ) - .out_dir("src/generated") - .build_server(true) - .build_client(true) - .compile(&["proto/health.proto"], &["proto/"])?; - - Ok(()) -} diff --git a/tonic-health/tests/bootstrap.rs b/tonic-health/tests/bootstrap.rs new file mode 100644 index 000000000..bbf4f9cfa --- /dev/null +++ b/tonic-health/tests/bootstrap.rs @@ -0,0 +1,30 @@ +use std::{path::PathBuf, process::Command}; + +#[test] +fn bootstrap() { + let iface_files = &["proto/health.proto"]; + let dirs = &["proto"]; + + let out_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) + .join("src") + .join("generated"); + + tonic_build::configure() + .build_client(true) + .build_server(true) + .out_dir(format!("{}", out_dir.display())) + .compile(iface_files, dirs) + .unwrap(); + + let status = Command::new("git") + .arg("diff") + .arg("--exit-code") + .arg("--") + .arg(format!("{}", out_dir.display())) + .status() + .unwrap(); + + if !status.success() { + panic!("You should commit the protobuf files"); + } +}