-
Notifications
You must be signed in to change notification settings - Fork 28
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
docs: add design for operation interceptors #758
Conversation
docs/design/interceptors.md
Outdated
|
||
* **Execution** - one end-to-end invocation against an SDK client | ||
|
||
* **Attempt** - a single attempt at performing an execution, executions may be retired multiple times |
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.
nit: retired (👴) -> retried
docs/design/interceptors.md
Outdated
* request pipeline. The duration between invocation of this hook and [readBeforeDeserialization] is very | ||
* close to the amount of time spent marshalling the request. | ||
* | ||
* **Available Information**: [RequestInterceptorContext.request] and |
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.
what is the use case for reading RequestInterceptorContext.request
after it has been serialized into protocolRequest
?
docs/design/interceptors.md
Outdated
): Any { | ||
val input = context.request as? FooInput ?: error("expected type FooInput") | ||
return if (input.bar == null) { | ||
return input.copy { |
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.
nit: unnecessary extra return
protocol clients would be generated to reference the more specific interceptor type: | ||
|
||
```kotlin | ||
typealias HttpInterceptor = Interceptor<Any, Any, HttpRequest, HttpResponse> |
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.
Minor: alternatively
typealias HttpInterceptor<I, O> = Interceptor<I, O, HttpRequest, HttpResponse>
typealias GenericHttpInterceptor = Interceptor<Any, Any, HttpRequest, HttpResponse>
(i.e. give them a typealias to use for operation-scoped interceptors AND the generic one).
docs/design/interceptors.md
Outdated
|
||
## Terminology | ||
|
||
* **Execution** - one end-to-end invocation against an SDK client |
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.
Style: This description sounds imprecise to me. Perhaps something like "an end-to-end invocation of an operation provided by a service client"?
docs/design/interceptors.md
Outdated
Each interceptor hook can: | ||
• Read the messages generated so far during in the execution | ||
• Save information to a context object that is available across all hooks | ||
• (Write Hooks Only) Modify the input message, transport request message, transport response message or output |
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.
Nit: Doesn't use *
bullets and thus renders as a single paragraph:
Each interceptor hook can: • Read the messages generated so far during in the execution • Save information to a context object that is available across all hooks • (Write Hooks Only) Modify the input message, transport request message, transport response message or output
public interface Interceptor< | ||
Input, | ||
Output, | ||
ProtocolRequest, | ||
ProtocolResponse, | ||
> { |
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.
Nit: > {
indentation should be 0 not 4
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.
not according to ktlint :)
* **When**: This will ALWAYS be called once per execution, except when a failure occurs earlier in the | ||
* request pipeline. |
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.
Comment: I don't find the "ALWAYS" part of these descriptions very useful, particularly when they're immediately negated by a subsequent "except" clause.
docs/design/interceptors.md
Outdated
* **When**: This will ALWAYS be called once per execution. The duration between | ||
* invocation of this hook and [readAfterExecution] is very close to full duration of the execution |
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.
Question: What is the value of describing the expected interval between hooks? How would users act on that information and what would be the consequences of these expectations being inaccurate?
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.
These descriptions largely follow ref arch. I see some minor value as it helps infer how the operation is executed but I don't feel strongly about whether we include this verbiage or not.
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.
I think describing how the operation is executed is very important but probably as a cohesive, higher-level diagram/description and not as implications from the descriptions of intervals.
docs/design/interceptors.md
Outdated
access the operation'output in any hook until *after* the first deserialization hook). Additionally the hooks are | ||
specific about the nullability of particular context information (e.g. the `modifyBeforeCompletion` hook may or may not | ||
have a protocol request or response available depending on how far the operation execution made it. | ||
* Hooks are non-blocking (note no `suspend` in any hook method) and implementators are expected to respect this |
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.
Nit: "implementators" → "implementors" or "implementations"
* Despite being generic over the input and output type, the `Input` and `Output` types of an `Interceptor` are expected | ||
to be `Any` in practice. This is because interceptors can be registered at the *client* level. Such an interceptor would | ||
be executed for every operation and would not be able to know the specific input or output type at compile time. | ||
Interceptors that execute for any operation are common and expected (e.g. add a header to every outgoing request). | ||
The downside to this flexibility is that customers will have to cast the input or output type from the context to the | ||
specific operation type. | ||
* NOTE: The modeled operation name will be available in the context (it is already available in the execution context) | ||
which can be used on interceptors registered at the client level that want to deal with specific operations. |
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.
Comment: In principle, what do we think about providing types for hooks which only execute on a single operation (and ignore other operations)? The benefit would be allowing users to easily add strongly-typed interceptors without casting or checking themselves:
val client = S3Client {
interceptors += ListObjectsMaxKeysInterceptor(10)
}
class ListObjectsMaxKeysInterceptor(defaultMaxKeys: Int): ListObjectsInterceptor() {
override fun modifyBeforeSerialization(context: RequestInterceptorContext<ListObjectsRequest>) = when {
context.request.maxKeys == null -> context.request.copy { maxKeys = defaultMaxKeys }
else -> context.request
}
}
To achieve this, I think we'd need the following:
Remove input/output types on Interceptor
interface Interceptor<ProtocolRequest, ProtocolResponse> {
fun modifyBeforeSerialization(context: RequestInterceptorContext<Any>): Any = context.request
fun modifyBeforeCompletion(context: ResponseInterceptorContext<Any, Any, ProtocolRequest?, ProtocolResponse?>): Result<Any> = context.response
}
typealias HttpInterceptor = Interceptor<HttpRequest, HttpResponse>
(only including a few hooks for demonstration purposes)
Add a new TypedInterceptor
with input/output types (effectively the old Interceptor
)
interface TypedInterceptor<Input, Output, ProtocolRequest, ProtocolResponse> {
fun modifyBeforeSerialization(context: RequestInterceptorContext<Input>): Input = context.request
fun modifyBeforeCompletion(context: ResponseInterceptorContext<Input, Output, ProtocolRequest?, ProtocolResponse?>): Result<Output> = context.response
}
typealias TypedHttpInterceptor<Input, Output> = TypedInterceptor<Input, Output, HttpRequest, HttpResponse>
Codegen per-op interceptor classes
Example for ListObjects:
open class ListObjectsInterceptor : HttpInterceptor, TypedHttpInterceptor<ListObjectsRequest, ListObjectsResponse> {
final override fun modifyBeforeSerialization(context: RequestInterceptorContext<Any>): Any = when {
context.opName == "ListObjects" -> {
val typedContext = object : RequestInterceptorContext<ListObjectsRequest> {
override val opName = "ListObjects"
override val request = context.request as ListObjectsRequest
}
modifyBeforeSerialization(typedContext)
}
else -> context.request
}
final override fun modifyBeforeCompletion(context: ResponseInterceptorContext<Any, Any, HttpRequest?, HttpResponse?>): Result<Any> = when {
context.opName == "ListObjects" -> {
val typedContext = object : ResponseInterceptorContext<ListObjectsRequest, ListObjectsResponse, HttpRequest?, HttpResponse?> {
override val opName = "ListObjects"
override val request = context.request as ListObjectsRequest
override val protocolRequest = context.protocolRequest
override val protocolResponse = context.protocolResponse
override val response = context.response as Result<ListObjectsResponse>
}
modifyBeforeCompletion(typedContext)
}
else -> context.response
}
}
Ignore the inline anonymous contexts, I'm assuming we'll have some default implementation class to use at runtime. Also, the operation name checking and casting may be reusable enough to encapsulate in a base class to shrink the per-op interceptor classes down to just the types and operation name...not sure yet.
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.
Ya something like this may work and is in back of my mind. I actually think we may not even need new types depending on if we thread Any
as a bound on some of the generics.
Let's get interceptors wired up and before we land it come back to revisit typed interceptors (it will give us more room to play with these ideas once we have a working framework).
docs/design/interceptors.md
Outdated
|
||
|
||
The `Interceptor` interface defined above would not be useful to a customer without specific types. Each protocol will | ||
get it's own `typealias` that fills in the generics that make sense for a particular protocol. For instance HTTP based |
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.
Nit: "it's" → "its"
Interceptors will be able to be registered at client creation time as well as through runtime plugins (TBD). | ||
|
||
|
||
Generated client config will be modified to include a list of interceptors that can be appended to: |
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.
Comment: May want to include a note indicating that these config-specified interceptors are strictly additive to any pre-existing, non-configurable interceptors.
docs/design/interceptors.md
Outdated
* Interceptors registered via. smithy default plugins | ||
* (AWS Services) Interceptors registered via AWS default plugins (AWS SDKs only) | ||
* Interceptors registered via. service-customization plugins | ||
* Interceptors registered via. client-level plugins | ||
* Interceptors registered via. client-level configuration | ||
* Interceptors registered via. operation-level plugins | ||
* Interceptors registered via operation-level configuration |
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.
Style: Repeating "Interceptors registered via" on every item is unnecessary. Also, this may be clearer as a numbered (rather than bulleted) list.
Kudos, SonarCloud Quality Gate passed! |
Issue #
#122
Description of changes
Adds design doc for interceptors
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.