Skip to content
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
merged 12 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions design/src/server/anatomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,44 @@ pub struct PokemonService<S> {
}
```

The following schematic summarizes the composition:

```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 PokemonService {
state RoutingService {
in --> UpgradeLayer: HTTP Request
in --> C2: HTTP Request
in --> C3: HTTP Request
in --> C4: HTTP Request
state C1 {
state L {
state UpgradeLayer {
direction LR
[*] --> S: Model Input
S --> [*] : Model Output
}
}
}
C2
C3
C4
}

}
C2 --> [*]: HTTP Response
C3 --> [*]: HTTP Response
C4 --> [*]: HTTP Response
```

## Plugins
<!-- TODO(missing_doc): Link to "Write a Plugin" documentation -->

Expand Down
350 changes: 350 additions & 0 deletions design/src/server/middleware.md
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
Copy link
Contributor

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.

Copy link
Contributor Author

@hlbarber hlbarber Oct 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hows this?
debae27


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();
```
Loading