commonMain.aws.smithy.kotlin.runtime.http.operation.SdkOperationExecution.kt Maven / Gradle / Ivy
The newest version!
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.smithy.kotlin.runtime.http.operation
import aws.smithy.kotlin.runtime.InternalApi
import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics
import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric
import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric
import aws.smithy.kotlin.runtime.client.LogMode
import aws.smithy.kotlin.runtime.client.endpoints.authOptions
import aws.smithy.kotlin.runtime.client.logMode
import aws.smithy.kotlin.runtime.collections.attributesOf
import aws.smithy.kotlin.runtime.collections.emptyAttributes
import aws.smithy.kotlin.runtime.collections.merge
import aws.smithy.kotlin.runtime.http.*
import aws.smithy.kotlin.runtime.http.auth.SignHttpRequest
import aws.smithy.kotlin.runtime.http.interceptors.InterceptorExecutor
import aws.smithy.kotlin.runtime.http.middleware.RetryMiddleware
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
import aws.smithy.kotlin.runtime.http.request.dumpRequest
import aws.smithy.kotlin.runtime.http.request.immutableView
import aws.smithy.kotlin.runtime.http.request.toBuilder
import aws.smithy.kotlin.runtime.http.response.dumpResponse
import aws.smithy.kotlin.runtime.identity.Identity
import aws.smithy.kotlin.runtime.io.Handler
import aws.smithy.kotlin.runtime.io.middleware.Middleware
import aws.smithy.kotlin.runtime.io.middleware.Phase
import aws.smithy.kotlin.runtime.operation.ExecutionContext
import aws.smithy.kotlin.runtime.retries.RetryStrategy
import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy
import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy
import aws.smithy.kotlin.runtime.retries.policy.StandardRetryPolicy
import aws.smithy.kotlin.runtime.telemetry.logging.debug
import aws.smithy.kotlin.runtime.telemetry.logging.logger
import aws.smithy.kotlin.runtime.telemetry.logging.trace
import aws.smithy.kotlin.runtime.telemetry.metrics.measureSeconds
import kotlin.coroutines.coroutineContext
import aws.smithy.kotlin.runtime.io.middleware.decorate as decorateHandler
@InternalApi
public typealias SdkHttpRequest = OperationRequest
/**
* Configure the execution of an operation from [Request] to [Response].
*
* An operation has several "phases" of its lifecycle that can be intercepted and customized.
* Technically any phase can act on the request or the response. The phases are named with their intended use; however,
* nothing prevents e.g. registering something in [initialize] that acts on the output type though.
*
* ## Middleware Phases
*
* ```
* Initialize ---> ---> Mutate
* |
* v
*
* |
* v
* +---------------------+
* | OnEachAttempt |
* | | |
* | v |
* | |
* | | |
* | v |
* | |
* | | |
* | v |
* | |
* | | |
* | v |
* | |
* | | |
* | v |
* | Receive |
* | | |
* | v |
* | |
* +---------------------+
* ```
*
* In the above diagram the phase relationships and sequencing are shown. Requests start at [initialize] and proceed
* until the actual HTTP client engine call. The response then is returned up the call stack and traverses the phases
* in reverse.
*
* Phases enclosed in brackets `<>` are implicit phases that are always present and cannot be intercepted directly
* (only one is allowed to exist). These are usually configured directly when the operation is built.
*
* ### Default Behaviors
*
* By default, every operation is:
* * Retried using the configured [retryStrategy] and [retryPolicy].
* * Signed using the resolved authentication scheme
*/
@InternalApi
public class SdkOperationExecution {
/**
* **Request Middlewares**: Prepare the input [Request] (e.g. set any default parameters if needed) before
* serialization
*
* **Response Middlewares**: Finalize the [Response] before returning it to the caller
*/
public val initialize: Phase, Response> = Phase, Response>()
/**
* **Request Middlewares**: Modify the outgoing HTTP request. This phase runs BEFORE the retry loop and
* is suitable for any middleware that only needs to run once and does not need to modify the outgoing request
* per/attempt.
*
* At this phase the [Request] (operation input) has been serialized to an HTTP request.
*
* **Response Middlewares**: Modify the output after deserialization
*/
public val mutate: Phase = Phase()
/**
* **Request Middlewares**: Modify the outgoing HTTP request. This phase is conceptually the same as [mutate]
* but it runs on every attempt. Each attempt will not see modifications made by previous attempts.
*
* **Response Middlewares**: Modify the output after deserialization on a per/attempt basis.
*/
public val onEachAttempt: Phase = Phase()
/**
* **Request Middlewares**: First chance to intercept after signing (and last chance before the final request is sent).
*
* **Response Middlewares**: First chance to intercept after the raw HTTP response is received and before deserialization
* into operation output type [Response].
*/
public val receive: Phase = Phase()
/**
* The authentication config to use. Defaults to [OperationAuthConfig.Anonymous] which uses
* anonymous authentication (no auth).
*/
public var auth: OperationAuthConfig = OperationAuthConfig.Anonymous
/**
* The endpoint resolver for the operation
*/
public var endpointResolver: EndpointResolver? = null
/**
* The retry strategy to use. Defaults to [StandardRetryStrategy]
*/
public var retryStrategy: RetryStrategy = StandardRetryStrategy()
/**
* The retry policy to use. Defaults to [StandardRetryPolicy.Default]
*/
public var retryPolicy: RetryPolicy = StandardRetryPolicy.Default
}
/**
* Decorate the "raw" [HttpHandler] with the execution phases (middleware) of this operation and
* return a handler to be used specifically for the given operation.
*/
internal fun SdkOperationExecution.decorate(
handler: HttpHandler,
op: SdkHttpOperation,
): Handler, Response> {
val interceptors = InterceptorExecutor(op.context, op.interceptors, op.typeInfo)
// ensure http calls are tracked
receive.register(HttpCallMiddleware())
// run before trace middleware because interceptors can modify right before sending to an engine
receive.register(InterceptorTransmitMiddleware(interceptors))
receive.intercept(Phase.Order.After, ::httpTraceMiddleware)
val receiveHandler = decorateHandler(handler, receive)
val deserializeHandler = op.deserializer.decorate(receiveHandler, interceptors)
val authHandler = AuthHandler(deserializeHandler, interceptors, auth, endpointResolver)
val onEachAttemptHandler = decorateHandler(authHandler, onEachAttempt)
val retryHandler = decorateHandler(onEachAttemptHandler, RetryMiddleware(retryStrategy, retryPolicy, interceptors))
val mutateHandler = decorateHandler(MutateHandler(retryHandler), mutate)
val serializeHandler = op.serializer.decorate(mutateHandler, interceptors)
val initializeHandler = decorateHandler(InitializeHandler(serializeHandler), initialize)
return OperationHandler(initializeHandler, interceptors)
}
private fun HttpSerializer.decorate(
inner: Handler,
interceptors: InterceptorExecutor,
): Handler, O> = SerializeHandler(inner, this, interceptors)
private fun HttpDeserializer.decorate(
inner: Handler,
interceptors: InterceptorExecutor,
): Handler = DeserializeHandler(inner, this, interceptors)
// internal glue used to marry one phase to another
/**
* Outermost handler that wraps the entire middleware pipeline and handles interceptor hooks related
* to the start/end of an operation
*/
private class OperationHandler(
private val inner: Handler, Output>,
private val interceptors: InterceptorExecutor,
) : Handler, Output> {
override suspend fun call(request: OperationRequest): Output {
coroutineContext.trace> { "operation started" }
val result = interceptors.readBeforeExecution(request.subject)
.mapCatching {
inner.call(request)
}
.let {
interceptors.modifyBeforeCompletion(it)
}
.let {
interceptors.readAfterExecution(it)
}
when {
result.isSuccess -> coroutineContext.trace> { "operation completed successfully" }
result.isFailure -> coroutineContext.trace>(result.exceptionOrNull()) { "operation failed" }
}
return result.getOrThrow()
}
}
private class InitializeHandler(
private val inner: Handler,
) : Handler {
override suspend fun call(request: Input): Output = inner.call(request)
}
private class SerializeHandler(
private val inner: Handler,
private val serializer: HttpSerializer,
private val interceptors: InterceptorExecutor,
) : Handler, Output> {
override suspend fun call(request: OperationRequest): Output {
val modified = interceptors.modifyBeforeSerialization(request.subject)
.let { request.copy(subject = it) }
interceptors.readBeforeSerialization(modified.subject)
// store finalized operation input for later middleware to read if needed
request.context[HttpOperationContext.OperationInput] = modified.subject as Any
val requestBuilder = when (serializer) {
is HttpSerializer.NonStreaming -> serializer.serialize(modified.context, modified.subject)
is HttpSerializer.Streaming -> serializer.serialize(modified.context, modified.subject)
}
interceptors.readAfterSerialization(requestBuilder.immutableView())
return inner.call(SdkHttpRequest(modified.context, requestBuilder))
}
}
private class MutateHandler