commonMain.aws.smithy.kotlin.runtime.http.operation.SdkHttpOperation.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.client.SdkClientOption
import aws.smithy.kotlin.runtime.collections.get
import aws.smithy.kotlin.runtime.http.HttpHandler
import aws.smithy.kotlin.runtime.http.complete
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
import aws.smithy.kotlin.runtime.operation.ExecutionContext
import aws.smithy.kotlin.runtime.telemetry.trace.withSpan
import aws.smithy.kotlin.runtime.util.Uuid
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.job
import kotlin.reflect.KClass
/**
* A (Smithy) HTTP based operation.
* @property execution Phases used to execute the operation request and get a response instance
* @property context An [ExecutionContext] instance scoped to this operation
* @property serializer The component responsible for serializing the input type `I` into an HTTP request builder
* @property deserializer The component responsible for deserializing an HTTP response into the output type `O`
* @property typeInfo the operation type info used internally for interceptors to function correctly
* @property telemetry the telemetry parameters used to instrument the operation with
*/
@InternalApi
public class SdkHttpOperation internal constructor(
public val execution: SdkOperationExecution,
public val context: ExecutionContext,
internal val serializer: HttpSerializer,
internal val deserializer: HttpDeserializer,
internal val typeInfo: OperationTypeInfo,
internal val telemetry: SdkOperationTelemetry,
) {
@Suppress("DEPRECATION")
internal constructor(
execution: SdkOperationExecution,
context: ExecutionContext,
serializer: HttpSerialize,
deserializer: HttpDeserialize,
typeInfo: OperationTypeInfo,
telemetry: SdkOperationTelemetry,
) : this(execution, context, serializer.intoSerializer(), deserializer.intoDeserializer(), typeInfo, telemetry)
init {
context[HttpOperationContext.SdkInvocationId] = Uuid.random().toString()
}
/**
* Interceptors that will be executed as part of this operation. The difference between phases and interceptors
* is the former is internal only whereas the latter is external customer facing. Middleware is also allowed to
* suspend whereas interceptors are meant to be executed quickly.
*/
public val interceptors: MutableList = mutableListOf()
/**
* Install a middleware into this operation's execution stack
*/
public fun install(middleware: ModifyRequestMiddleware) {
middleware.install(this)
}
// Convenience overloads for various types of middleware that target different phases
// NOTE: Using install isn't strictly necessary, it's just a pattern for self registration
public fun install(middleware: InitializeMiddleware) {
middleware.install(this)
}
public fun install(middleware: MutateMiddleware) {
middleware.install(this)
}
public fun install(middleware: ReceiveMiddleware) {
middleware.install(this)
}
public fun install(middleware: InlineMiddleware) {
middleware.install(this)
}
@InternalApi
public companion object {
public inline fun build(block: SdkHttpOperationBuilder.() -> Unit): SdkHttpOperation =
SdkHttpOperationBuilder(
I::class,
O::class,
).apply(block).build()
}
}
/**
* Gets the unique ID that identifies the active SDK request in this [ExecutionContext].
*/
@InternalApi
public val ExecutionContext.sdkInvocationId: String
get() = get(HttpOperationContext.SdkInvocationId)
/**
* Round trip an operation using the given [HttpHandler]
*/
@InternalApi
public suspend fun SdkHttpOperation.roundTrip(
httpHandler: HttpHandler,
input: I,
): O = execute(httpHandler, input) { it }
/**
* Make an operation request with the given [input] and return the result of executing [block] with the output.
*
* The response and any resources will remain open until the end of the [block]. This facilitates streaming
* output responses where the underlying raw HTTP connection needs to remain open
*/
@InternalApi
public suspend fun SdkHttpOperation.execute(
httpHandler: HttpHandler,
input: I,
block: suspend (O) -> R,
): R {
val handler = execution.decorate(httpHandler, this)
val request = OperationRequest(context, input)
val (span, telemetryCtx) = instrument()
try {
return withSpan(span, telemetryCtx) {
val output = handler.call(request)
block(output)
}
} finally {
context.cleanup()
}
}
internal data class OperationTypeInfo(
val inputType: KClass<*>,
val outputType: KClass<*>,
)
@InternalApi
public class SdkHttpOperationBuilder(
private val inputType: KClass<*>,
private val outputType: KClass<*>,
) {
public val telemetry: SdkOperationTelemetry = SdkOperationTelemetry()
@Suppress("DEPRECATION")
@Deprecated("use serializeWith")
public var serializer: HttpSerialize? = null
set(value) {
field = value
serializeWith = value?.intoSerializer()
}
public var serializeWith: HttpSerializer? = null
@Suppress("DEPRECATION")
@Deprecated("use deserializeWith")
public var deserializer: HttpDeserialize? = null
set(value) {
field = value
deserializeWith = value?.intoDeserializer()
}
public var deserializeWith: HttpDeserializer? = null
public val execution: SdkOperationExecution = SdkOperationExecution()
public val context: ExecutionContext = ExecutionContext()
/**
* The name of the operation
*/
public var operationName: String? = null
/**
* The name of the service the request is sent to
*/
public var serviceName: String? = null
/**
* (Optional) prefix to prepend to a (resolved) hostname
*/
public var hostPrefix: String? = null
public fun build(): SdkHttpOperation {
val opSerializer = requireNotNull(serializeWith) { "SdkHttpOperation.serializeWith must not be null" }
val opDeserializer = requireNotNull(deserializeWith) { "SdkHttpOperation.deserializeWith must not be null" }
requireNotNull(operationName) { "operationName is a required HTTP execution attribute" }
requireNotNull(serviceName) { "serviceName is a required HTTP execution attribute" }
context[SdkClientOption.OperationName] = operationName!!
context[SdkClientOption.ServiceName] = serviceName!!
hostPrefix?.let { context[HttpOperationContext.HostPrefix] = it }
val typeInfo = OperationTypeInfo(inputType, outputType)
return SdkHttpOperation(execution, context, opSerializer, opDeserializer, typeInfo, telemetry)
}
}
/**
* Configure HTTP operation context elements
*/
@InternalApi
public inline fun SdkHttpOperationBuilder.context(block: ExecutionContext.() -> Unit) {
context.apply(block)
}
private suspend fun ExecutionContext.cleanup() {
// pull the raw response(s) out of the context and cleanup any resources
getOrNull(HttpOperationContext.HttpCallList)?.forEach { it.complete() }
// at this point everything associated with this single operation should be cleaned up
coroutineContext.job.cancelAndJoin()
}