Skip to content

Commit 45a1465

Browse files
luigi617luigi liu
andauthored
Retry 2026 (#1901)
* Retry standard strategy (#1883) * new standard behaviour * adapt to new strategy changing function * ktlint * new standard behaviour * adapt to new strategy changing function * ktlint * service-based error and default value for backoff * add testing and fix constant init * testing * changelog * apiDump * preserve old behaviour * remove duplicate setting * error * remove serviceName dependency and use specific service codegen for variable * fix * fix * apiDump * fix * apiDump * change log * fix and add test * fix --------- Co-authored-by: luigi liu <luigiliu@amazon.com> * Retry long polling (#1892) * new standard behaviour * adapt to new strategy changing function * ktlint * new standard behaviour * adapt to new strategy changing function * ktlint * service-based error and default value for backoff * add testing and fix constant init * testing * changelog * apiDump * long polling * long polling * fix awsretrypolicy * long polling * ktling * add enableLongPollingBackoff * new test * fix * retrigger CI * changelog * fix --------- Co-authored-by: luigi liu <luigiliu@amazon.com> * version bump --------- Co-authored-by: luigi liu <luigiliu@amazon.com>
1 parent fb52226 commit 45a1465

17 files changed

Lines changed: 719 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "6068d89a-2389-4750-9de7-52a56e6f43ce",
3+
"type": "feature",
4+
"description": "Add retry logic for long polling operations. See the [announcement](https://github.com/aws/aws-sdk-kotlin/discussions/1885) for more details."
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "63e5c52a-caf3-42a7-97d2-eabfaf8b1921",
3+
"type": "feature",
4+
"description": "Add new standard retry behavior behind `AWS_NEW_RETRIES_2026` feature flag with updated backoff defaults, DynamoDB-specific overrides, and service-scoped error classification. See the [announcement](https://github.com/aws/aws-sdk-kotlin/discussions/1885) for more details."
5+
}

aws-runtime/aws-config/api/aws-config.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ public abstract class aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory
258258
protected fun finalizeEnvironmentalConfig (Laws/smithy/kotlin/runtime/client/SdkClient$Builder;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
259259
public final fun fromEnvironment (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
260260
public static synthetic fun fromEnvironment$default (Laws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
261+
protected fun getDefaultInitialDelay-FghU774 ()Lkotlin/time/Duration;
262+
protected fun getDefaultMaxAttempts ()Ljava/lang/Integer;
261263
}
262264

263265
public abstract interface class aws/sdk/kotlin/runtime/config/AwsSdkClientConfig : aws/smithy/kotlin/runtime/client/SdkClientConfig {
@@ -307,6 +309,7 @@ public final class aws/sdk/kotlin/runtime/config/AwsSdkSetting {
307309
public final fun getAwsExecutionEnv ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
308310
public final fun getAwsIgnoreEndpointUrls ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
309311
public final fun getAwsMaxAttempts ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
312+
public final fun getAwsNewRetries ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
310313
public final fun getAwsProfile ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
311314
public final fun getAwsRegion ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
312315
public final fun getAwsRequestChecksumCalculation ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;

aws-runtime/aws-config/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import aws.sdk.kotlin.gradle.codegen.smithyKotlinProjectionSrcDir
88

99
plugins {
1010
alias(libs.plugins.aws.kotlin.repo.tools.smithybuild)
11+
alias(libs.plugins.kotlinx.serialization)
1112
}
1213

1314
description = "Support for AWS configuration"
@@ -59,6 +60,7 @@ kotlin {
5960
dependencies {
6061
implementation(libs.mockk)
6162
implementation(libs.kotest.runner.junit5)
63+
implementation(libs.kaml)
6264
}
6365
}
6466

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import aws.sdk.kotlin.runtime.config.endpoints.resolveUseFips
1515
import aws.sdk.kotlin.runtime.config.profile.AwsProfile
1616
import aws.sdk.kotlin.runtime.config.profile.AwsSharedConfig
1717
import aws.sdk.kotlin.runtime.config.profile.loadAwsSharedConfig
18-
import aws.sdk.kotlin.runtime.config.retries.resolveRetryStrategy
18+
import aws.sdk.kotlin.runtime.config.retries.buildRetryStrategy
19+
import aws.sdk.kotlin.runtime.config.retries.resolveRetryConfig
1920
import aws.sdk.kotlin.runtime.config.useragent.resolveUserAgentAppId
2021
import aws.sdk.kotlin.runtime.region.resolveRegion
2122
import aws.sdk.kotlin.runtime.region.resolveSigV4aSigningRegionSet
@@ -36,6 +37,7 @@ import aws.smithy.kotlin.runtime.telemetry.trace.withSpan
3637
import aws.smithy.kotlin.runtime.util.LazyAsyncValue
3738
import aws.smithy.kotlin.runtime.util.PlatformProvider
3839
import aws.smithy.kotlin.runtime.util.asyncLazy
40+
import kotlin.time.Duration
3941

4042
/**
4143
* Abstract base class all AWS client companion objects inherit from
@@ -55,6 +57,19 @@ public abstract class AbstractAwsSdkClientFactory<
5557
TConfig : AwsSdkClientConfig,
5658
TConfigBuilder : SdkClientConfig.Builder<TConfig>,
5759
TConfigBuilder : AwsSdkClientConfig.Builder {
60+
61+
/**
62+
* Service-specific default max attempts for the retry strategy.
63+
* Override in generated code for services that require non-standard defaults (e.g., DynamoDB uses 4).
64+
*/
65+
protected open val defaultMaxAttempts: Int? = null
66+
67+
/**
68+
* Service-specific default initial delay for the retry strategy.
69+
* Override in generated code for services that require non-standard defaults (e.g., DynamoDB uses 25ms).
70+
*/
71+
protected open val defaultInitialDelay: Duration? = null
72+
5873
/**
5974
* Construct a [TClient] by resolving the configuration from the current environment.
6075
*/
@@ -75,7 +90,8 @@ public abstract class AbstractAwsSdkClientFactory<
7590
// As a DslBuilderProperty, the value of retryStrategy cannot be checked for nullability because it may have
7691
// been set using a DSL. Thus, set the resolved strategy _first_ to ensure it's used as the fallback.
7792
if (config is RetryStrategyClientConfig.Builder) {
78-
config.retryStrategy = resolveRetryStrategy(profile = profile)
93+
val retryConfig = resolveRetryConfig(platform, profile)
94+
config.retryStrategy = buildRetryStrategy(retryConfig, defaultMaxAttempts, defaultInitialDelay)
7995
}
8096

8197
block?.let(config::apply)

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@ public object AwsSdkSetting {
237237
* Configures an ordered preference of auth schemes
238238
*/
239239
public val AwsAuthSchemePreference: EnvironmentSetting<String> = strEnvSetting("aws.authSchemePreference", "AWS_AUTH_SCHEME_PREFERENCE")
240+
241+
/**
242+
* Enables the new retry behavior. When set, takes precedence over
243+
* the `SMITHY_NEW_RETRIES_2026` environment variable defined in smithy-kotlin.
244+
*/
245+
public val AwsNewRetries: EnvironmentSetting<Boolean> = boolEnvSetting("aws.newRetries2026", "AWS_NEW_RETRIES_2026")
240246
}
241247

242248
/**

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/retries/ResolveRetryStrategy.kt

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import aws.sdk.kotlin.runtime.config.profile.AwsProfile
1212
import aws.sdk.kotlin.runtime.config.profile.loadAwsSharedConfig
1313
import aws.sdk.kotlin.runtime.config.profile.maxAttempts
1414
import aws.sdk.kotlin.runtime.config.profile.retryMode
15+
import aws.smithy.kotlin.runtime.CoreSettings
1516
import aws.smithy.kotlin.runtime.client.config.RetryMode
1617
import aws.smithy.kotlin.runtime.config.resolve
1718
import aws.smithy.kotlin.runtime.retries.AdaptiveRetryStrategy
@@ -20,34 +21,111 @@ import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy
2021
import aws.smithy.kotlin.runtime.util.LazyAsyncValue
2122
import aws.smithy.kotlin.runtime.util.PlatformProvider
2223
import aws.smithy.kotlin.runtime.util.asyncLazy
24+
import kotlin.time.Duration
25+
import kotlin.time.Duration.Companion.milliseconds
26+
27+
// Standard retry defaults (New Retry Behavior)
28+
internal val STANDARD_INITIAL_DELAY = 50.milliseconds
29+
internal const val STANDARD_SCALE_FACTOR = 2.0
30+
internal const val STANDARD_RETRY_COST = 14
31+
internal const val STANDARD_THROTTLING_RETRY_COST = 5
2332

2433
/**
25-
* Attempt to resolve the retry strategy used to make requests by fetching the max attempts and retry mode. Currently,
26-
* we only support the legacy and standard retry modes.
34+
* Resolved retry configuration sourced from environment variables and profile settings.
2735
*/
28-
@InternalSdkApi
29-
public suspend fun resolveRetryStrategy(
30-
platformProvider: PlatformProvider = PlatformProvider.System,
31-
profile: LazyAsyncValue<AwsProfile> = asyncLazy { loadAwsSharedConfig(platformProvider).activeProfile },
32-
): RetryStrategy {
36+
internal data class ResolvedRetryConfig(
37+
val maxAttempts: Int?,
38+
val retryMode: RetryMode,
39+
val useNewRetries: Boolean,
40+
)
41+
42+
/**
43+
* Resolve the retry configuration (max attempts, retry mode, and new retries flag) from environment variables
44+
* and profile settings.
45+
*/
46+
internal suspend fun resolveRetryConfig(
47+
platformProvider: PlatformProvider,
48+
profile: LazyAsyncValue<AwsProfile>,
49+
): ResolvedRetryConfig {
50+
val useNewRetries = AwsSdkSetting.AwsNewRetries.resolve(platformProvider)
51+
?: CoreSettings.resolveNewRetriesEnabled(platformProvider)
52+
3353
val maxAttempts = AwsSdkSetting.AwsMaxAttempts.resolve(platformProvider)
3454
?: profile.get().maxAttempts
3555

56+
maxAttempts?.let {
57+
if (it < 1) throw ConfigurationException("max attempts was $it, but should be at least 1")
58+
}
59+
3660
val retryMode = AwsSdkSetting.AwsRetryMode.resolve(platformProvider)
3761
?: profile.get().retryMode
3862
?: RetryMode.STANDARD
3963

40-
val factory = when (retryMode) {
64+
return ResolvedRetryConfig(maxAttempts, retryMode, useNewRetries)
65+
}
66+
67+
/**
68+
* Attempt to resolve the retry strategy from environment variables and profile settings.
69+
*/
70+
@InternalSdkApi
71+
public suspend fun resolveRetryStrategy(
72+
platformProvider: PlatformProvider = PlatformProvider.System,
73+
profile: LazyAsyncValue<AwsProfile> = asyncLazy { loadAwsSharedConfig(platformProvider).activeProfile },
74+
): RetryStrategy {
75+
val config = resolveRetryConfig(platformProvider, profile)
76+
return buildRetryStrategy(config)
77+
}
78+
79+
internal fun buildRetryStrategy(config: ResolvedRetryConfig): RetryStrategy = buildRetryStrategy(config, defaultMaxAttempts = null, defaultInitialDelay = null)
80+
81+
internal fun buildRetryStrategy(
82+
config: ResolvedRetryConfig,
83+
defaultMaxAttempts: Int?,
84+
defaultInitialDelay: Duration?,
85+
): RetryStrategy {
86+
val factory = when (config.retryMode) {
4187
RetryMode.STANDARD, RetryMode.LEGACY -> StandardRetryStrategy
4288
RetryMode.ADAPTIVE -> AdaptiveRetryStrategy
4389
}
4490

4591
return factory {
46-
maxAttempts?.let {
47-
if (it < 1) {
48-
throw ConfigurationException("max attempts was $it, but should be at least 1")
49-
}
50-
this.maxAttempts = it
51-
}
92+
configureRetryDefaults(
93+
configuredMaxAttempts = config.maxAttempts,
94+
useNewRetries = config.useNewRetries,
95+
defaultMaxAttempts = defaultMaxAttempts,
96+
defaultInitialDelay = defaultInitialDelay,
97+
)
98+
}
99+
}
100+
101+
/**
102+
* Configures the retry strategy builder with the resolved max attempts and, when new retries are enabled,
103+
* all standard defaults as defined in the New Retry Behavior.
104+
*/
105+
internal fun StandardRetryStrategy.Config.Builder.configureRetryDefaults(
106+
configuredMaxAttempts: Int? = null,
107+
useNewRetries: Boolean = false,
108+
defaultMaxAttempts: Int? = null,
109+
defaultInitialDelay: Duration? = null,
110+
) {
111+
if (!useNewRetries) {
112+
configuredMaxAttempts?.let { maxAttempts = it }
113+
return
52114
}
115+
116+
enableLongPollingBackoff = true
117+
118+
delayProvider {
119+
initialDelay = defaultInitialDelay ?: STANDARD_INITIAL_DELAY
120+
scaleFactor = STANDARD_SCALE_FACTOR
121+
}
122+
123+
tokenBucket {
124+
retryCost = STANDARD_RETRY_COST
125+
timeoutRetryCost = STANDARD_THROTTLING_RETRY_COST
126+
}
127+
128+
maxAttempts = configuredMaxAttempts
129+
?: defaultMaxAttempts
130+
?: StandardRetryStrategy.Config.DEFAULT_MAX_ATTEMPTS
53131
}

aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/config/JavaSystemPropertiesFallbackTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import kotlin.test.Ignore
1111
import kotlin.test.Test
1212
import kotlin.test.assertEquals
1313
import kotlin.test.assertNull
14+
import kotlin.test.assertTrue
1415

1516
class JavaSystemPropertiesFallbackTest {
1617
@Test
@@ -92,4 +93,33 @@ class JavaSystemPropertiesFallbackTest {
9293
val result = AwsSdkSetting.AwsSigV4aSigningRegionSet.resolve(platform)
9394
assertEquals("env-regions", result)
9495
}
96+
97+
@Test
98+
fun testNewRetriesSystemPropertyTakesPrecedence() {
99+
val platform = TestPlatformProvider(
100+
props = mapOf("aws.newRetries2026" to "true"),
101+
env = mapOf("AWS_NEW_RETRIES_2026" to "false"),
102+
)
103+
104+
val result = AwsSdkSetting.AwsNewRetries.resolve(platform)
105+
assertTrue(result!!)
106+
}
107+
108+
@Test
109+
fun testNewRetriesEnvironmentFallback() {
110+
val platform = TestPlatformProvider(
111+
env = mapOf("AWS_NEW_RETRIES_2026" to "true"),
112+
)
113+
114+
val result = AwsSdkSetting.AwsNewRetries.resolve(platform)
115+
assertTrue(result!!)
116+
}
117+
118+
@Test
119+
fun testNewRetriesNullWhenNothingSet() {
120+
val platform = TestPlatformProvider()
121+
122+
val result = AwsSdkSetting.AwsNewRetries.resolve(platform)
123+
assertNull(result)
124+
}
95125
}

aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/config/retries/ResolveRetryStrategyTest.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@ package aws.sdk.kotlin.runtime.config.retries
77

88
import aws.sdk.kotlin.runtime.ConfigurationException
99
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
10+
import aws.sdk.kotlin.runtime.config.profile.loadAwsSharedConfig
1011
import aws.smithy.kotlin.runtime.ClientException
1112
import aws.smithy.kotlin.runtime.retries.AdaptiveRetryStrategy
1213
import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy
14+
import aws.smithy.kotlin.runtime.retries.delay.ExponentialBackoffWithJitter
15+
import aws.smithy.kotlin.runtime.retries.delay.StandardRetryTokenBucket
1316
import aws.smithy.kotlin.runtime.util.TestPlatformProvider
17+
import aws.smithy.kotlin.runtime.util.asyncLazy
1418
import kotlinx.coroutines.test.runTest
1519
import kotlin.test.Test
1620
import kotlin.test.assertEquals
1721
import kotlin.test.assertFailsWith
22+
import kotlin.test.assertFalse
1823
import kotlin.test.assertIs
24+
import kotlin.test.assertTrue
1925

2026
class ResolveRetryStrategyTest {
2127
@Test
@@ -243,4 +249,51 @@ class ResolveRetryStrategyTest {
243249
val strategy = assertIs<StandardRetryStrategy>(resolveRetryStrategy(platform))
244250
assertEquals(expectedMaxAttempts, strategy.config.maxAttempts)
245251
}
252+
253+
@Test
254+
fun itResolvesNewRetriesFromEnvironmentVariable() = runTest {
255+
val platform = TestPlatformProvider(
256+
env = mapOf(AwsSdkSetting.AwsNewRetries.envVar to "true"),
257+
)
258+
259+
val strategy = assertIs<StandardRetryStrategy>(resolveRetryStrategy(platform))
260+
val delayConfig = assertIs<ExponentialBackoffWithJitter.Config>(strategy.config.delayProvider.config)
261+
val bucketConfig = assertIs<StandardRetryTokenBucket.Config>(strategy.config.tokenBucket.config)
262+
263+
assertEquals(STANDARD_INITIAL_DELAY, delayConfig.initialDelay)
264+
assertEquals(STANDARD_SCALE_FACTOR, delayConfig.scaleFactor)
265+
assertEquals(STANDARD_RETRY_COST, bucketConfig.retryCost)
266+
assertEquals(STANDARD_THROTTLING_RETRY_COST, bucketConfig.timeoutRetryCost)
267+
assertTrue(strategy.config.enableLongPollingBackoff)
268+
}
269+
270+
@Test
271+
fun itResolvesNewRetriesFromSystemProperty() = runTest {
272+
val platform = TestPlatformProvider(
273+
props = mapOf(AwsSdkSetting.AwsNewRetries.sysProp to "true"),
274+
)
275+
276+
val strategy = assertIs<StandardRetryStrategy>(resolveRetryStrategy(platform))
277+
val delayConfig = assertIs<ExponentialBackoffWithJitter.Config>(strategy.config.delayProvider.config)
278+
val bucketConfig = assertIs<StandardRetryTokenBucket.Config>(strategy.config.tokenBucket.config)
279+
280+
assertEquals(STANDARD_INITIAL_DELAY, delayConfig.initialDelay)
281+
assertEquals(STANDARD_SCALE_FACTOR, delayConfig.scaleFactor)
282+
assertEquals(STANDARD_RETRY_COST, bucketConfig.retryCost)
283+
assertEquals(STANDARD_THROTTLING_RETRY_COST, bucketConfig.timeoutRetryCost)
284+
assertTrue(strategy.config.enableLongPollingBackoff)
285+
}
286+
287+
@Test
288+
fun itDoesNotApplyNewRetriesDefaultsWhenDisabled() = runTest {
289+
val platform = TestPlatformProvider(
290+
env = mapOf(AwsSdkSetting.AwsNewRetries.envVar to "false"),
291+
)
292+
293+
val config = resolveRetryConfig(platform, asyncLazy { loadAwsSharedConfig(platform).activeProfile })
294+
assertFalse(config.useNewRetries)
295+
296+
val strategy = assertIs<StandardRetryStrategy>(resolveRetryStrategy(platform))
297+
assertFalse(strategy.config.enableLongPollingBackoff)
298+
}
246299
}

0 commit comments

Comments
 (0)