diff --git a/core/retries/src/main/java/software/amazon/awssdk/retries/AdaptiveRetryStrategy.java b/core/retries/src/main/java/software/amazon/awssdk/retries/AdaptiveRetryStrategy.java index de4e9127fd68..9615bfa89edc 100644 --- a/core/retries/src/main/java/software/amazon/awssdk/retries/AdaptiveRetryStrategy.java +++ b/core/retries/src/main/java/software/amazon/awssdk/retries/AdaptiveRetryStrategy.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.retries; +import java.time.Duration; import java.util.function.Predicate; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -64,15 +65,33 @@ public interface AdaptiveRetryStrategy extends RetryStrategy { * */ static AdaptiveRetryStrategy.Builder builder() { + return builder(false); + } + + /** + * Create a new {@link AdaptiveRetryStrategy.Builder} with v2.0 or v2.1 retry constants. + * + * @param retries2026Enabled when {@code true}, uses v2.1 constants (50ms base delay, differentiated token costs); + * when {@code false}, uses v2.0 constants (100ms base delay, uniform token costs) + */ + static AdaptiveRetryStrategy.Builder builder(boolean retries2026Enabled) { + Duration baseDelay = retries2026Enabled ? DefaultRetryStrategy.Standard.BASE_DELAY_V21 + : DefaultRetryStrategy.Standard.BASE_DELAY_V20; + int exceptionCost = retries2026Enabled ? DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST_V21 + : DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST_V20; + // 2026 does not treat throttling exceptions differently from others + int throttlingCost = retries2026Enabled ? DefaultRetryStrategy.Standard.THROTTLING_EXCEPTION_TOKEN_COST_V21 + : exceptionCost; return DefaultAdaptiveRetryStrategy .builder() .maxAttempts(DefaultRetryStrategy.Adaptive.MAX_ATTEMPTS) .tokenBucketStore(TokenBucketStore.builder() .tokenBucketMaxCapacity(DefaultRetryStrategy.Standard.TOKEN_BUCKET_SIZE) .build()) - .tokenBucketExceptionCost(DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST) + .tokenBucketExceptionCost(exceptionCost) + .throttlingTokenBucketExceptionCost(throttlingCost) .rateLimiterTokenBucketStore(RateLimiterTokenBucketStore.builder().build()) - .backoffStrategy(BackoffStrategy.exponentialDelay(DefaultRetryStrategy.Standard.BASE_DELAY, + .backoffStrategy(BackoffStrategy.exponentialDelay(baseDelay, DefaultRetryStrategy.Standard.MAX_BACKOFF)) .throttlingBackoffStrategy(BackoffStrategy.exponentialDelay( DefaultRetryStrategy.Standard.THROTTLED_BASE_DELAY, diff --git a/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java b/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java index b02709c847e6..879ddd002d49 100644 --- a/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java +++ b/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java @@ -87,11 +87,19 @@ public static AdaptiveRetryStrategy.Builder adaptiveStrategyBuilder() { static final class Standard { static final int MAX_ATTEMPTS = 3; - static final Duration BASE_DELAY = Duration.ofMillis(100); + + // v2.1 constants + static final Duration BASE_DELAY_V21 = Duration.ofMillis(50); + static final int DEFAULT_EXCEPTION_TOKEN_COST_V21 = 14; + static final int THROTTLING_EXCEPTION_TOKEN_COST_V21 = 5; + + // v2.0 constants + static final Duration BASE_DELAY_V20 = Duration.ofMillis(100); + static final int DEFAULT_EXCEPTION_TOKEN_COST_V20 = 5; + static final Duration THROTTLED_BASE_DELAY = Duration.ofSeconds(1); static final Duration MAX_BACKOFF = Duration.ofSeconds(20); static final int TOKEN_BUCKET_SIZE = 500; - static final int DEFAULT_EXCEPTION_TOKEN_COST = 5; private Standard() { } diff --git a/core/retries/src/main/java/software/amazon/awssdk/retries/StandardRetryStrategy.java b/core/retries/src/main/java/software/amazon/awssdk/retries/StandardRetryStrategy.java index dff43d017333..30b06e2e5c99 100644 --- a/core/retries/src/main/java/software/amazon/awssdk/retries/StandardRetryStrategy.java +++ b/core/retries/src/main/java/software/amazon/awssdk/retries/StandardRetryStrategy.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.retries; +import java.time.Duration; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.retries.api.BackoffStrategy; @@ -56,6 +57,23 @@ public interface StandardRetryStrategy extends RetryStrategy { * */ static Builder builder() { + return builder(false); + } + + /** + * Create a new {@link StandardRetryStrategy.Builder} with v2.0 or v2.1 retry constants. + * + * @param retries2026Enabled when {@code true}, uses v2.1 constants (50ms base delay, differentiated token costs); + * when {@code false}, uses v2.0 constants (100ms base delay, uniform token costs) + */ + static Builder builder(boolean retries2026Enabled) { + Duration baseDelay = retries2026Enabled ? DefaultRetryStrategy.Standard.BASE_DELAY_V21 + : DefaultRetryStrategy.Standard.BASE_DELAY_V20; + int exceptionCost = retries2026Enabled ? DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST_V21 + : DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST_V20; + // 2026 does not treat throttling exceptions differently from others + int throttlingCost = retries2026Enabled ? DefaultRetryStrategy.Standard.THROTTLING_EXCEPTION_TOKEN_COST_V21 + : exceptionCost; return DefaultStandardRetryStrategy .builder() .maxAttempts(DefaultRetryStrategy.Standard.MAX_ATTEMPTS) @@ -63,8 +81,9 @@ static Builder builder() { .builder() .tokenBucketMaxCapacity(DefaultRetryStrategy.Standard.TOKEN_BUCKET_SIZE) .build()) - .tokenBucketExceptionCost(DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST) - .backoffStrategy(BackoffStrategy.exponentialDelay(DefaultRetryStrategy.Standard.BASE_DELAY, + .tokenBucketExceptionCost(exceptionCost) + .throttlingTokenBucketExceptionCost(throttlingCost) + .backoffStrategy(BackoffStrategy.exponentialDelay(baseDelay, DefaultRetryStrategy.Standard.MAX_BACKOFF)) .throttlingBackoffStrategy(BackoffStrategy.exponentialDelay(DefaultRetryStrategy.Standard.THROTTLED_BASE_DELAY, DefaultRetryStrategy.Standard.MAX_BACKOFF)); diff --git a/core/retries/src/main/java/software/amazon/awssdk/retries/internal/BaseRetryStrategy.java b/core/retries/src/main/java/software/amazon/awssdk/retries/internal/BaseRetryStrategy.java index 89bce90f7155..9a00ba6d2cb0 100644 --- a/core/retries/src/main/java/software/amazon/awssdk/retries/internal/BaseRetryStrategy.java +++ b/core/retries/src/main/java/software/amazon/awssdk/retries/internal/BaseRetryStrategy.java @@ -57,6 +57,7 @@ public abstract class BaseRetryStrategy implements DefaultAwareRetryStrategy { protected final BackoffStrategy throttlingBackoffStrategy; protected final Predicate treatAsThrottling; protected final int exceptionCost; + protected final int throttlingExceptionCost; protected final TokenBucketStore tokenBucketStore; protected final Set defaultsAdded; protected final boolean useClientDefaults; @@ -71,6 +72,8 @@ public abstract class BaseRetryStrategy implements DefaultAwareRetryStrategy { this.throttlingBackoffStrategy = Validate.paramNotNull(builder.throttlingBackoffStrategy, "throttlingBackoffStrategy"); this.treatAsThrottling = Validate.paramNotNull(builder.treatAsThrottling, "treatAsThrottling"); this.exceptionCost = Validate.paramNotNull(builder.exceptionCost, "exceptionCost"); + this.throttlingExceptionCost = builder.throttlingExceptionCost != null + ? builder.throttlingExceptionCost : this.exceptionCost; this.tokenBucketStore = Validate.paramNotNull(builder.tokenBucketStore, "tokenBucketStore"); this.defaultsAdded = Collections.unmodifiableSet( Validate.paramNotNull(new HashSet<>(builder.defaultsAdded), "defaultsAdded")); @@ -194,10 +197,13 @@ protected void updateStateForRetry(RefreshRetryTokenRequest request) { * amount for the specific kind of failure. */ protected int exceptionCost(RefreshRetryTokenRequest request) { - if (circuitBreakerEnabled) { - return exceptionCost; + if (!circuitBreakerEnabled) { + return 0; } - return 0; + if (treatAsThrottling.test(request.failure())) { + return throttlingExceptionCost; + } + return exceptionCost; } /** @@ -408,6 +414,7 @@ public abstract static class Builder implements DefaultAwareRetryStrategy.Builde private Boolean circuitBreakerEnabled; private Boolean useClientDefaults; private Integer exceptionCost; + private Integer throttlingExceptionCost; private BackoffStrategy backoffStrategy; private BackoffStrategy throttlingBackoffStrategy; private Predicate treatAsThrottling = throwable -> false; @@ -423,6 +430,7 @@ public abstract static class Builder implements DefaultAwareRetryStrategy.Builde this.maxAttempts = strategy.maxAttempts; this.circuitBreakerEnabled = strategy.circuitBreakerEnabled; this.exceptionCost = strategy.exceptionCost; + this.throttlingExceptionCost = strategy.throttlingExceptionCost; this.backoffStrategy = strategy.backoffStrategy; this.throttlingBackoffStrategy = strategy.throttlingBackoffStrategy; this.treatAsThrottling = strategy.treatAsThrottling; @@ -463,6 +471,10 @@ void setTokenBucketExceptionCost(int exceptionCost) { this.exceptionCost = exceptionCost; } + void setThrottlingTokenBucketExceptionCost(int throttlingExceptionCost) { + this.throttlingExceptionCost = throttlingExceptionCost; + } + void setUseClientDefaults(Boolean useClientDefaults) { this.useClientDefaults = useClientDefaults; } diff --git a/core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultAdaptiveRetryStrategy.java b/core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultAdaptiveRetryStrategy.java index e5e56b602b39..c52c7859526f 100644 --- a/core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultAdaptiveRetryStrategy.java +++ b/core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultAdaptiveRetryStrategy.java @@ -129,6 +129,11 @@ public Builder tokenBucketExceptionCost(int exceptionCost) { return this; } + public Builder throttlingTokenBucketExceptionCost(int throttlingExceptionCost) { + setThrottlingTokenBucketExceptionCost(throttlingExceptionCost); + return this; + } + public Builder rateLimiterTokenBucketStore(RateLimiterTokenBucketStore rateLimiterTokenBucketStore) { this.rateLimiterTokenBucketStore = rateLimiterTokenBucketStore; return this; diff --git a/core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultStandardRetryStrategy.java b/core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultStandardRetryStrategy.java index 3a6a120fa398..5a10f781ea87 100644 --- a/core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultStandardRetryStrategy.java +++ b/core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultStandardRetryStrategy.java @@ -90,6 +90,11 @@ public Builder tokenBucketExceptionCost(int exceptionCost) { return this; } + public Builder throttlingTokenBucketExceptionCost(int throttlingExceptionCost) { + setThrottlingTokenBucketExceptionCost(throttlingExceptionCost); + return this; + } + public Builder tokenBucketStore(TokenBucketStore tokenBucketStore) { setTokenBucketStore(tokenBucketStore); return this; diff --git a/core/retries/src/test/java/software/amazon/awssdk/retries/AdaptiveRetryStrategyV21ConstantsTest.java b/core/retries/src/test/java/software/amazon/awssdk/retries/AdaptiveRetryStrategyV21ConstantsTest.java new file mode 100644 index 000000000000..9cc5ce02dc22 --- /dev/null +++ b/core/retries/src/test/java/software/amazon/awssdk/retries/AdaptiveRetryStrategyV21ConstantsTest.java @@ -0,0 +1,137 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.retries; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.retries.api.AcquireInitialTokenResponse; +import software.amazon.awssdk.retries.api.RefreshRetryTokenRequest; +import software.amazon.awssdk.retries.api.RefreshRetryTokenResponse; +import software.amazon.awssdk.retries.api.RetryStrategy; +import software.amazon.awssdk.retries.api.RetryToken; +import software.amazon.awssdk.retries.api.internal.AcquireInitialTokenRequestImpl; +import software.amazon.awssdk.retries.internal.DefaultRetryToken; + +/** + * Tests that {@code AdaptiveRetryStrategy.builder(boolean retries2026Enabled)} selects the correct + * v2.0 or v2.1 constants for base delay, exception token cost, and throttling token cost. + */ +class AdaptiveRetryStrategyV21ConstantsTest { + + private static final int BUCKET_CAPACITY = 500; + + @Test + void v21Enabled_nonThrottlingRetry_deducts14Tokens() { + RetryStrategy strategy = AdaptiveRetryStrategy.builder(true) + .retryOnException(t -> true) + .treatAsThrottling(t -> false) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("transient")); + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 14); + } + + @Test + void v21Enabled_throttlingRetry_deducts5Tokens() { + RetryStrategy strategy = AdaptiveRetryStrategy.builder(true) + .retryOnException(t -> true) + .treatAsThrottling(t -> true) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("throttled")); + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5); + } + + @Test + void v20_nonThrottlingRetry_deducts5Tokens() { + RetryStrategy strategy = AdaptiveRetryStrategy.builder(false) + .retryOnException(t -> true) + .treatAsThrottling(t -> false) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("transient")); + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5); + } + + @Test + void v20_throttlingRetry_deducts5Tokens() { + RetryStrategy strategy = AdaptiveRetryStrategy.builder(false) + .retryOnException(t -> true) + .treatAsThrottling(t -> true) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("throttled")); + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5); + } + + @Test + void v21Enabled_backoffUses50msBaseDelay() { + RetryStrategy strategy = AdaptiveRetryStrategy.builder(true) + .retryOnException(t -> true) + .build(); + + RefreshRetryTokenResponse response = refreshToken(strategy, new RuntimeException("err")); + // First retry delay should include exponential backoff component in [0, 50ms] + assertThat(response.delay()).isBetween(Duration.ZERO, Duration.ofMillis(50)); + } + + @Test + void v20_backoffUses100msBaseDelay() { + RetryStrategy strategy = AdaptiveRetryStrategy.builder(false) + .retryOnException(t -> true) + .build(); + + RefreshRetryTokenResponse response = refreshToken(strategy, new RuntimeException("err")); + // First retry delay should include exponential backoff component in [0, 100ms] + assertThat(response.delay()).isBetween(Duration.ZERO, Duration.ofMillis(100)); + } + + @Test + void noArgBuilder_usesV20Constants() { + RetryStrategy strategy = AdaptiveRetryStrategy.builder() + .retryOnException(t -> true) + .treatAsThrottling(t -> false) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("transient")); + // v2.0: exception cost is 5 + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5); + } + + /** + * Acquires an initial token, triggers one retry. Returns the token after the retry (before success). + */ + private DefaultRetryToken retryOnceBeforeSuccess(RetryStrategy strategy, Exception failure) { + AcquireInitialTokenResponse initial = strategy.acquireInitialToken(AcquireInitialTokenRequestImpl.create("test")); + RetryToken token = initial.token(); + + RefreshRetryTokenResponse refreshResponse = strategy.refreshRetryToken( + RefreshRetryTokenRequest.builder().token(token).failure(failure).build()); + + return (DefaultRetryToken) refreshResponse.token(); + } + + /** + * Acquires an initial token and triggers one refresh to get the backoff delay. + */ + private RefreshRetryTokenResponse refreshToken(RetryStrategy strategy, Exception failure) { + AcquireInitialTokenResponse initial = strategy.acquireInitialToken(AcquireInitialTokenRequestImpl.create("test")); + return strategy.refreshRetryToken( + RefreshRetryTokenRequest.builder().token(initial.token()).failure(failure).build()); + } +} diff --git a/core/retries/src/test/java/software/amazon/awssdk/retries/StandardRetryStrategyV21ConstantsTest.java b/core/retries/src/test/java/software/amazon/awssdk/retries/StandardRetryStrategyV21ConstantsTest.java new file mode 100644 index 000000000000..16dd17307694 --- /dev/null +++ b/core/retries/src/test/java/software/amazon/awssdk/retries/StandardRetryStrategyV21ConstantsTest.java @@ -0,0 +1,138 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.retries; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.retries.api.AcquireInitialTokenResponse; +import software.amazon.awssdk.retries.api.BackoffStrategy; +import software.amazon.awssdk.retries.api.RefreshRetryTokenRequest; +import software.amazon.awssdk.retries.api.RefreshRetryTokenResponse; +import software.amazon.awssdk.retries.api.RetryStrategy; +import software.amazon.awssdk.retries.api.RetryToken; +import software.amazon.awssdk.retries.api.internal.AcquireInitialTokenRequestImpl; +import software.amazon.awssdk.retries.internal.DefaultRetryToken; + +/** + * Tests that {@code StandardRetryStrategy.builder(boolean retries2026Enabled)} selects the correct + * v2.0 or v2.1 constants for base delay, exception token cost, and throttling token cost. + */ +class StandardRetryStrategyV21ConstantsTest { + + private static final int BUCKET_CAPACITY = 500; + + @Test + void v21Enabled_nonThrottlingRetry_deducts14Tokens() { + RetryStrategy strategy = StandardRetryStrategy.builder(true) + .retryOnException(t -> true) + .treatAsThrottling(t -> false) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("transient")); + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 14); + } + + @Test + void v21Enabled_throttlingRetry_deducts5Tokens() { + RetryStrategy strategy = StandardRetryStrategy.builder(true) + .retryOnException(t -> true) + .treatAsThrottling(t -> true) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("throttled")); + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5); + } + + @Test + void v20_nonThrottlingRetry_deducts5Tokens() { + RetryStrategy strategy = StandardRetryStrategy.builder(false) + .retryOnException(t -> true) + .treatAsThrottling(t -> false) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("transient")); + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5); + } + + @Test + void v20_throttlingRetry_deducts5Tokens() { + RetryStrategy strategy = StandardRetryStrategy.builder(false) + .retryOnException(t -> true) + .treatAsThrottling(t -> true) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("throttled")); + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5); + } + + @Test + void v21Enabled_backoffUses50msBaseDelay() { + RetryStrategy strategy = StandardRetryStrategy.builder(true) + .retryOnException(t -> true) + .build(); + + RefreshRetryTokenResponse response = refreshToken(strategy, new RuntimeException("err")); + // First retry delay should be in [0, 50ms] (jittered) + assertThat(response.delay()).isBetween(Duration.ZERO, Duration.ofMillis(50)); + } + + @Test + void v20_backoffUses100msBaseDelay() { + RetryStrategy strategy = StandardRetryStrategy.builder(false) + .retryOnException(t -> true) + .build(); + + RefreshRetryTokenResponse response = refreshToken(strategy, new RuntimeException("err")); + // First retry delay should be in [0, 100ms] (jittered) + assertThat(response.delay()).isBetween(Duration.ZERO, Duration.ofMillis(100)); + } + + @Test + void noArgBuilder_usesV20Constants() { + RetryStrategy strategy = StandardRetryStrategy.builder() + .retryOnException(t -> true) + .treatAsThrottling(t -> false) + .build(); + + DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("transient")); + // v2.0: exception cost is 5 + assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5); + } + + /** + * Acquires an initial token, triggers one retry. Returns the token after the retry (before success). + */ + private DefaultRetryToken retryOnceBeforeSuccess(RetryStrategy strategy, Exception failure) { + AcquireInitialTokenResponse initial = strategy.acquireInitialToken(AcquireInitialTokenRequestImpl.create("test")); + RetryToken token = initial.token(); + + RefreshRetryTokenResponse refreshResponse = strategy.refreshRetryToken( + RefreshRetryTokenRequest.builder().token(token).failure(failure).build()); + + return (DefaultRetryToken) refreshResponse.token(); + } + + /** + * Acquires an initial token and triggers one refresh to get the backoff delay. + */ + private RefreshRetryTokenResponse refreshToken(RetryStrategy strategy, Exception failure) { + AcquireInitialTokenResponse initial = strategy.acquireInitialToken(AcquireInitialTokenRequestImpl.create("test")); + return strategy.refreshRetryToken( + RefreshRetryTokenRequest.builder().token(initial.token()).failure(failure).build()); + } +}