Skip to content

Commit 540545a

Browse files
jjantrcoh
authored andcommitted
Add AlbHealthCheckLayer (#2540)
## Motivation and Context Services often need the ability to report health status via health checks (see [ALB Health Checks](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html)). This PR adds a simple layer that allows configuring your service to respond to these health check requests. ## Description Adds `AlbHealthCheckLayer`, and `AlbHealthCheckService`. ## Testing Added this layer to the `pokemon-service` binary, and added an integration test for it. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 6a51a56 commit 540545a

File tree

8 files changed

+231
-9
lines changed

8 files changed

+231
-9
lines changed

CHANGELOG.next.toml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,31 @@
1111
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
1212
# author = "rcoh"
1313

14+
[[smithy-rs]]
15+
message = """
16+
Implement layer for servers to handle [ALB health checks](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html).
17+
Take a look at `aws_smithy_http_server::plugin::alb_health_check` to learn about it.
18+
"""
19+
references = ["smithy-rs#2540"]
20+
meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "server" }
21+
author = "jjant"
22+
23+
[[smithy-rs]]
24+
message = "Implement `PluginPipeline::http_layer` which allows you to apply a `tower::Layer` to all operations."
25+
references = ["smithy-rs#2540"]
26+
meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "server" }
27+
author = "jjant"
28+
1429
[[aws-sdk-rust]]
15-
message = "Implement std::error::Error#source() properly for the service meta Error enum"
30+
message = "Implement std::error::Error#source() properly for the service meta Error enum."
1631
references = ["aws-sdk-rust#784"]
1732
meta = { "breaking" = false, "tada" = false, "bug" = false }
1833
author = "abusch"
1934

2035
[[smithy-rs]]
21-
message = "Implement std::error::Error#source() properly for the service meta Error enum"
36+
message = "Implement std::error::Error#source() properly for the service meta Error enum."
2237
references = ["aws-sdk-rust#784"]
23-
meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client"}
38+
meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client" }
2439
author = "abusch"
2540

2641
[[aws-sdk-rust]]
@@ -32,7 +47,7 @@ author = "jdisanti"
3247
[[smithy-rs]]
3348
message = "The outputs for event stream operations now implement the `Sync` auto-trait."
3449
references = ["smithy-rs#2496"]
35-
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "all"}
50+
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "all" }
3651
author = "jdisanti"
3752

3853
[[aws-sdk-rust]]
@@ -44,7 +59,7 @@ author = "eduardomourar"
4459
[[smithy-rs]]
4560
message = "Clients now compile for the `wasm32-unknown-unknown` and `wasm32-wasi` targets when no default features are enabled. WebAssembly is not officially supported yet, but this is a great first step towards it!"
4661
references = ["smithy-rs#2254"]
47-
meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "client"}
62+
meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "client" }
4863
author = "eduardomourar"
4964

5065
[[smithy-rs]]
@@ -56,7 +71,7 @@ author = "jdisanti"
5671
[[smithy-rs]]
5772
message = "Streaming operations now emit the request ID at the `debug` log level like their non-streaming counterparts."
5873
references = ["smithy-rs#2495"]
59-
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "client"}
74+
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "client" }
6075
author = "jdisanti"
6176

6277
[[smithy-rs]]

examples/pokemon-service/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ async-stream = "0.3"
2424
rand = "0.8.5"
2525
serial_test = "1.0.0"
2626

27+
# We use hyper client in tests
28+
hyper = {version = "0.14.25", features = ["server", "client"] }
29+
2730
# This dependency is only required for testing the `pokemon-service-tls` program.
2831
hyper-rustls = { version = "0.23.2", features = ["http2"] }
2932

examples/pokemon-service/src/main.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ mod plugin;
88
use std::{net::SocketAddr, sync::Arc};
99

1010
use aws_smithy_http_server::{
11-
extension::OperationExtensionExt, instrumentation::InstrumentExt, plugin::PluginPipeline,
12-
request::request_id::ServerRequestIdProviderLayer, AddExtensionLayer,
11+
extension::OperationExtensionExt,
12+
instrumentation::InstrumentExt,
13+
plugin::{alb_health_check::AlbHealthCheckLayer, PluginPipeline},
14+
request::request_id::ServerRequestIdProviderLayer,
15+
AddExtensionLayer,
1316
};
1417
use clap::Parser;
1518

19+
use hyper::StatusCode;
1620
use plugin::PrintExt;
1721

