-
Notifications
You must be signed in to change notification settings - Fork 193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add middleware documentation #1844
Merged
+394
−3
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
7009042
Tweak plugin.rs documentation
cb7dc53
Add middleware.md
b5d210a
Improve diagram
d2a2bdf
Improve introduction
32fbe4c
Fix spelling
b40bbc1
Add summary schematic
8351af2
Address feedback
debae27
Contrast A with B
4e91c26
Address feedback
2691033
Merge branch 'main' into harryb/middleware-docs
crisidev 85d5eb9
Merge branch 'main' into harryb/middleware-docs
hlbarber bf2c7b5
Merge branch 'main' into harryb/middleware-docs
hlbarber File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,350 @@ | ||
# Middleware | ||
|
||
The following document provides a brief survey of the various positions middleware can be inserted in Smithy Rust. | ||
|
||
We use the [Pokémon service](https://github.com/awslabs/smithy-rs/blob/main/codegen-core/common-test-models/pokemon.smithy) as a reference model throughout. | ||
|
||
```smithy | ||
/// A Pokémon species forms the basis for at least one Pokémon. | ||
@title("Pokémon Species") | ||
resource PokemonSpecies { | ||
identifiers: { | ||
name: String | ||
}, | ||
read: GetPokemonSpecies, | ||
} | ||
|
||
/// A users current Pokémon storage. | ||
resource Storage { | ||
identifiers: { | ||
user: String | ||
}, | ||
read: GetStorage, | ||
} | ||
|
||
/// The Pokémon Service allows you to retrieve information about Pokémon species. | ||
@title("Pokémon Service") | ||
@restJson1 | ||
service PokemonService { | ||
version: "2021-12-01", | ||
resources: [PokemonSpecies, Storage], | ||
operations: [ | ||
GetServerStatistics, | ||
DoNothing, | ||
CapturePokemon, | ||
CheckHealth | ||
], | ||
} | ||
``` | ||
|
||
## Introduction to Tower | ||
|
||
Smithy Rust is built on top of [`tower`](https://github.com/tower-rs/tower). | ||
|
||
> Tower is a library of modular and reusable components for building robust networking clients and servers. | ||
|
||
The `tower` library is centered around two main interfaces, the [`Service`](https://docs.rs/tower/latest/tower/trait.Service.html) trait and the [`Layer`](https://docs.rs/tower/latest/tower/trait.Layer.html) trait. | ||
|
||
The `Service` trait can be thought of as an asynchronous function from a request to a response, `async fn(Request) -> Result<Response, Error>`, coupled with a mechanism to [handle back pressure](https://docs.rs/tower/latest/tower/trait.Service.html#backpressure), while the `Layer` trait can be thought of as a way of decorating a `Service`, transforming either the request or response. | ||
|
||
Middleware in `tower` typically conforms to the following pattern, a `Service` implementation of the form | ||
|
||
```rust | ||
pub struct NewService<S> { | ||
inner: S, | ||
/* auxillary data */ | ||
} | ||
``` | ||
|
||
and a complementary | ||
|
||
```rust | ||
pub struct NewLayer { | ||
/* auxiliary data */ | ||
} | ||
|
||
impl<S> Layer<S> for NewLayer { | ||
type Service = NewService<S>; | ||
|
||
fn layer(&self, inner: S) -> Self::Service { | ||
NewService { | ||
inner, | ||
/* auxiliary fields */ | ||
} | ||
} | ||
} | ||
``` | ||
|
||
The `NewService` modifies the behavior of the inner `Service` `S` while the `NewLayer` takes auxiliary data and constructs `NewService<S>` from `S`. | ||
|
||
Customers are then able to stack middleware by composing `Layer`s using combinators such as [`ServiceBuilder::layer`](https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html#method.layer) and [`Stack`](https://docs.rs/tower/latest/tower/layer/util/struct.Stack.html). | ||
|
||
<!-- TODO(Update documentation): There's a `Layer` implementation on tuples about to be merged, give it as an example here. --> | ||
|
||
## Applying Middleware | ||
|
||
One of the primary goals is to provide configurability and extensibility through the application of middleware. The customer is able to apply `Layer`s in a variety of key places during the request/response lifecycle. The following schematic labels each configurable middleware position from A to D: | ||
|
||
```mermaid | ||
stateDiagram-v2 | ||
state in <<fork>> | ||
state "GetPokemonSpecies" as C1 | ||
state "GetStorage" as C2 | ||
state "DoNothing" as C3 | ||
state "..." as C4 | ||
direction LR | ||
[*] --> in : HTTP Request | ||
UpgradeLayer --> [*]: HTTP Response | ||
state A { | ||
state PokemonService { | ||
state RoutingService { | ||
in --> UpgradeLayer: HTTP Request | ||
in --> C2: HTTP Request | ||
in --> C3: HTTP Request | ||
in --> C4: HTTP Request | ||
state B { | ||
state C1 { | ||
state C { | ||
state UpgradeLayer { | ||
direction LR | ||
[*] --> Handler: Model Input | ||
Handler --> [*] : Model Output | ||
state D { | ||
Handler | ||
} | ||
} | ||
} | ||
} | ||
C2 | ||
C3 | ||
C4 | ||
} | ||
} | ||
} | ||
} | ||
C2 --> [*]: HTTP Response | ||
C3 --> [*]: HTTP Response | ||
C4 --> [*]: HTTP Response | ||
``` | ||
|
||
where `UpgradeLayer` is the `Layer` converting Smithy model structures to HTTP structures and the `RoutingService` is responsible for routing requests to the appropriate operation. | ||
|
||
### A) Outer Middleware | ||
|
||
The output of the Smithy service builder provides the user with a `Service<http::Request, Response = http::Response>` implementation. A `Layer` can be applied around the entire `Service`. | ||
|
||
```rust | ||
// This is a HTTP `Service`. | ||
let app /* : PokemonService<Route<B>> */ = PokemonService::builder() | ||
.get_pokemon_species(/* handler */) | ||
/* ... */ | ||
.build(); | ||
|
||
// Construct `TimeoutLayer`. | ||
let timeout_layer = TimeoutLayer::new(Duration::from_secs(3)); | ||
|
||
// Apply a 3 second timeout to all responses. | ||
let app = timeout_layer.layer(app); | ||
``` | ||
|
||
### B) Route Middleware | ||
|
||
A _single_ layer can be applied to _all_ routes inside the `Router`. This exists as a method on the output of the service builder. | ||
|
||
```rust | ||
// Construct `TraceLayer`. | ||
let trace_layer = TraceLayer::new_for_http(Duration::from_secs(3)); | ||
|
||
let app /* : PokemonService<Route<B>> */ = PokemonService::builder() | ||
.get_pokemon_species(/* handler */) | ||
/* ... */ | ||
.build() | ||
// Apply HTTP logging after routing. | ||
.layer(&trace_layer); | ||
``` | ||
|
||
Note that requests pass through this middleware immediately _after_ routing succeeds and therefore will _not_ be encountered if routing fails. This means that the [TraceLayer](https://docs.rs/tower-http/latest/tower_http/trace/struct.TraceLayer.html) in the example above does _not_ provide logs unless routing has completed. This contrasts to [middleware A](#a-outer-middleware), which _all_ requests/responses pass through when entering/leaving the service. | ||
|
||
### C) Operation Specific HTTP Middleware | ||
|
||
A "HTTP layer" can be applied to specific operations. | ||
|
||
```rust | ||
// Construct `TraceLayer`. | ||
let trace_layer = TraceLayer::new_for_http(Duration::from_secs(3)); | ||
|
||
// Apply HTTP logging to only the `GetPokemonSpecies` operation. | ||
let layered_handler = GetPokemonSpecies::from_handler(/* handler */).layer(trace_layer); | ||
|
||
let app /* : PokemonService<Route<B>> */ = PokemonService::builder() | ||
.get_pokemon_species_operation(layered_handler) | ||
/* ... */ | ||
.build(); | ||
``` | ||
|
||
This middleware transforms the operations HTTP requests and responses. | ||
|
||
### D) Operation Specific Model Middleware | ||
|
||
A "model layer" can be applied to specific operations. | ||
|
||
```rust | ||
// A handler `Service`. | ||
let handler_svc = service_fn(/* handler */); | ||
|
||
// Construct `BufferLayer`. | ||
let buffer_layer = BufferLayer::new(3); | ||
|
||
// Apply a 3 item buffer to `handler_svc`. | ||
let handler_svc = buffer_layer.layer(handler_svc); | ||
|
||
let layered_handler = GetPokemonSpecies::from_service(handler_svc); | ||
|
||
let app /* : PokemonService<Route<B>> */ = PokemonService::builder() | ||
.get_pokemon_species_operation(layered_handler) | ||
/* ... */ | ||
.build(); | ||
``` | ||
|
||
In contrast to [position C](#c-operation-specific-http-middleware), this middleware transforms the operations modelled inputs to modelled outputs. | ||
|
||
## Plugin System | ||
|
||
Suppose we want to apply a different `Layer` to every operation. In this case, position B (`PokemonService::layer`) will not suffice because it applies a single `Layer` to all routes and while position C (`Operation::layer`) would work, it'd require the customer constructs the `Layer` by hand for every operation. | ||
|
||
Consider the following middleware: | ||
|
||
```rust | ||
/// A [`Service`] that adds a print log. | ||
#[derive(Clone, Debug)] | ||
pub struct PrintService<S> { | ||
inner: S, | ||
name: &'static str, | ||
} | ||
|
||
impl<R, S> Service<R> for PrintService<S> | ||
where | ||
S: Service<R>, | ||
{ | ||
type Response = S::Response; | ||
type Error = S::Error; | ||
type Future = S::Future; | ||
|
||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { | ||
self.inner.poll_ready(cx) | ||
} | ||
|
||
fn call(&mut self, req: R) -> Self::Future { | ||
println!("Hi {}", self.name); | ||
self.inner.call(req) | ||
} | ||
} | ||
|
||
/// A [`Layer`] which constructs the [`PrintService`]. | ||
#[derive(Debug)] | ||
pub struct PrintLayer { | ||
name: &'static str, | ||
} | ||
impl<S> Layer<S> for PrintLayer { | ||
type Service = PrintService<S>; | ||
|
||
fn layer(&self, service: S) -> Self::Service { | ||
PrintService { | ||
inner: service, | ||
name: self.name, | ||
} | ||
} | ||
} | ||
``` | ||
|
||
The plugin system provides a way to construct then apply `Layer`s in position [C](#c-operation-specific-http-middleware) and [D](#d-operation-specific-model-middleware), using the [protocol](https://awslabs.github.io/smithy/2.0/aws/protocols/index.html) and [operation shape](https://awslabs.github.io/smithy/2.0/spec/service-types.html#service-operations) as parameters. | ||
|
||
An example of a `PrintPlugin` which applies a layer printing the operation name: | ||
|
||
```rust | ||
/// A [`Plugin`] for a service builder to add a [`PrintLayer`] over operations. | ||
#[derive(Debug)] | ||
pub struct PrintPlugin; | ||
|
||
impl<P, Op, S, L> Plugin<P, Op, S, L> for PrintPlugin | ||
where | ||
Op: OperationShape, | ||
{ | ||
type Service = S; | ||
type Layer = Stack<L, PrintLayer>; | ||
|
||
fn map(&self, input: Operation<S, L>) -> Operation<Self::Service, Self::Layer> { | ||
input.layer(PrintLayer { name: Op::NAME }) | ||
} | ||
} | ||
``` | ||
|
||
An alternative example which applies a layer for a given protocol: | ||
|
||
```rust | ||
/// A [`Plugin`] for a service builder to add a [`PrintLayer`] over operations. | ||
#[derive(Debug)] | ||
pub struct PrintPlugin; | ||
|
||
impl<Op, S, L> Plugin<AwsRestJson1, Op, S, L> for PrintPlugin | ||
{ | ||
type Service = S; | ||
type Layer = Stack<L, PrintLayer>; | ||
|
||
fn map(&self, input: Operation<S, L>) -> Operation<Self::Service, Self::Layer> { | ||
input.layer(PrintLayer { name: "AWS REST JSON v1" }) | ||
} | ||
} | ||
|
||
impl<Op, S, L> Plugin<AwsRestXml, Op, S, L> for PrintPlugin | ||
{ | ||
type Service = S; | ||
type Layer = Stack<L, PrintLayer>; | ||
|
||
fn map(&self, input: Operation<S, L>) -> Operation<Self::Service, Self::Layer> { | ||
input.layer(PrintLayer { name: "AWS REST XML" }) | ||
} | ||
} | ||
``` | ||
|
||
A `Plugin` can then be applied to all operations using the `Pluggable::apply` method | ||
|
||
```rust | ||
pub trait Pluggable<NewPlugin> { | ||
type Output; | ||
|
||
/// Applies a [`Plugin`] to the service builder. | ||
fn apply(self, plugin: NewPlugin) -> Self::Output; | ||
} | ||
``` | ||
|
||
which is implemented on every service builder. | ||
|
||
The plugin system is designed to hide the details of the `Plugin` and `Pluggable` trait from the average consumer. Such customers should instead interact with utility methods on the service builder which are vended by extension traits and enjoy self contained documentation. | ||
|
||
```rust | ||
/// An extension trait of [`Pluggable`]. | ||
/// | ||
/// This provides a [`print`](PrintExt::print) method to all service builders. | ||
pub trait PrintExt: Pluggable<PrintPlugin> { | ||
/// Causes all operations to print the operation name when called. | ||
/// | ||
/// This works by applying the [`PrintPlugin`]. | ||
fn print(self) -> Self::Output | ||
where | ||
Self: Sized, | ||
{ | ||
self.apply(PrintPlugin) | ||
} | ||
} | ||
``` | ||
|
||
which allows for | ||
|
||
```rust | ||
let app /* : PokemonService<Route<B>> */ = PokemonService::builder() | ||
.get_pokemon_species_operation(layered_handler) | ||
/* ... */ | ||
.print() | ||
.build(); | ||
``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I understand it, I believe the difference between A) and B) is not clear in the doc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hows this?
debae27