Skip to content

Commit 6e221cc

Browse files
authored
refactor: make HttpCall context explicit (#939)
1 parent c1d88cb commit 6e221cc

File tree

55 files changed

+371
-211
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+371
-211
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "5cd4e551-c1b1-486a-b2f2-e6d0066de913",
3+
"type": "misc",
4+
"description": "**BREAKING**: Refactor HttpCall and HttpResponse to not be data classes and make the call context more explicit"
5+
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import software.amazon.smithy.kotlin.codegen.model.toSymbol
1717
object RuntimeTypes {
1818
object Http : RuntimeTypePackage(KotlinDependency.HTTP) {
1919
val HttpBody = symbol("HttpBody")
20+
val HttpCall = symbol("HttpCall")
2021
val HttpMethod = symbol("HttpMethod")
2122
val ByteArrayContent = symbol("ByteArrayContent", subpackage = "content")
2223
val readAll = symbol("readAll")
@@ -41,7 +42,6 @@ object RuntimeTypes {
4142
}
4243

4344
object Response : RuntimeTypePackage(KotlinDependency.HTTP, "response") {
44-
val HttpCall = symbol("HttpCall")
4545
val HttpResponse = symbol("HttpResponse")
4646
}
4747
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGenerator.kt

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
7373
* The function should have the following signature:
7474
*
7575
* ```
76-
* suspend fun throwFooOperationError(context: ExecutionContext, response: HttpResponse): Nothing {
76+
* suspend fun throwFooOperationError(context: ExecutionContext, call: HttpCall): Nothing {
7777
* <-- CURRENT WRITER CONTEXT -->
7878
* }
7979
* ```
@@ -494,19 +494,11 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
494494

495495
reference(outputSymbol, SymbolReference.ContextOption.DECLARE)
496496
}
497-
val operationDeserializerSymbols = setOf(
498-
RuntimeTypes.HttpClient.Operation.HttpDeserialize,
499-
RuntimeTypes.Http.Response.HttpResponse,
500-
)
501497

502498
val resolver = getProtocolHttpBindingResolver(ctx.model, ctx.service)
503499
val responseBindings = resolver.responseBindings(op)
504500
ctx.delegator.useSymbolWriter(deserializerSymbol) { writer ->
505-
// import all of http, http.response , and serde packages. All serializers requires one or more of the symbols
506-
// and most require quite a few. Rather than try and figure out which specific ones are used just take them
507-
// all to ensure all the various DSL builders are available, etc
508501
writer
509-
.addImport(operationDeserializerSymbols)
510502
.write("")
511503
.openBlock(
512504
"internal class #T: #T<#T> {",
@@ -529,7 +521,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
529521
writer.addImport(RuntimeTypes.Http.isSuccess)
530522
writer.withBlock("if (!response.status.#T()) {", "}", RuntimeTypes.Http.isSuccess) {
531523
val errorHandlerFn = operationErrorHandler(ctx, op)
532-
write("#T(context, response)", errorHandlerFn)
524+
write("#T(context, call)", errorHandlerFn)
533525
}
534526
}
535527

@@ -583,11 +575,12 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
583575
writer
584576
.addImport(RuntimeTypes.Core.ExecutionContext)
585577
.openBlock(
586-
"override suspend fun deserialize(context: #T, response: #T): #T {",
578+
"override suspend fun deserialize(context: #T, call: #T): #T {",
587579
RuntimeTypes.Core.ExecutionContext,
588-
RuntimeTypes.Http.Response.HttpResponse,
580+
RuntimeTypes.Http.HttpCall,
589581
outputSymbol,
590582
)
583+
.write("val response = call.response")
591584
.call {
592585
if (outputSymbol.shape?.isError == false && op != null) {
593586
// handle operation errors

codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,10 @@ internal class ConstantQueryStringOperationSerializer: HttpSerialize<ConstantQue
365365
val expectedContents = """
366366
internal class SmokeTestOperationDeserializer: HttpDeserialize<SmokeTestResponse> {
367367
368-
override suspend fun deserialize(context: ExecutionContext, response: HttpResponse): SmokeTestResponse {
368+
override suspend fun deserialize(context: ExecutionContext, call: HttpCall): SmokeTestResponse {
369+
val response = call.response
369370
if (!response.status.isSuccess()) {
370-
throwSmokeTestError(context, response)
371+
throwSmokeTestError(context, call)
371372
}
372373
val builder = SmokeTestResponse.Builder()
373374
42 KB
Loading
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Structured Concurrency
2+
3+
This document gives an overview of the [CoroutineScope](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/)(s)
4+
used by the SDK. It is meant to be a living document and provide additional context and guidance for SDK authors.
5+
6+
## Definitions
7+
8+
* **Coroutine** - An instance of a suspendable computation. A coroutine itself is represented by a Job. It is responsible for coroutine’s lifecycle, cancellation, and parent-child relations.
9+
* **CoroutineContext** - Persistent (immutable) context for a coroutine
10+
* **CoroutineScope** - Defines a scope for new coroutines which delimits the lifetime of the coroutine
11+
* **Structured Concurrency** - A principle expected by coroutines (in Kotlin) whereby new coroutines can only be launched into a specific CoroutineScope. This constraint ensures that coroutines are not leaked and that errors are propagated correctly. An outer scope cannot complete until all its child coroutines complete.
12+
* **HttpCall** - A single HTTP request/response pair.
13+
* **HttpClientEngine** - A component responsible for executing an HTTP request and returning an HttpCall
14+
* **ExecutionContext** - Operation scoped (mutable) context used to drive an operation to completion
15+
16+
17+
## Scopes
18+
19+
The SDK has three implementations of `CoroutineScope`:
20+
21+
1. [ExecutionContext](https://github.com/awslabs/smithy-kotlin/blob/main/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/operation/ExecutionContext.kt)
22+
2. [HttpClientEngine](https://github.com/awslabs/smithy-kotlin/blob/main/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/engine/HttpClientEngine.kt)
23+
3. [HttpCall](https://github.com/awslabs/smithy-kotlin/blob/main/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/response/HttpCall.kt)
24+
25+
26+
`ExecutionContext` implements `CoroutineScope` to provide a place for any background work to be done as part of implementing an operation.
27+
Its scope begins and ends with an operation. The only place it is currently utilized is to launch background task(s) to process input event streams
28+
(pull data from the customer input event stream, transform it, and write it to the HTTP body).
29+
30+
`HttpClientEngine` implements `CoroutineScope` and is used to launch HTTP requests. The scope of the engine is from creation to when it is closed.
31+
Making HTTP request tasks children of this scope means that an engine won’t be shutdown until in-flight requests are complete.
32+
The engine scope is a [SupervisorJob](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html) such that individual HTTP requests can fail independent of one another without causing all requests to be cancelled.
33+
34+
`HttpCall` implements `CoroutineScope` to provide a place for any background work required to fulfill an HTTP request/response. It is described
35+
in more detail in the following section.
36+
37+
### HttpCall Scope
38+
39+
Individual HTTP calls get their own `CoroutineContext` whose scope begins with executing the request and ends when `HttpCall::complete()` is invoked.
40+
This context/scope is referred to as the "call context". The call context is tied to the `HttpEngine` and is used primarily to launch background work
41+
required to fulfill executing the request and response. It is the parent to the `HttpClientEngine::roundTrip` invocation as well as any coroutines
42+
launched required to send the request body or fulfill the response body (e.g. reading off the wire and adapting to the the SDK I/O primitives).
43+
44+
Visually the parent/child relationships look like this:
45+
46+
![HttpCall Context](resources/http-call-context.png)
47+
48+
49+
The call context is _not_ a child of the operation context it is invoked from. It is however configured (manually) to be cancelled if the caller cancels the operation (dotted red line).
50+
51+
### FAQ
52+
53+
**Why is the call context not a child of the operation context?**
54+
55+
By the time `roundTrip` returns we will have executed the request and received everything but the response body. The only work that should be launched in the background as a child of the call context at this point is work required to fulfill the HTTP response body. Any errors in coroutines tied to the call context are surfaced through the HTTP body itself when it is read.
56+
57+
Coroutines launched into the `HttpCall` scope are all meant to service executing a single HTTP request/response pair. It is logically consuming resources associated with the engine and as such is a child of the engine executing the request. It being a child of the engine also gives accurate accounting
58+
of in-flight requests and allows for a clean engine shutdown that doesn't abrubtly close resources in-use.
59+
60+
**Why is the call context the parent to the `roundTrip` invocation?**
61+
62+
The `roundTrip` function is designed to return after the HTTP response headers are available. The body is not processed until deserialization or in the case of streaming responses when the caller consumes the body. As such the `roundTrip` function is responsible for only a subset of the overall HTTP
63+
call (request/response) lifecycle.

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ kotlin.mpp.stability.nowarn=true
55
kotlin.native.ignoreDisabledTargets=true
66

77
# SDK
8-
sdkVersion=0.26.1-SNAPSHOT
8+
sdkVersion=0.27.0-SNAPSHOT
99

1010
# kotlin
11-
kotlinVersion=1.8.22
11+
kotlinVersion=1.8.22

runtime/protocol/aws-event-stream/common/src/aws/smithy/kotlin/runtime/awsprotocol/eventstream/FrameEncoder.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ public suspend fun Flow<SdkBuffer>.asEventStreamHttpBody(scope: CoroutineScope):
4444
private var job: Job? = null
4545

4646
override fun readFrom(): SdkByteReadChannel {
47-
// FIXME - delaying launch here until the channel is consumed from the HTTP engine is a hacky way
48-
// of enforcing ordering to ensure the ExecutionContext is updated with the
49-
// AwsSigningAttributes.RequestSignature by the time the messages are collected and sign() is called
50-
5147
// Although rare, nothing stops downstream consumers from invoking readFrom() more than once.
5248
// Only launch background collection task on first call
5349
if (job == null) {

runtime/protocol/http-client-engines/http-client-engine-crt/common/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngine.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
package aws.smithy.kotlin.runtime.http.engine.crt
77

8+
import aws.smithy.kotlin.runtime.http.HttpCall
89
import aws.smithy.kotlin.runtime.http.config.EngineFactory
910
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
1011
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
1112
import aws.smithy.kotlin.runtime.http.engine.callContext
1213
import aws.smithy.kotlin.runtime.http.request.HttpRequest
13-
import aws.smithy.kotlin.runtime.http.response.HttpCall
1414
import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers
1515
import aws.smithy.kotlin.runtime.operation.ExecutionContext
1616
import aws.smithy.kotlin.runtime.telemetry.logging.logger

runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/AsyncStressTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ package aws.smithy.kotlin.runtime.http.engine.crt
77

88
import aws.smithy.kotlin.runtime.http.HttpMethod
99
import aws.smithy.kotlin.runtime.http.SdkHttpClient
10+
import aws.smithy.kotlin.runtime.http.complete
1011
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
1112
import aws.smithy.kotlin.runtime.http.request.url
12-
import aws.smithy.kotlin.runtime.http.response.complete
1313
import aws.smithy.kotlin.runtime.httptest.TestWithLocalServer
1414
import aws.smithy.kotlin.runtime.net.Host
1515
import aws.smithy.kotlin.runtime.net.Scheme

0 commit comments

Comments
 (0)