Skip to content

Commit 8dd6bd0

Browse files
authored
refactor(rt): upstream HttpClientEngine interface changes (#608)
* refactor(rt): upstream HttpClientEngine interface changes * replace default retry middleware with AWS specific component that sets retry headers * add max attempts to retry header
1 parent 4363cac commit 8dd6bd0

File tree

9 files changed

+137
-14
lines changed

9 files changed

+137
-14
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package aws.sdk.kotlin.runtime.http.middleware
2+
3+
import aws.smithy.kotlin.runtime.http.middleware.Retry
4+
import aws.smithy.kotlin.runtime.http.operation.*
5+
import aws.smithy.kotlin.runtime.http.request.header
6+
import aws.smithy.kotlin.runtime.io.Handler
7+
import aws.smithy.kotlin.runtime.retries.RetryStrategy
8+
import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy
9+
import aws.smithy.kotlin.runtime.util.InternalApi
10+
import aws.smithy.kotlin.runtime.util.get
11+
12+
/**
13+
* The per/operation unique client side ID header name. This will match
14+
* the [HttpOperationContext.SdkRequestId]
15+
*/
16+
internal const val AMZ_SDK_INVOCATION_ID_HEADER = "amz-sdk-invocation-id"
17+
18+
/**
19+
* Details about the current request such as the attempt number, maximum possible attempts, ttl, etc
20+
*/
21+
internal const val AMZ_SDK_REQUEST_HEADER = "amz-sdk-request"
22+
23+
/**
24+
* Retry requests with the given strategy and policy. This middleware customizes the default [Retry] implementation
25+
* to add AWS specific retry headers
26+
*
27+
* @param strategy the [RetryStrategy] to retry failed requests with
28+
* @param policy the [RetryPolicy] used to determine when to retry
29+
*/
30+
@InternalApi
31+
public class AwsRetryMiddleware<O>(
32+
strategy: RetryStrategy,
33+
policy: RetryPolicy<Any?>
34+
) : Retry<O>(strategy, policy) {
35+
36+
override suspend fun <H : Handler<SdkHttpRequest, O>> handle(request: SdkHttpRequest, next: H): O {
37+
request.subject.header(AMZ_SDK_INVOCATION_ID_HEADER, request.context[HttpOperationContext.SdkRequestId])
38+
return super.handle(request, next)
39+
}
40+
41+
override fun onAttempt(request: SdkHttpRequest, attempt: Int) {
42+
// setting ttl would never be accurate, just set what we know which is attempt and maybe max attempt
43+
val maxAttempts = strategy.options.maxAttempts?.let { "; max=$it" } ?: ""
44+
request.subject.header(AMZ_SDK_REQUEST_HEADER, "attempt=${attempt}$maxAttempts")
45+
}
46+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package aws.sdk.kotlin.runtime.http.middleware
2+
3+
import aws.sdk.kotlin.runtime.http.retries.AwsDefaultRetryPolicy
4+
import aws.smithy.kotlin.runtime.client.ExecutionContext
5+
import aws.smithy.kotlin.runtime.http.Headers
6+
import aws.smithy.kotlin.runtime.http.HttpBody
7+
import aws.smithy.kotlin.runtime.http.HttpStatusCode
8+
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
9+
import aws.smithy.kotlin.runtime.http.operation.*
10+
import aws.smithy.kotlin.runtime.http.request.HttpRequest
11+
import aws.smithy.kotlin.runtime.http.response.HttpCall
12+
import aws.smithy.kotlin.runtime.http.response.HttpResponse
13+
import aws.smithy.kotlin.runtime.http.sdkHttpClient
14+
import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy
15+
import aws.smithy.kotlin.runtime.retries.StandardRetryStrategyOptions
16+
import aws.smithy.kotlin.runtime.retries.delay.DelayProvider
17+
import aws.smithy.kotlin.runtime.retries.delay.StandardRetryTokenBucket
18+
import aws.smithy.kotlin.runtime.retries.delay.StandardRetryTokenBucketOptions
19+
import aws.smithy.kotlin.runtime.time.Instant
20+
import aws.smithy.kotlin.runtime.util.get
21+
import kotlinx.coroutines.ExperimentalCoroutinesApi
22+
import kotlinx.coroutines.test.runTest
23+
import kotlin.test.Test
24+
import kotlin.test.assertEquals
25+
import kotlin.test.assertTrue
26+
27+
@OptIn(ExperimentalCoroutinesApi::class)
28+
class AwsRetryMiddlewareTest {
29+
30+
private val mockEngine = object : HttpClientEngineBase("test") {
31+
override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall {
32+
val resp = HttpResponse(HttpStatusCode.OK, Headers.Empty, HttpBody.Empty)
33+
return HttpCall(request, resp, Instant.now(), Instant.now())
34+
}
35+
}
36+
private val client = sdkHttpClient(mockEngine)
37+
38+
@Test
39+
fun testItSetsRetryHeaders() = runTest {
40+
// see retry-header SEP
41+
val op = SdkHttpOperation.build<Unit, Unit> {
42+
serializer = UnitSerializer
43+
deserializer = UnitDeserializer
44+
context {
45+
// required operation context
46+
operationName = "TestOperation"
47+
service = "TestService"
48+
}
49+
}
50+
51+
val delayProvider = DelayProvider { }
52+
val strategy = StandardRetryStrategy(
53+
StandardRetryStrategyOptions.Default,
54+
StandardRetryTokenBucket(StandardRetryTokenBucketOptions.Default),
55+
delayProvider
56+
)
57+
val maxAttempts = strategy.options.maxAttempts
58+
59+
op.install(AwsRetryMiddleware(strategy, AwsDefaultRetryPolicy))
60+
61+
op.roundTrip(client, Unit)
62+
val calls = op.context.attributes[HttpOperationContext.HttpCallList]
63+
val sdkRequestId = op.context[HttpOperationContext.SdkRequestId]
64+
65+
assertTrue(calls.all { it.request.headers[AMZ_SDK_INVOCATION_ID_HEADER] == sdkRequestId })
66+
calls.forEachIndexed { idx, call ->
67+
assertEquals("attempt=${idx + 1}; max=$maxAttempts", call.request.headers[AMZ_SDK_REQUEST_HEADER])
68+
}
69+
}
70+
}

aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/RecursionDetectionTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class RecursionDetectionTest {
3838
}
3939

4040
private val mockEngine = object : HttpClientEngineBase("test") {
41-
override suspend fun roundTrip(request: HttpRequest): HttpCall {
41+
override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall {
4242
val resp = HttpResponse(HttpStatusCode.fromValue(200), Headers.Empty, HttpBody.Empty)
4343
val now = Instant.now()
4444
return HttpCall(request, resp, now, now)

aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/ResolveAwsEndpointTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import aws.sdk.kotlin.runtime.endpoint.AwsEndpoint
1010
import aws.sdk.kotlin.runtime.endpoint.AwsEndpointResolver
1111
import aws.sdk.kotlin.runtime.endpoint.CredentialScope
1212
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningAttributes
13+
import aws.smithy.kotlin.runtime.client.ExecutionContext
1314
import aws.smithy.kotlin.runtime.http.*
1415
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
1516
import aws.smithy.kotlin.runtime.http.operation.*
@@ -27,7 +28,7 @@ import kotlin.test.assertEquals
2728
class ResolveAwsEndpointTest {
2829

2930
private val mockEngine = object : HttpClientEngineBase("test") {
30-
override suspend fun roundTrip(request: HttpRequest): HttpCall {
31+
override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall {
3132
val resp = HttpResponse(HttpStatusCode.OK, Headers.Empty, HttpBody.Empty)
3233
return HttpCall(request, resp, Instant.now(), Instant.now())
3334
}

aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import aws.sdk.kotlin.runtime.http.ApiMetadata
99
import aws.sdk.kotlin.runtime.http.loadAwsUserAgentMetadataFromEnvironment
1010
import aws.sdk.kotlin.runtime.http.operation.customUserAgentMetadata
1111
import aws.sdk.kotlin.runtime.testing.TestPlatformProvider
12+
import aws.smithy.kotlin.runtime.client.ExecutionContext
1213
import aws.smithy.kotlin.runtime.http.Headers
1314
import aws.smithy.kotlin.runtime.http.HttpBody
1415
import aws.smithy.kotlin.runtime.http.HttpStatusCode
@@ -31,7 +32,7 @@ import kotlin.test.assertTrue
3132
@OptIn(ExperimentalCoroutinesApi::class)
3233
class UserAgentTest {
3334
private val mockEngine = object : HttpClientEngineBase("test") {
34-
override suspend fun roundTrip(request: HttpRequest): HttpCall {
35+
override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall {
3536
val resp = HttpResponse(HttpStatusCode.fromValue(200), Headers.Empty, HttpBody.Empty)
3637
val now = Instant.now()
3738
return HttpCall(request, resp, now, now)

aws-runtime/protocols/aws-json-protocols/common/test/aws/sdk/kotlin/runtime/protocol/json/AwsJsonProtocolTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class AwsJsonProtocolTest {
2727
@Test
2828
fun testSetJsonProtocolHeaders() = runTest {
2929
val mockEngine = object : HttpClientEngineBase("test") {
30-
override suspend fun roundTrip(request: HttpRequest): HttpCall {
30+
override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall {
3131
val resp = HttpResponse(HttpStatusCode.OK, Headers.Empty, HttpBody.Empty)
3232
val now = Instant.now()
3333
return HttpCall(request, resp, now, now)
@@ -58,7 +58,7 @@ class AwsJsonProtocolTest {
5858
@Test
5959
fun testEmptyBody() = runTest {
6060
val mockEngine = object : HttpClientEngineBase("test") {
61-
override suspend fun roundTrip(request: HttpRequest): HttpCall {
61+
override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall {
6262
val resp = HttpResponse(HttpStatusCode.OK, Headers.Empty, HttpBody.Empty)
6363
val now = Instant.now()
6464
return HttpCall(request, resp, now, now)
@@ -86,7 +86,7 @@ class AwsJsonProtocolTest {
8686
@Test
8787
fun testDoesNotOverride() = runTest {
8888
val mockEngine = object : HttpClientEngineBase("test") {
89-
override suspend fun roundTrip(request: HttpRequest): HttpCall {
89+
override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall {
9090
val resp = HttpResponse(HttpStatusCode.OK, Headers.Empty, HttpBody.Empty)
9191
val now = Instant.now()
9292
return HttpCall(request, resp, now, now)

codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsDefaultRetryIntegration.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
package aws.sdk.kotlin.codegen
77

88
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
9-
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
109
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
1110
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
1211
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware
@@ -15,8 +14,8 @@ import software.amazon.smithy.kotlin.codegen.retries.StandardRetryMiddleware
1514
import software.amazon.smithy.model.shapes.OperationShape
1615

1716
/**
18-
* Adds AWS-specific retry wrappers around operation invocations. This replaces
19-
* [StandardRetryPolicy][aws.smithy.kotlin.runtime.retries.impl] with
17+
* Replace the [StandardRetryMiddleware] with AWS specific retry middleware (AwsRetryMiddleware)
18+
* as well as replace the [StandardRetryPolicy][aws.smithy.kotlin.runtime.retries.impl] with
2019
* [AwsDefaultRetryPolicy][aws.sdk.kotlin.runtime.http.retries].
2120
*/
2221
class AwsDefaultRetryIntegration : KotlinIntegration {
@@ -26,12 +25,14 @@ class AwsDefaultRetryIntegration : KotlinIntegration {
2625
): List<ProtocolMiddleware> = resolved.replace(middleware) { it is StandardRetryMiddleware }
2726

2827
private val middleware = object : ProtocolMiddleware {
29-
override val name: String = RuntimeTypes.Http.Middlware.Retry.name
28+
override val name: String = AwsRuntimeTypes.Http.Middleware.AwsRetryMiddleware.name
3029

3130
override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) {
32-
writer.addImport(RuntimeTypes.Http.Middlware.Retry)
33-
writer.addImport(AwsRuntimeTypes.Http.Retries.AwsDefaultRetryPolicy)
34-
writer.write("op.install(#T(config.retryStrategy, AwsDefaultRetryPolicy))", RuntimeTypes.Http.Middlware.Retry)
31+
writer.write(
32+
"op.install(#T(config.retryStrategy, #T))",
33+
AwsRuntimeTypes.Http.Middleware.AwsRetryMiddleware,
34+
AwsRuntimeTypes.Http.Retries.AwsDefaultRetryPolicy
35+
)
3536
}
3637
}
3738
}

codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ object AwsRuntimeTypes {
5555
object Retries {
5656
val AwsDefaultRetryPolicy = runtimeSymbol("AwsDefaultRetryPolicy", AwsKotlinDependency.AWS_HTTP, "retries")
5757
}
58+
object Middleware {
59+
val AwsRetryMiddleware = runtimeSymbol("AwsRetryMiddleware", AwsKotlinDependency.AWS_HTTP, "middleware")
60+
}
5861
}
5962

6063
object JsonProtocols {

services/sts/common/test/aws/sdk/kotlin/services/sts/StsAuthTests.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package aws.sdk.kotlin.services.sts
77

88
import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
99
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
10+
import aws.smithy.kotlin.runtime.client.ExecutionContext
1011
import aws.smithy.kotlin.runtime.http.Headers
1112
import aws.smithy.kotlin.runtime.http.HttpBody
1213
import aws.smithy.kotlin.runtime.http.HttpStatusCode
@@ -32,7 +33,7 @@ class StsAuthTests {
3233
private val mockEngine = object : HttpClientEngineBase("mock-engine") {
3334
var capturedRequest: HttpRequest? = null
3435

35-
override suspend fun roundTrip(request: HttpRequest): HttpCall {
36+
override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall {
3637
capturedRequest = request
3738
val callContext = callContext()
3839
val now = Instant.now()

0 commit comments

Comments
 (0)