1822
use pokemon_service::{
@@ -47,7 +51,12 @@ pub async fn main() {
4751
// `Response::extensions`, or infer routing failure when it's missing.
4852
.insert_operation_extension()
4953
// Adds `tracing` spans and events to the request lifecycle.
50-
.instrument();
54+
.instrument()
55+
// Handle `/ping` health check requests.
56+
.http_layer(AlbHealthCheckLayer::from_handler("/ping", |_req| async {
57+
StatusCode::OK
58+
}));
59+
5160
let app = PokemonService::builder_with_plugins(plugins)
5261
// Build a registry containing implementations to all the operations in the service. These
5362
// are async functions or async closures that take as input the operation's input and

examples/pokemon-service/tests/common/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ pub async fn run_server() -> ChildDrop {
2323
ChildDrop(child)
2424
}
2525

26+
pub fn base_url() -> String {
27+
format!("http://{DEFAULT_ADDRESS}:{DEFAULT_PORT}")
28+
}
29+
2630
pub fn client() -> Client<DynConnector, DynMiddleware<DynConnector>> {
2731
let authority = Authority::from_str(&format!("{DEFAULT_ADDRESS}:{DEFAULT_PORT}"))
2832
.expect("could not parse authority");

examples/pokemon-service/tests/simple.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,11 @@ async fn simple_integration_test() {
8181

8282
let service_statistics_out = client.get_server_statistics().send().await.unwrap();
8383
assert_eq!(2, service_statistics_out.calls_count.unwrap());
84+
85+
let hyper_client = hyper::Client::new();
86+
let health_check_url = format!("{}/ping", common::base_url());
87+
let health_check_url = hyper::Uri::try_from(health_check_url).unwrap();
88+
let result = hyper_client.get(health_check_url).await.unwrap();
89+
90+
assert_eq!(result.status(), 200);
8491
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
//! Middleware for handling [ALB health
7+
//! checks](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html).
8+
//!
9+
//! # Example
10+
//!
11+
//! ```no_run
12+
//! # use aws_smithy_http_server::{body, plugin::{PluginPipeline, alb_health_check::AlbHealthCheckLayer}};
13+
//! # use hyper::{Body, Response, StatusCode};
14+
//! let plugins = PluginPipeline::new()
15+
//! // Handle all `/ping` health check requests by returning a `200 OK`.
16+
//! .http_layer(AlbHealthCheckLayer::from_handler("/ping", |_req| async {
17+
//! StatusCode::OK
18+
//! }));
19+
//!
20+
//! ```
21+
22+
use std::borrow::Cow;
23+
use std::convert::Infallible;
24+
use std::task::{Context, Poll};
25+
26+
use futures_util::{Future, FutureExt};
27+
use http::StatusCode;
28+
use hyper::{Body, Request, Response};
29+
use pin_project_lite::pin_project;
30+
use tower::{service_fn, util::Oneshot, Layer, Service, ServiceExt};
31+
32+
use crate::body::BoxBody;
33+
34+
use super::either::EitherProj;
35+
use super::Either;
36+
37+
/// A [`tower::Layer`] used to apply [`AlbHealthCheckService`].
38+
#[derive(Clone, Debug)]
39+
pub struct AlbHealthCheckLayer<HealthCheckHandler> {
40+
health_check_uri: Cow<'static, str>,
41+
health_check_handler: HealthCheckHandler,
42+
}
43+
44+
impl AlbHealthCheckLayer<()> {
45+
/// Handle health check requests at `health_check_uri` with the specified handler.
46+
pub fn from_handler<HandlerFuture: Future<Output = StatusCode>, H: Fn(Request<Body>) -> HandlerFuture + Clone>(
47+
health_check_uri: impl Into<Cow<'static, str>>,
48+
health_check_handler: H,
49+
) -> AlbHealthCheckLayer<
50+
impl Service<
51+
Request<Body>,
52+
Response = StatusCode,
53+
Error = Infallible,
54+
Future = impl Future<Output = Result<StatusCode, Infallible>>,
55+
> + Clone,
56+
> {
57+
let service = service_fn(move |req| health_check_handler(req).map(Ok));
58+
59+
AlbHealthCheckLayer::new(health_check_uri, service)
60+
}
61+
62+
/// Handle health check requests at `health_check_uri` with the specified service.
63+
pub fn new<H: Service<Request<Body>, Response = StatusCode>>(
64+
health_check_uri: impl Into<Cow<'static, str>>,
65+
health_check_handler: H,
66+
) -> AlbHealthCheckLayer<H> {
67+
AlbHealthCheckLayer {
68+
health_check_uri: health_check_uri.into(),
69+
health_check_handler,
70+
}
71+
}
72+
}
73+
74+
impl<S, H: Clone> Layer<S> for AlbHealthCheckLayer<H> {
75+
type Service = AlbHealthCheckService<H, S>;
76+
77+
fn layer(&self, inner: S) -> Self::Service {
78+
AlbHealthCheckService {
79+
inner,
80+
layer: self.clone(),
81+
}
82+
}
83+
}
84+
85+
/// A middleware [`Service`] responsible for handling health check requests.
86+
#[derive(Clone, Debug)]
87+
pub struct AlbHealthCheckService<H, S> {
88+
inner: S,
89+
layer: AlbHealthCheckLayer<H>,
90+
}
91+
92+
impl<H, S> Service<Request<Body>> for AlbHealthCheckService<H, S>
93+
where
94+
S: Service<Request<Body>, Response = Response<BoxBody>> + Clone,
95+
S::Future: std::marker::Send + 'static,
96+
H: Service<Request<Body>, Response = StatusCode, Error = Infallible> + Clone,
97+
{
98+
type Response = S::Response;
99+
100+
type Error = S::Error;
101+
102+
type Future = AlbHealthCheckFuture<H, S>;
103+
104+
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
105+
// The check that the service is ready is done by `Oneshot` below.
106+
Poll::Ready(Ok(()))
107+
}
108+
109+
fn call(&mut self, req: Request<Body>) -> Self::Future {
110+
if req.uri() == self.layer.health_check_uri.as_ref() {
111+
let clone = self.layer.health_check_handler.clone();
112+
let service = std::mem::replace(&mut self.layer.health_check_handler, clone);
113+
let handler_future = service.oneshot(req);
114+
115+
AlbHealthCheckFuture::handler_future(handler_future)
116+
} else {
117+
let clone = self.inner.clone();
118+
let service = std::mem::replace(&mut self.inner, clone);
119+
let service_future = service.oneshot(req);
120+
121+
AlbHealthCheckFuture::service_future(service_future)
122+
}
123+
}
124+
}
125+
126+
type HealthCheckFutureInner<H, S> = Either<Oneshot<H, Request<Body>>, Oneshot<S, Request<Body>>>;
127+
128+
pin_project! {
129+
/// Future for [`AlbHealthCheckService`].
130+
pub struct AlbHealthCheckFuture<H: Service<Request<Body>, Response = StatusCode>, S: Service<Request<Body>>> {
131+
#[pin]
132+
inner: HealthCheckFutureInner<H, S>
133+
}
134+
}
135+
136+
impl<H: Service<Request<Body>, Response = StatusCode>, S: Service<Request<Body>>> AlbHealthCheckFuture<H, S> {
137+
fn handler_future(handler_future: Oneshot<H, Request<Body>>) -> Self {
138+
Self {
139+
inner: Either::Left { value: handler_future },
140+
}
141+
}
142+
143+
fn service_future(service_future: Oneshot<S, Request<Body>>) -> Self {
144+
Self {
145+
inner: Either::Right { value: service_future },
146+
}
147+
}
148+
}
149+
150+
impl<
151+
H: Service<Request<Body>, Response = StatusCode, Error = Infallible>,
152+
S: Service<Request<Body>, Response = Response<BoxBody>>,
153+
> Future for AlbHealthCheckFuture<H, S>
154+
{
155+
type Output = Result<S::Response, S::Error>;
156+
157+
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
158+
let either_proj = self.project().inner.project();
159+
160+
match either_proj {
161+
EitherProj::Left { value } => {
162+
let polled: Poll<Self::Output> = value.poll(cx).map(|res| {
163+
res.map(|status_code| {
164+
Response::builder()
165+
.status(status_code)
166+
.body(crate::body::empty())
167+
.unwrap()
168+
})
169+
.map_err(|never| match never {})
170+
});
171+
polled
172+
}
173+
EitherProj::Right { value } => value.poll(cx),
174+
}
175+
}
176+
}

rust-runtime/aws-smithy-http-server/src/plugin/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
//! ```
119119
//!
120120
121+
pub mod alb_health_check;
121122
mod closure;
122123
mod either;
123124
mod filter;

rust-runtime/aws-smithy-http-server/src/plugin/pipeline.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use crate::operation::Operation;
77
use crate::plugin::{IdentityPlugin, Plugin, PluginStack};
88

9+
use super::HttpLayer;
10+
911
/// A wrapper struct for composing [`Plugin`]s.
1012
/// It is used as input for the `builder_with_plugins` method on the generate service struct
1113
/// (e.g. `PokemonService::builder_with_plugins`).
@@ -168,6 +170,11 @@ impl<P> PluginPipeline<P> {
168170
pub fn push<NewPlugin>(self, new_plugin: NewPlugin) -> PluginPipeline<PluginStack<NewPlugin, P>> {
169171
PluginPipeline(PluginStack::new(new_plugin, self.0))
170172
}
173+
174+
/// Applies a single [`tower::Layer`] to all operations _before_ they are deserialized.
175+
pub fn http_layer<L>(self, layer: L) -> PluginPipeline<PluginStack<HttpLayer<L>, P>> {
176+
PluginPipeline(PluginStack::new(HttpLayer(layer), self.0))
177+
}
171178
}
172179

173180
impl<P, Op, S, L, InnerPlugin> Plugin<P, Op, S, L> for PluginPipeline<InnerPlugin>

0 commit comments

Comments
 (0)