Skip to content

Commit 252bdad

Browse files
authored
feat: add support for call timeout and attempt timeout (#1343)
1 parent 503817c commit 252bdad

File tree

14 files changed

+339
-24
lines changed

14 files changed

+339
-24
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "02bc36ae-952e-40ec-a0c2-38a8c8673508",
3+
"type": "feature",
4+
"description": "Enable configuration of timeouts for calls and attempt",
5+
"issues": [
6+
"https://github.com/smithy-lang/smithy-kotlin/issues/1320"
7+
]
8+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ object RuntimeTypes {
7171
object Config : RuntimeTypePackage(KotlinDependency.HTTP_CLIENT, "config") {
7272
val HttpClientConfig = symbol("HttpClientConfig")
7373
val HttpEngineConfig = symbol("HttpEngineConfig")
74+
val TimeoutConfig = symbol("TimeoutConfig")
7475
}
7576

7677
object Engine : RuntimeTypePackage(KotlinDependency.HTTP_CLIENT, "engine") {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class ServiceClientConfigGenerator(
5757
add(RuntimeConfigProperty.RetryStrategy)
5858
add(RuntimeConfigProperty.TelemetryProvider)
5959

60+
add(RuntimeConfigProperty.AttemptTimeout)
61+
add(RuntimeConfigProperty.CallTimeout)
62+
6063
if (shape.hasTrait<ClientContextParamsTrait>()) {
6164
addAll(clientContextConfigProps(shape.expectTrait()))
6265
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import software.amazon.smithy.kotlin.codegen.core.*
99
import software.amazon.smithy.kotlin.codegen.integration.SectionId
1010
import software.amazon.smithy.kotlin.codegen.integration.SectionKey
1111
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
12-
import software.amazon.smithy.kotlin.codegen.model.*
12+
import software.amazon.smithy.kotlin.codegen.model.getTrait
13+
import software.amazon.smithy.kotlin.codegen.model.hasIdempotentTokenMember
14+
import software.amazon.smithy.kotlin.codegen.model.hasStreamingMember
1315
import software.amazon.smithy.kotlin.codegen.model.knowledge.AuthIndex
16+
import software.amazon.smithy.kotlin.codegen.model.operationSignature
1417
import software.amazon.smithy.kotlin.codegen.rendering.auth.AuthSchemeProviderAdapterGenerator
1518
import software.amazon.smithy.kotlin.codegen.rendering.auth.IdentityProviderConfigGenerator
1619
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointResolverAdapterGenerator
@@ -342,6 +345,8 @@ open class HttpProtocolClientGenerator(
342345
"}",
343346
RuntimeTypes.Core.ExecutionContext,
344347
) {
348+
putIfAbsent(RuntimeTypes.HttpClient.Operation.HttpOperationContext, "AttemptTimeout", nullable = true)
349+
putIfAbsent(RuntimeTypes.HttpClient.Operation.HttpOperationContext, "CallTimeout", nullable = true)
345350
putIfAbsent(RuntimeTypes.SmithyClient.SdkClientOption, "ClientName")
346351
putIfAbsent(RuntimeTypes.SmithyClient.SdkClientOption, "LogMode")
347352
if (ctx.service.hasIdempotentTokenMember(ctx.model)) {

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/RuntimeConfigProperty.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import software.amazon.smithy.codegen.core.Symbol
1010
import software.amazon.smithy.codegen.core.SymbolReference
1111
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
1212
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
13+
import software.amazon.smithy.kotlin.codegen.model.asNullable
1314
import software.amazon.smithy.kotlin.codegen.model.buildSymbol
1415

1516
/**
@@ -185,6 +186,31 @@ object RuntimeConfigProperty {
185186
The ordered preference of [AuthScheme] that this client will use.
186187
""".trimIndent()
187188
}
189+
190+
val AttemptTimeout = ConfigProperty {
191+
name = "attemptTimeout"
192+
symbol = KotlinTypes.Time.Duration.asNullable()
193+
baseClass = RuntimeTypes.HttpClient.Config.TimeoutConfig
194+
useNestedBuilderBaseClass()
195+
196+
documentation = """
197+
The maximum amount of time to wait for any single attempt of a request within the retry loop. By default,
198+
the value is `null` indicating no timeout is enforced. Attempt timeouts may be retried if allowed by the
199+
current retry policy and retry capacity.
200+
""".trimIndent()
201+
}
202+
203+
val CallTimeout = ConfigProperty {
204+
name = "callTimeout"
205+
symbol = KotlinTypes.Time.Duration.asNullable()
206+
baseClass = RuntimeTypes.HttpClient.Config.TimeoutConfig
207+
useNestedBuilderBaseClass()
208+
209+
documentation = """
210+
The maximum amount of time to wait for completion of a call, including any retries after the first attempt.
211+
By default, the value is `null` indicating no timeout is enforced. Call timeouts are not retried.
212+
""".trimIndent()
213+
}
188214
}
189215

190216
internal val Symbol.nestedBuilder: Symbol

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ package software.amazon.smithy.kotlin.codegen.rendering
88
import io.kotest.matchers.shouldBe
99
import io.kotest.matchers.string.shouldContain
1010
import software.amazon.smithy.codegen.core.SymbolReference
11-
import software.amazon.smithy.kotlin.codegen.core.*
11+
import software.amazon.smithy.kotlin.codegen.core.CodegenContext
12+
import software.amazon.smithy.kotlin.codegen.core.KotlinDependency
13+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
14+
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
1215
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
1316
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
1417
import software.amazon.smithy.kotlin.codegen.loadModelFromResource
@@ -42,14 +45,16 @@ class ServiceClientConfigGeneratorTest {
4245
contents.assertBalancedBracesAndParens()
4346

4447
val expectedCtor = """
45-
public class Config private constructor(builder: Builder) : HttpAuthConfig, HttpClientConfig, HttpEngineConfig by builder.buildHttpEngineConfig(), IdempotencyTokenConfig, RetryClientConfig, RetryStrategyClientConfig by builder.buildRetryStrategyClientConfig(), SdkClientConfig, TelemetryConfig {
48+
public class Config private constructor(builder: Builder) : HttpAuthConfig, HttpClientConfig, HttpEngineConfig by builder.buildHttpEngineConfig(), IdempotencyTokenConfig, RetryClientConfig, RetryStrategyClientConfig by builder.buildRetryStrategyClientConfig(), SdkClientConfig, TelemetryConfig, TimeoutConfig {
4649
"""
4750
contents.shouldContainWithDiff(expectedCtor)
4851

4952
val expectedProps = """
5053
override val clientName: String = builder.clientName
54+
override val attemptTimeout: Duration? = builder.attemptTimeout
5155
override val authSchemePreference: kotlin.collections.List<aws.smithy.kotlin.runtime.auth.AuthSchemeId>? = builder.authSchemePreference
5256
override val authSchemes: kotlin.collections.List<aws.smithy.kotlin.runtime.http.auth.AuthScheme> = builder.authSchemes
57+
override val callTimeout: Duration? = builder.callTimeout
5358
public val endpointProvider: TestEndpointProvider = requireNotNull(builder.endpointProvider) { "endpointProvider is a required configuration property" }
5459
override val idempotencyTokenProvider: IdempotencyTokenProvider = builder.idempotencyTokenProvider ?: IdempotencyTokenProvider.Default
5560
override val interceptors: kotlin.collections.List<aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor> = builder.interceptors
@@ -60,12 +65,19 @@ public class Config private constructor(builder: Builder) : HttpAuthConfig, Http
6065
contents.shouldContainWithDiff(expectedProps)
6166

6267
val expectedBuilder = """
63-
public class Builder : HttpAuthConfig.Builder, HttpClientConfig.Builder, HttpEngineConfig.Builder by HttpEngineConfigImpl.BuilderImpl(), IdempotencyTokenConfig.Builder, RetryClientConfig.Builder, RetryStrategyClientConfig.Builder by RetryStrategyClientConfigImpl.BuilderImpl(), SdkClientConfig.Builder<Config>, TelemetryConfig.Builder {
68+
public class Builder : HttpAuthConfig.Builder, HttpClientConfig.Builder, HttpEngineConfig.Builder by HttpEngineConfigImpl.BuilderImpl(), IdempotencyTokenConfig.Builder, RetryClientConfig.Builder, RetryStrategyClientConfig.Builder by RetryStrategyClientConfigImpl.BuilderImpl(), SdkClientConfig.Builder<Config>, TelemetryConfig.Builder, TimeoutConfig.Builder {
6469
/**
6570
* A reader-friendly name for the client.
6671
*/
6772
override var clientName: String = "Test"
6873
74+
/**
75+
* The maximum amount of time to wait for any single attempt of a request within the retry loop. By default,
76+
* the value is `null` indicating no timeout is enforced. Attempt timeouts may be retried if allowed by the
77+
* current retry policy and retry capacity.
78+
*/
79+
override var attemptTimeout: Duration? = null
80+
6981
/**
7082
* The ordered preference of [AuthScheme] that this client will use.
7183
*/
@@ -79,6 +91,12 @@ public class Config private constructor(builder: Builder) : HttpAuthConfig, Http
7991
*/
8092
override var authSchemes: kotlin.collections.List<aws.smithy.kotlin.runtime.http.auth.AuthScheme> = emptyList()
8193
94+
/**
95+
* The maximum amount of time to wait for completion of a call, including any retries after the first attempt.
96+
* By default, the value is `null` indicating no timeout is enforced. Call timeouts are not retried.
97+
*/
98+
override var callTimeout: Duration? = null
99+
82100
/**
83101
* The endpoint provider used to determine where to make service requests. **This is an advanced config
84102
* option.**
@@ -245,8 +263,10 @@ public class Config private constructor(builder: Builder) {
245263
// Expect logMode config value to override default to LogMode.Request
246264
val expectedConfigValues = """
247265
override val clientName: String = builder.clientName
266+
override val attemptTimeout: Duration? = builder.attemptTimeout
248267
override val authSchemePreference: kotlin.collections.List<aws.smithy.kotlin.runtime.auth.AuthSchemeId>? = builder.authSchemePreference
249268
override val authSchemes: kotlin.collections.List<aws.smithy.kotlin.runtime.http.auth.AuthScheme> = builder.authSchemes
269+
override val callTimeout: Duration? = builder.callTimeout
250270
public val customProp: Int? = builder.customProp
251271
public val endpointProvider: TestEndpointProvider = requireNotNull(builder.endpointProvider) { "endpointProvider is a required configuration property" }
252272
override val idempotencyTokenProvider: IdempotencyTokenProvider = builder.idempotencyTokenProvider ?: IdempotencyTokenProvider.Default

runtime/protocol/http-client/api/http-client.api

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ public final class aws/smithy/kotlin/runtime/http/config/HttpEngineConfig$Builde
4242
public abstract interface annotation class aws/smithy/kotlin/runtime/http/config/HttpEngineConfigDsl : java/lang/annotation/Annotation {
4343
}
4444

45+
public abstract interface class aws/smithy/kotlin/runtime/http/config/TimeoutConfig {
46+
public abstract fun getAttemptTimeout-FghU774 ()Lkotlin/time/Duration;
47+
public abstract fun getCallTimeout-FghU774 ()Lkotlin/time/Duration;
48+
}
49+
50+
public abstract interface class aws/smithy/kotlin/runtime/http/config/TimeoutConfig$Builder {
51+
public abstract fun getAttemptTimeout-FghU774 ()Lkotlin/time/Duration;
52+
public abstract fun getCallTimeout-FghU774 ()Lkotlin/time/Duration;
53+
public abstract fun setAttemptTimeout-BwNAW2A (Lkotlin/time/Duration;)V
54+
public abstract fun setCallTimeout-BwNAW2A (Lkotlin/time/Duration;)V
55+
}
56+
4557
public final class aws/smithy/kotlin/runtime/http/engine/AlpnId : java/lang/Enum {
4658
public static final field H2_PRIOR_KNOWLEDGE Laws/smithy/kotlin/runtime/http/engine/AlpnId;
4759
public static final field HTTP1_1 Laws/smithy/kotlin/runtime/http/engine/AlpnId;
@@ -489,10 +501,22 @@ public final class aws/smithy/kotlin/runtime/http/middleware/MutateHeaders : aws
489501
public final fun setIfMissing (Ljava/lang/String;Ljava/lang/String;)V
490502
}
491503

504+
public final class aws/smithy/kotlin/runtime/http/operation/AttemptTimeoutException : aws/smithy/kotlin/runtime/http/operation/ClientTimeoutException {
505+
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
506+
}
507+
492508
public abstract interface class aws/smithy/kotlin/runtime/http/operation/AuthSchemeResolver {
493509
public abstract fun resolve (Laws/smithy/kotlin/runtime/http/operation/OperationRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
494510
}
495511

512+
public final class aws/smithy/kotlin/runtime/http/operation/CallTimeoutException : aws/smithy/kotlin/runtime/http/operation/ClientTimeoutException {
513+
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
514+
}
515+
516+
public abstract class aws/smithy/kotlin/runtime/http/operation/ClientTimeoutException : aws/smithy/kotlin/runtime/ClientException {
517+
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;Z)V
518+
}
519+
496520
public abstract interface class aws/smithy/kotlin/runtime/http/operation/EndpointResolver {
497521
public abstract fun resolve (Laws/smithy/kotlin/runtime/http/operation/ResolveEndpointRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
498522
}
@@ -516,6 +540,8 @@ public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpDes
516540

517541
public final class aws/smithy/kotlin/runtime/http/operation/HttpOperationContext {
518542
public static final field INSTANCE Laws/smithy/kotlin/runtime/http/operation/HttpOperationContext;
543+
public final fun getAttemptTimeout ()Laws/smithy/kotlin/runtime/collections/AttributeKey;
544+
public final fun getCallTimeout ()Laws/smithy/kotlin/runtime/collections/AttributeKey;
519545
public final fun getClockSkew ()Laws/smithy/kotlin/runtime/collections/AttributeKey;
520546
public final fun getClockSkewApproximateSigningTime ()Laws/smithy/kotlin/runtime/collections/AttributeKey;
521547
public final fun getDefaultChecksumAlgorithm ()Laws/smithy/kotlin/runtime/collections/AttributeKey;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package aws.smithy.kotlin.runtime.http.config
2+
3+
import kotlin.time.Duration
4+
5+
/**
6+
* Defines optional timeout configuration for clients.
7+
*/
8+
public interface TimeoutConfig {
9+
/**
10+
* The maximum amount of time to wait for any single attempt of a request within the retry loop. By default, the
11+
* value is `null` indicating no timeout is enforced. Attempt timeouts may be retried if allowed by the current
12+
* retry policy and retry capacity.
13+
*/
14+
public val attemptTimeout: Duration?
15+
16+
/**
17+
* The maximum amount of time to wait for completion of a call, including any retries after the first attempt. By
18+
* default, the value is `null` indicating no timeout is enforced. Call timeouts are not retried.
19+
*/
20+
public val callTimeout: Duration?
21+
22+
/**
23+
* A mutable instance used to set timeout configuration for clients.
24+
*/
25+
public interface Builder {
26+
/**
27+
* The maximum amount of time to wait for any single attempt of a request within the retry loop. By default, the
28+
* value is `null` indicating no timeout is enforced. Attempt timeouts may be retried if allowed by the current
29+
* retry policy and retry capacity.
30+
*/
31+
public var attemptTimeout: Duration?
32+
33+
/**
34+
* The maximum amount of time to wait for completion of a call, including any retries after the first attempt.
35+
* By default, the value is `null` indicating no timeout is enforced. Call timeouts are not retried.
36+
*/
37+
public var callTimeout: Duration?
38+
}
39+
}

runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/engine/HttpClientEngine.kt

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import aws.smithy.kotlin.runtime.operation.ExecutionContext
1313
import kotlinx.atomicfu.atomic
1414
import kotlinx.coroutines.*
1515
import kotlin.coroutines.CoroutineContext
16+
import kotlin.coroutines.EmptyCoroutineContext
1617

1718
/**
1819
* Functionality a real HTTP client must provide.
@@ -39,12 +40,24 @@ public interface CloseableHttpClientEngine :
3940
Closeable
4041

4142
/**
42-
* Base class that SDK [HttpClientEngine]s SHOULD inherit from rather than implementing directly.
43+
* Base class that SDK [HttpClientEngine]s SHOULD inherit from rather than implementing directly. This class's
44+
* [CoroutineContext] will include [SupervisorJob] because the failure of individual requests should not affect other
45+
* requests or the overall engine.
4346
*/
4447
@InternalApi
45-
public abstract class HttpClientEngineBase(engineName: String) : CloseableHttpClientEngine {
46-
// why SupervisorJob? because failure of individual requests should not affect other requests or the overall engine
47-
override val coroutineContext: CoroutineContext = SupervisorJob() + CoroutineName("http-client-engine-$engineName-context")
48+
public abstract class HttpClientEngineBase private constructor(
49+
override val coroutineContext: CoroutineContext,
50+
) : CloseableHttpClientEngine {
51+
public constructor(engineName: String) : this(engineName, EmptyCoroutineContext)
52+
53+
/**
54+
* Initializes a new [HttpClientEngineBase]. This internal overload allows setting a base [CoroutineContext] which
55+
* is useful in tests using `runTest` (otherwise the base context will be empty and so will default to using
56+
* [Dispatchers.Default]).
57+
*/
58+
internal constructor(engineName: String, baseContext: CoroutineContext) :
59+
this(baseContext + SupervisorJob() + CoroutineName("http-client-engine-$engineName-context"))
60+
4861
private val closed = atomic(false)
4962

5063
final override fun close() {

runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/middleware/RetryMiddleware.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric
99
import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric
1010
import aws.smithy.kotlin.runtime.http.interceptors.InterceptorExecutor
1111
import aws.smithy.kotlin.runtime.http.operation.*
12-
import aws.smithy.kotlin.runtime.http.operation.deepCopy
1312
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
1413
import aws.smithy.kotlin.runtime.http.request.immutableView
1514
import aws.smithy.kotlin.runtime.http.request.toBuilder
@@ -84,7 +83,10 @@ internal class RetryMiddleware<I, O>(
8483
): Result<O> {
8584
val result = interceptors.readBeforeAttempt(request.subject.immutableView())
8685
.mapCatching {
87-
next.call(request)
86+
val attemptTimeout = request.context.getOrNull(HttpOperationContext.AttemptTimeout)
87+
scopedTimeout(TimeoutScope.Attempt(attempt), attemptTimeout) {
88+
next.call(request)
89+
}
8890
}
8991

9092
// get the http call for this attempt (if we made it that far)

0 commit comments

Comments
 (0)