Detailed Upgrade Guidance for 0.56 Release #853
jdisanti
announced in
Change Log
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Generated smithy-rs clients have undergone an architectural overhaul that replaces the tower
Service
trait (referred to as the “middleware” implementation) with a new orchestration system that will be referred to as the “orchestrator” for the rest of this post. Users that implement middleware via towerService
or any of ourMapRequest
layers will experience significant breaking changes that will require refactoring to resolve.This new implementation brings a much easier client creation API, and a less error-prone API for customizing the client that has more extension points and encourages best practices. It also brings smithy-rs generated clients closer to how generated clients work in other code generators (e.g.,
smithy-kotlin
,smithy-swift
, and others in the future) so that customizations written for one language’s Smithy clients can be easily ported to another language. In the long term, the overall high-level client architecture will be the same across the board for Smithy clients in all languages.At the highest level, the orchestrator can be described by its parts:
When a request is made, the config is augmented by the runtime plugins, and the runtime components for that request are established by the runtime plugins. The list of interceptors is retrieved from config, and then the orchestrator goes through a fixed request execution pipeline with various hook points for customization with interceptors and runtime components.
There isn’t a ton of documentation on how this all works under the hood yet, but we do have the following docs, as well as the docs of the
aws-smithy-runtime-api
andaws-smithy-runtime
crates, which cover the finer details.aws-smithy-runtime-api
docs (note: it may take a few days for docs.rs to update these after release)aws-smithy-runtime
docsMiddleware deprecation
The release of 0.56 makes the new orchestrator the default client implementation. The middleware implementation is still around and can be opted into if necessary, but this his highly discouraged as it will be completely removed and unsupported in the next minor (non-patch) smithy-rs release. If you find that your app’s not behaving as expected after the upgrade, you have a few options:
v0.55.0
.Interceptors aren’t middleware
But they’re pretty close. Here are the key differences:
Interceptors are a generic extension point with the goal of supporting “anything someone should be able to do” as opposed to “anything anyone might want to do.” Interceptors allow users to “inject” logic into specific stages of the request/response lifecycle. These stages are known as “hooks”.
Hooks are either read-only (read) or read/write (modify). The current list of hooks is as follows:
A hook will always run all applicable interceptors, even if one fails. If one or more interceptors run by a hook fail, orchestrator execution will skip ahead. The hook that is skipped to depends on whether the retry loop was entered. The Modify Before Attempt Completion and Read After Attempt hooks will always run if an interceptor fails during the retry loop. The Modify Before Execution Completion and Read After Execution hooks will always run.
If you’re relying on custom middlewares, you’ll need to convert them to the interceptor pattern in order to upgrade the SDK. Let’s look at two of the middlewares the SDK team had to convert as an example.
What hook(s) should I implement for my interceptor?
While it’s possible for a single interceptor to implement all of the above hooks at once, implementing one is enough for most use-cases. The question, then, is “which one should I implement?” When making this decision, here are a few things to consider:
ConfigBag
, when is that value set?NOTE: The most common hook implemented by our internal interceptors is Modify Before Signing. The second most common hook is Modify Before Transmit*.
*
What if my middleware modifies or replaces the request body?
Middleware that completely replace the HTTP request body will not translate to interceptors well. You can use an interceptor to
std::mem::swap
the HTTP body out with another one (andSdkBody
can wrap any arbitrary streaming body implementation), but unless you replace the body with an in-memory variant of SdkBody, you will lose the ability to retry. Note: you would have potentially lost the ability to retry in middleware also, depending on how the middleware was implemented.If you need to replace the HTTP body with one that is streaming as opposed to in-memory, you should do it as a custom
HttpConnector
instead of trying to do it in an interceptor.Setting interceptors
All necessary interceptors are set by default, but users may want to define and set their own. Interceptors can be added one-at-a-time or in a list:
The above method works by taking ownership of
self
. The two methods below take a mutable reference toself
.Example #1: Updating the recursion detection middleware
The recursion detection middleware is simple, but has a very important job. It detects when an SDK is being run in a Lambda and inserts a special tracking header so that services handling the request can avoid infinitely recursive lambda invocations.
Both the middleware and interceptors implementations rely on the same inner function:
The middleware is defined as a struct that implements the
MapRequest
helper trait:When converted to the new interceptor pattern, it looks like this:
Not so different, right? The most significant decision when converting simple middleware into interceptors is what hook(s) to implement. In this case, we chose the
modify_before_signing
hook, inserting the trace ID header right before signing the request. That way, the configured signer may choose to sign it.Example #2: Reconnecting when retrying 50X errors
By default, the AWS SDK for Rust won’t re-use a connection if it’s retrying a server error. This helps avoid sending another request to a server that’s struggling to respond, instead allowing another server to handle it. The way this is implemented is a bit complex but that’s what makes it a good example.
The
PoisonService
middleware is responsible for “poisoning” connections to servers that have returned 50X errors. When a connection is “poisoned” that signals to the underlying HTTP client that it shouldn’t be re-used for future requests.Because the
PoisonService
is a bit more complex, it implements thetower::Service
trait instead of theMapRequest
helper trait:When converted to an interceptor, we end up with this:
Summary
We hope this new method of extending the request/response lifecycle will be easy to use, but if you have trouble upgrading your middlewares, please file an issue and we’ll do our best to advise you.
Config hasn’t changed much
While the internals of SDK configuration has changed, we’ve put effort into ensuring the user experience remains the same.
SdkConfig
and service specific configs will have all the same fields they did before, as well as a few additions:push_interceptor
: Add aSharedInterceptor
that runs for specific hooks in the request/response lifecycle. Generally speaking, interceptors set to run on a hook are executedaccording to the pre-defined priorityin a non-deterministic order. However, the default set of interceptors provided by the SDK are guaranteed to run before interceptors set by the user, ensuring the user always has the final say.SharedInterceptor
is a wrapper type for structs implementingInterceptor
.time_source
: Set the time source for a client. The time source is used to provide the current time. By default, this will use the host machine’s system time. If you’re running on WASM, you’ll have to set your own time source depending on your specific runtime.Generic clients are simpler to use
If you’re generating a generic smithy client, client configuration and construction works just like it does for SDK clients, but with less defaults:
CustomizableOperation::map_operation
has been removedA customizable operation allows a user to modify the current operation before it is dispatched.
map_request
andmutate_request
onCustomizableOperation
can be used in the orchestrator in the exactly the same way as before. However,map_operation
will no longer be supported since the orchestrator does not use theaws_smithy_http::operation::Operation
struct.The following examples demonstrate how to upgrade existing code that uses
map_operation
in order to achieve the same functionality in the orchestrator.Example #1: Inserting an item into a property bag
Previously
map_operation
allowed users to put an item intoaws_smithy_http::property_bag::PropertyBag
like so:This will no longer compile in the orchestrator because
Operation
will cease to exist. You can upgrade the above code as follows (or any use case that inserts an item into thePropertyBag
for that matter):Just remember that items to be stored using
store_put
need to implement the Storable trait (here is an example of how a type can be madeStorable
).Furthermore, the
Interceptor
trait defines several methods, and which one to use depends on the existing code to upgrade (the example above happens to usemodify_before_signing
). Refer back toWhat hook(s) should I implement for my interceptor?
for more details.Example #2: Extracting information from
aws_smithy_http::operation::Metadata
map_operation
used to allow users to extract fields in aMetadata
, which is a struct attached to an operation that identifies the API being called. It can be useful to obtain a service name and an operation name for a use case such as metrics collection. For instance,This piece of code can be upgraded as follows. You need to define your interceptor that extracts a
Metadata
out ofConfigBag
:Beta Was this translation helpful? Give feedback.
All reactions