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 "Anatomy of a Service" documentation #1782

Merged
merged 29 commits into from
Oct 11, 2022
Merged

Conversation

hlbarber
Copy link
Contributor

@hlbarber hlbarber commented Sep 29, 2022

Motivation and Context

The new service builder API, described in Service Builder Improvements, is built on a variety of interfaces. Proper documentation should be provided to onboard new contributors on how they stack.

Description

Rendered

  • Tweaks to Rust documentation.
  • Add anatomy.md and various diagrams.

Notes

@hlbarber hlbarber added documentation Improvements or additions to documentation server Rust server SDK labels Sep 29, 2022

As mentioned, `L` is applied _after_ the `Operation<S, L>` has been "upgraded" to a HTTP service. The procedure of upgrading a model service to a HTTP service is described in the [Upgrading a Model Service](#upgrading-a-model-service) section below.

## Serialization and Deserialization
Copy link
Contributor Author

@hlbarber hlbarber Sep 29, 2022

Choose a reason for hiding this comment

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

At the moment this document is dominated by the builder API and the mechanisms directly under it. However this makes up a small percentage of the server side code.

I think it'd make sense to write a section here on how various shapes structure/enum/map etc to map to Rust types. Not in terms of the actual imperative steps that need to take place in the Kotlin but a kind of declarative description on the mapping. Maybe a table would be nice here?

This section on "Serialization and Deserialization" can then be a subsection of that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that is already fairly well covered in section 5 here: https://awslabs.github.io/smithy-rs/design/smithy/simple_shapes.html


To summarize, the `S`, in `Operation<S, L>`, is a **model service** constructed from a `Handler` or a `OperationService` subject to the constraints of a `OperationShape`.

Now, what about the `L` in `Operation<S, L>`?
Copy link
Contributor Author

@hlbarber hlbarber Sep 29, 2022

Choose a reason for hiding this comment

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

The exact reason behind L is a mystery until after we describe its role in the upgrade procedure in a later section. If this is too confusing then reviewers should mention it and I can try remedy it.

The following nomenclature will aid us in our survey. We describe a `tower::Service` as a "model service" if it's request and response are Smithy structures, as defined by the `OperationShape` trait - the `GetPokemonSpeciesInput`, `GetPokemonSpeciesOutput`, and `GetPokemonSpeciesError` described above. Similarly, we describe a `tower::Service` as a "HTTP service" if it's request and response are [`http`](https://github.com/hyperium/http) structures - `http::Request` and `http::Response`.

<!-- TODO(hidden_docs): Link to `Operation` and `OperationShapeExt` documentation -->
In contrast to the marker ZSTs above, the `Operation<S, L>` structure holds the actual runtime behavior of an operation, which is specified, during construction, by the customer. The `S` here is a model service, this is specified during construction of the `Operation<S, L>`. The constructors exist on the marker ZSTs as an extension trait to `OperationShape`, namely `OperationShapeExt`:
Copy link
Contributor Author

@hlbarber hlbarber Sep 29, 2022

Choose a reason for hiding this comment

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

In constrast...

No reason is given for why the implementation makes a distinction here. If the immediate reaction is to ask "Why is there a distinction between Op: OperationShape and Operation<S, L>?" then the documentation should provide a satisfactory explanation.


Notice that we can "upgrade" a model service to a HTTP service using `FromRequest` and `IntoResponse` described in the prior section:

![Upgrade Data Flow Diagram](imgs/upgrade-dfd.png)
Copy link
Contributor Author

@hlbarber hlbarber Sep 29, 2022

Choose a reason for hiding this comment

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

I think all of these diagrams look bad. It's supposed to be a data flow diagram. If anyone has any better ideas for these schematics please comment. UML sequence diagram perhaps? What about the diagrams which indicate composition?

Copy link
Contributor Author

@hlbarber hlbarber Sep 30, 2022

Choose a reason for hiding this comment

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

There's also the question of where should we host them? Is checking them into the commit OK? LFS? S3?

Copy link
Contributor

Choose a reason for hiding this comment

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

If you use mermaid, GitHub will automatically render the diagrams (see here as an example when you switch to preview mode). That can save us the hosting headache.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

mermaid looks incredible - this is a great idea.

Copy link
Contributor

Choose a reason for hiding this comment

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

mdbook-mermaid is already used in the CI pipeline as well, so they should render correctly on GitHub Pages as well (should we choose to index these in the book later on).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How're these?

e40aff9

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking sharp!

design/src/docs/anatomy.md Outdated Show resolved Hide resolved

As seen in the `Pluggable` documentation, third-parties can use extension traits over `Pluggable` to extend the API of builders.

## Accessing Unmodelled Data
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This section is quite brief. The full story amounts to pepper the code with Exts: FromParts<P> in a predictable way and can be read quite easily from the source code.

Should I go into more detail? Should I include the paragraph above?

design/src/docs/anatomy.md Outdated Show resolved Hide resolved
}
```

## Plugins
Copy link
Contributor Author

@hlbarber hlbarber Sep 29, 2022

Choose a reason for hiding this comment

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

There should be a standalone tutorial on actually writing a Plugin. And when there is we should link to it here.

design/src/docs/anatomy.md Outdated Show resolved Hide resolved
@hlbarber hlbarber marked this pull request as ready for review September 30, 2022 12:32
@hlbarber hlbarber requested review from a team as code owners September 30, 2022 12:32
@smithy-lang smithy-lang deleted a comment from github-actions bot Sep 30, 2022
@smithy-lang smithy-lang deleted a comment from github-actions bot Sep 30, 2022
@github-actions
Copy link

A new generated diff is ready to view.

A new doc preview is ready to view.

@smithy-lang smithy-lang deleted a comment from github-actions bot Sep 30, 2022
@smithy-lang smithy-lang deleted a comment from github-actions bot Sep 30, 2022
```rust
/// Provides a protocol aware extraction from a [`Request`]. This consumes the
/// [`Request`], in contrast to [`FromParts`].
pub trait FromRequest<Protocol>: Sized {
Copy link
Contributor Author

@hlbarber hlbarber Sep 30, 2022

Choose a reason for hiding this comment

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

These traits and others all have a loose counterparts in axum e.g. https://docs.rs/axum/latest/axum/extract/trait.FromRequest.html because

  1. We used to use axum extensively and inherited some of the machinery.
  2. These traits are occur as common solutions across all frameworks (Rocket, Actix).

Should we mention these facts?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's necessary, it's a fairly common approach in the Rust web ecosystem (as you stated).

}
```

Here `PluginStack` works in a similar way to [`tower::layer::util::Stack`](https://docs.rs/tower/latest/tower/layer/util/struct.Stack.html) - allowing users to append a new plugin rather than replacing the currently set one.
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not immediately clear to me when a user should opt for a Plugin instead of attaching a Layer to the overall Router.
A concrete example of common functionality that could only be accomplished with a Plugin instead of a Layer would ⭐

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a very good observation! Plugins allow us to modify the "upgrade procedure" from model services to HTTP services. There we can "squish in" middlewares that work at the model service layer, whereas the ones applied to the overall Router need to operate at the HTTP service layer, before routing has taken place. You'll notice this difference if you look closely at the diagrams.

You can pull the thread for the primary motivation for introducing plugins in item 2 in the Service Builder Improvements RFC, to perform operation-specific logging behavior, but there are other use cases where we want to modify behavior at the model service layer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How's this?
8ff688e

There's an unfortunate conflict, which exists for now, between the Router struct, which is essentially an enum of the protocol specific routers and the Router trait which is an interface for routing requests to services. This makes things more confusing for people today.

Copy link
Contributor

@david-perez david-perez left a comment

Choose a reason for hiding this comment

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

Excellent documentation. I read it in the headspace of a newcomer and in my opinion everything is very well-explained.

design/src/docs/anatomy.md Outdated Show resolved Hide resolved

Observe that there are two constructors provided: `from_handler` which takes a `H: Handler` and `from_service` which takes a `S: OperationService`. In both cases `Self` is passed as a parameter to the traits - this constrains `handler: H` and `svc: S` to the signature given by the implementation of `OperationShape` on `Self`.

The [`Handler`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/operation/handler.rs#L21-L29) and [`OperationService`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/operation/operation_service.rs#L15-L29) both serve a similar purpose - they provide a common interface for converting to a model service `S`. The `Handler<GetPokemonSpecies>` trait covers all closures taking `GetPokemonSpeciesInput` and asynchronously returning a `Result<GetPokemonSpeciesOutput, GetPokemonSpeciesError>` - they are converted to a model service by a `IntoService`. The `OperationService<GetPokemonSpecies>` trait covers all `tower::Service`s with request `GetPokemonSpeciesInput`, response `GetPokemonSpeciesOutput` and error `GetPokemonSpeciesOutput` - they are converted to a model service by a `Normalize` (this is a very small conversion which flattens request tuples).
Copy link
Contributor

Choose a reason for hiding this comment

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

closures and functions.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's the first time IntoService is mentioned, but it's not a link and the definition does not appear inline.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think readers will be confused by the mention of Normalize unless you go into more detail here. There are things you left out in the definition of OperationShapeExt above: PollError, Exts... I think it's fair game to omit IntoService here too and maybe cover it later in the text.

Perhaps a suitable educational narrative would be to start with a minimal definition of OperationShape and OperationShapeExt, and keep expanding them as requirements and problems are highlighted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's some detailed documentation in the operation.rs module. I'll link to that so they can see the details if they want.

design/src/docs/anatomy.md Outdated Show resolved Hide resolved
design/src/docs/anatomy.md Outdated Show resolved Hide resolved
design/src/docs/anatomy.md Outdated Show resolved Hide resolved
design/src/docs/anatomy.md Outdated Show resolved Hide resolved
}
```

The `builder` constructor provides a `PokemonServiceBuilder` where `build` cannot be called until all operations are set because `MissingOperation` purposefully doesn't implement `Upgradable`. In contrast, the `unchecked_builder` which sets all `Op{N}` to `FailOnMissingOperation` can be immediately built, however any unset operations are upgraded into a service which always returns status code 500, as noted in [Upgrading a Model Service](#upgrading-a-model-service).
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd omit unchecked_builder entirely. It's just going to make the reader raise their eyebrows as to its utility if we don't mention here the multi-Lambda based set up use case, which is not relevant to this text.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm on the fence here - if we don't have unchecked_builder and FailOnMissingOperation then we don't have an example of an alternative Upgradable::upgrade path which weakens the motivation for the Upgradable trait entirely - which might raise eyebrows.

design/src/docs/anatomy.md Outdated Show resolved Hide resolved
design/src/docs/anatomy.md Outdated Show resolved Hide resolved
}
```

Here `PluginStack` works in a similar way to [`tower::layer::util::Stack`](https://docs.rs/tower/latest/tower/layer/util/struct.Stack.html) - allowing users to append a new plugin rather than replacing the currently set one.
Copy link
Contributor

Choose a reason for hiding this comment

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

That's a very good observation! Plugins allow us to modify the "upgrade procedure" from model services to HTTP services. There we can "squish in" middlewares that work at the model service layer, whereas the ones applied to the overall Router need to operate at the HTTP service layer, before routing has taken place. You'll notice this difference if you look closely at the diagrams.

You can pull the thread for the primary motivation for introducing plugins in item 2 in the Service Builder Improvements RFC, to perform operation-specific logging behavior, but there are other use cases where we want to modify behavior at the model service layer.

@github-actions
Copy link

github-actions bot commented Oct 3, 2022

A new generated diff is ready to view.

A new doc preview is ready to view.

@smithy-lang smithy-lang deleted a comment from github-actions bot Oct 6, 2022
@smithy-lang smithy-lang deleted a comment from github-actions bot Oct 6, 2022
@smithy-lang smithy-lang deleted a comment from github-actions bot Oct 6, 2022
@smithy-lang smithy-lang deleted a comment from github-actions bot Oct 6, 2022
@smithy-lang smithy-lang deleted a comment from github-actions bot Oct 6, 2022
@smithy-lang smithy-lang deleted a comment from github-actions bot Oct 6, 2022
@github-actions
Copy link

A new generated diff is ready to view.

A new doc preview is ready to view.

Copy link
Contributor

@crisidev crisidev left a comment

Choose a reason for hiding this comment

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

Really good stuff here Harry!

@hlbarber hlbarber enabled auto-merge (squash) October 11, 2022 15:22
@smithy-lang smithy-lang deleted a comment from github-actions bot Oct 11, 2022
Copy link
Contributor

@jjant jjant left a comment

Choose a reason for hiding this comment

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

Great work @hlbarber!


## Accessing Unmodelled Data

An additional omitted detail is that we provide an "escape hatch" allowing `Handler`s and `OperationService`s to accept data that isn't modelled. In addition to accepting `Op::Input` they can accept additional arguments which implement the [`FromParts`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/request.rs#L114-L121) trait:
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have any example of using FromParts? If so, it would probably be useful to link it here.

@github-actions
Copy link

A new generated diff is ready to view.

A new doc preview is ready to view.

@hlbarber hlbarber force-pushed the harryb/anatomy-service branch from a47ba8b to 06b604d Compare October 11, 2022 18:10
@github-actions
Copy link

A new generated diff is ready to view.

A new doc preview is ready to view.

@hlbarber hlbarber merged commit bf3fbc2 into main Oct 11, 2022
@hlbarber hlbarber deleted the harryb/anatomy-service branch October 11, 2022 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation server Rust server SDK
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants