This value must be less than or equal to {@link #prefetchTime(Duration)}. Setting this equal to + * {@code prefetchTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking). + * + *
By default, this is 1 minute. + * + * @param staleTime the duration before expiration that triggers mandatory (blocking) refresh + */ + Builder staleTime(Duration staleTime); + + /** + * Configure the amount of time, relative to credential expiration, that defines the advisory refresh window. When + * the cached credentials are within this window (i.e., their remaining lifetime is less than this duration), the + * provider will attempt to refresh them proactively. If the refresh fails, the provider returns the existing cached + * credentials without error and will not attempt another refresh until a backoff period has elapsed. + * + *
When {@link #asyncCredentialUpdateEnabled(Boolean)} is true, advisory refreshes happen in a background thread + * and callers immediately receive the current cached credentials. When it is false, one caller will block to perform + * the refresh while other callers receive the current cached credentials. + * + *
This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to + * {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking). + * + *
If not explicitly set, the advisory refresh window is computed dynamically based on the credential's + * remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90 + * minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each + * successful refresh. + * + * @param prefetchTime the duration before expiration that triggers advisory (proactive) refresh + */ + Builder prefetchTime(Duration prefetchTime); } private static final class BuilderImpl implements Builder { @@ -323,6 +382,8 @@ private static final class BuilderImpl implements Builder { private Boolean asyncCredentialUpdateEnabled; private String asyncThreadName; private String sourceChain; + private Duration staleTime; + private Duration prefetchTime; private BuilderImpl() { asyncThreadName("container-credentials-provider"); @@ -333,6 +394,8 @@ private BuilderImpl(ContainerCredentialsProvider credentialsProvider) { this.asyncCredentialUpdateEnabled = credentialsProvider.asyncCredentialUpdateEnabled; this.asyncThreadName = credentialsProvider.asyncThreadName; this.sourceChain = credentialsProvider.sourceChain; + this.staleTime = credentialsProvider.staleTime; + this.prefetchTime = credentialsProvider.prefetchTime; } @Override @@ -365,6 +428,26 @@ public void setAsyncThreadName(String asyncThreadName) { asyncThreadName(asyncThreadName); } + @Override + public Builder staleTime(Duration staleTime) { + this.staleTime = staleTime; + return this; + } + + public void setStaleTime(Duration staleTime) { + staleTime(staleTime); + } + + @Override + public Builder prefetchTime(Duration prefetchTime) { + this.prefetchTime = prefetchTime; + return this; + } + + public void setPrefetchTime(Duration prefetchTime) { + prefetchTime(prefetchTime); + } + /** * An optional string denoting previous credentials providers that are chained with this one. *
Note: This method is primarily intended for use by AWS SDK internal components
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java
index d9059aa0298e..2a2cd6a7b9fa 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java
@@ -16,9 +16,11 @@
package software.amazon.awssdk.auth.credentials;
import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.auth.credentials.internal.LazyAwsCredentialsProvider;
+import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileFileSupplier;
import software.amazon.awssdk.utils.SdkAutoCloseable;
@@ -134,6 +136,11 @@ public AwsCredentials resolveCredentials() {
return providerChain.resolveCredentials();
}
+ @Override
+ public CompletableFuture By default, this is disabled. This value must be less than or equal to {@link #prefetchTime(Duration)}. Setting this equal to
+ * {@code prefetchTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
+ *
+ * By default, this is 1 minute.
*
- * @param duration the amount of time before expiration for when to consider the credentials to be stale and need refresh
+ * @param duration the duration before expiration that triggers mandatory (blocking) refresh
*/
Builder staleTime(Duration duration);
+ /**
+ * Configure the amount of time, relative to credential expiration, that defines the advisory refresh window. When
+ * the cached credentials are within this window (i.e., their remaining lifetime is less than this duration), the
+ * provider will attempt to refresh them proactively. If the refresh fails, the provider returns the existing cached
+ * credentials without error and will not attempt another refresh until a backoff period has elapsed.
+ *
+ * When {@link #asyncCredentialUpdateEnabled(Boolean)} is true, advisory refreshes happen in a background thread
+ * and callers immediately receive the current cached credentials. When it is false, one caller will block to perform
+ * the refresh while other callers receive the current cached credentials.
+ *
+ * This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
+ * {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
+ *
+ * If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
+ * remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
+ * minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
+ * successful refresh.
+ *
+ * @param duration the duration before expiration that triggers advisory (proactive) refresh
+ */
+ Builder prefetchTime(Duration duration);
+
/**
* Build a {@link InstanceProfileCredentialsProvider} from the provided configuration.
*/
@@ -382,6 +430,7 @@ static final class BuilderImpl implements Builder {
private Supplier Note: This method is primarily intended for use by AWS SDK internal components
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java
index 08d9cf373ab2..8d764e331834 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java
@@ -24,8 +24,11 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;
+import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
import software.amazon.awssdk.utils.DateUtils;
@@ -37,6 +40,7 @@
import software.amazon.awssdk.utils.Validate;
import software.amazon.awssdk.utils.builder.CopyableBuilder;
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
+import software.amazon.awssdk.utils.cache.CacheRefreshUtils;
import software.amazon.awssdk.utils.cache.CachedSupplier;
import software.amazon.awssdk.utils.cache.NonBlocking;
import software.amazon.awssdk.utils.cache.RefreshResult;
@@ -44,21 +48,33 @@
/**
* A credentials provider that can load credentials from an external process. This is used to support the credential_process
* setting in the profile credentials file. See
- * sourcing credentials
- * from external processes for more information.
+ * sourcing
+ * credentials from external processes for more information.
*
- *
- * This class can be initialized using {@link #builder()}.
+ * This provider caches credentials returned by the external process and refreshes them before they expire. If a refresh
+ * attempt fails after credentials have entered the mandatory refresh window, the provider raises an exception to the caller
+ * (unlike other credential providers that return cached credentials on failure).
*
- *
- * Available settings:
+ * This class can be initialized using {@link #builder()}.
+ *
+ * Regardless of this setting, callers will block if credentials enter the mandatory refresh window (defined by
+ * {@link #staleTime(Duration)}).
*
* By default, this is disabled. This value must be less than or equal to {@link #prefetchTime(Duration)}. Setting this equal to
+ * {@code prefetchTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
+ *
+ * By default, this is 1 minute.
+ *
+ * @param staleTime the duration before expiration that triggers mandatory (blocking) refresh
+ */
+ public Builder staleTime(Duration staleTime) {
+ this.staleTime = staleTime;
+ return this;
+ }
+
+ /**
+ * Configure the amount of time, relative to credential expiration, that defines the advisory refresh window. When
+ * the cached credentials are within this window (i.e., their remaining lifetime is less than this duration), the
+ * provider will attempt to refresh them proactively. If the refresh fails during the advisory window, the provider
+ * returns the existing cached credentials. If the refresh fails after credentials have entered the mandatory refresh
+ * window (defined by {@link #staleTime(Duration)}), the provider raises an exception.
+ *
+ * When {@link #asyncCredentialUpdateEnabled(Boolean)} is true, advisory refreshes happen in a background thread
+ * and callers immediately receive the current cached credentials. When it is false, one caller will block to perform
+ * the refresh while other callers receive the current cached credentials.
+ *
+ * This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
+ * {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
+ *
+ * If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
+ * remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
+ * minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
+ * successful refresh.
+ *
+ * @param prefetchTime the duration before expiration that triggers advisory (proactive) refresh
+ */
+ public Builder prefetchTime(Duration prefetchTime) {
+ this.prefetchTime = prefetchTime;
+ return this;
+ }
+
/**
* Configure the command that should be executed to retrieve credentials.
* See {@link ProcessBuilder} for details on how this command is used.
@@ -338,10 +434,13 @@ public Builder command(List Default: 15 seconds. When {@link #asyncCredentialUpdateEnabled(Boolean)} is true, advisory refreshes happen in a background thread
+ * and callers immediately receive the current cached credentials. When it is false, one caller will block to perform
+ * the refresh while other callers receive the current cached credentials.
*
- * By default, this is 5 minutes.
+ * This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
+ * {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
+ *
+ * If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
+ * remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
+ * minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
+ * successful refresh.
+ *
+ * @param prefetchTime the duration before expiration that triggers advisory (proactive) refresh
*/
Builder prefetchTime(Duration prefetchTime);
/**
- * Configure the amount of time, relative to STS token expiration, that the cached credentials are considered stale and
- * must be updated. All threads will block until the value is updated.
+ * Configure the amount of time, relative to credential expiration, that defines the mandatory refresh window. When
+ * the cached credentials are within this window (i.e., their remaining lifetime is less than this duration), the
+ * provider will block all callers until a refresh attempt completes. If the refresh attempt fails, the provider
+ * returns the cached credentials and will not attempt another refresh until a backoff period has elapsed.
+ *
+ * This value must be less than or equal to {@link #prefetchTime(Duration)}. Setting this equal to
+ * {@code prefetchTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
* By default, this is 1 minute.
+ *
+ * @param staleTime the duration before expiration that triggers mandatory (blocking) refresh
*/
Builder staleTime(Duration staleTime);
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java
index 6999e25af250..46068658eb7c 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java
@@ -15,10 +15,12 @@
package software.amazon.awssdk.auth.credentials.internal;
+import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.utils.IoUtils;
import software.amazon.awssdk.utils.Lazy;
import software.amazon.awssdk.utils.SdkAutoCloseable;
@@ -45,6 +47,15 @@ public AwsCredentials resolveCredentials() {
return delegate.getValue().resolveCredentials();
}
+ @Override
+ public CompletableFuture When a target service rejects credentials with an authentication error,
+ * the SDK calls this method so the provider can mark its cache for refresh.
+ * The next call to {@link #resolveIdentity} will attempt to fetch fresh credentials.
+ *
+ * Implementations MUST only invalidate if the currently-cached identity matches
+ * the rejected identity (e.g., same access key ID).
+ *
+ * The default implementation is a no-op, suitable for providers that do not cache.
+ *
+ * @param identity The identity that was rejected by the service.
+ */
+ default CompletableFuture When this returns {@code true}, the SDK may invalidate cached credentials so that the next attempt
+ * resolves fresh ones.
+ *
+ * @return true if the exception is classified as an authentication error, otherwise false.
+ */
+ public boolean isAuthenticationError() {
+ return false;
+ }
+
/**
* @return {@link Builder} instance to construct a new {@link SdkServiceException}.
*/
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java
index ac795a34c494..51f46a5ffb1b 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java
@@ -155,11 +155,12 @@ public static When a service returns an authentication error (as determined by
+ * {@link SdkServiceException#isAuthenticationError()}), this helper retrieves the
+ * {@link SelectedAuthScheme} from the execution context and calls
+ * {@link IdentityProvider#invalidate} so the next retry attempt resolves fresh credentials.
+ *
+ * All exceptions from the invalidation path are caught and logged at debug level.
+ * Invalidation failures never disrupt the normal request/retry flow.
+ */
+@SdkInternalApi
+public final class AuthErrorInvalidationHelper {
+
+ private static final Logger LOG = Logger.loggerFor(AuthErrorInvalidationHelper.class);
+
+ private AuthErrorInvalidationHelper() {
+ }
+
+ /**
+ * Checks whether the given exception is an auth error that should trigger
+ * credential invalidation. If so, retrieves the identity provider from the
+ * {@link SelectedAuthScheme} and calls invalidate() on it.
+ *
+ * @param exception The exception from the failed request attempt
+ * @param context The request execution context containing auth scheme info
+ */
+ public static void invalidateIfAuthError(Throwable exception, RequestExecutionContext context) {
+ if (!(exception instanceof SdkServiceException)) {
+ return;
+ }
+
+ SdkServiceException serviceException = (SdkServiceException) exception;
+ if (!serviceException.isAuthenticationError()) {
+ return;
+ }
+
+ SelectedAuthScheme> selectedAuthScheme =
+ context.executionAttributes().getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME);
+
+ if (selectedAuthScheme == null || selectedAuthScheme.identityProvider() == null) {
+ return;
+ }
+
+ try {
+ doInvalidate(selectedAuthScheme);
+ } catch (Exception e) {
+ LOG.debug(() -> "Failed to invalidate identity provider after auth error: " + e.getMessage(), e);
+ }
+ }
+
+ private static By default, this is enabled.
*/
Builder asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled);
/**
- * Configure the amount of time, relative to login token expiration, that the cached credentials are considered stale and
- * should no longer be used. All threads will block until the value is updated.
+ * Configure the amount of time, relative to credential expiration, that defines the mandatory refresh window. When
+ * the cached credentials are within this window (i.e., their remaining lifetime is less than this duration), the
+ * provider will block all callers until a refresh attempt completes. If the refresh attempt fails, the provider
+ * returns the cached credentials and will not attempt another refresh until a backoff period has elapsed.
+ *
+ * This value must be less than or equal to {@link #prefetchTime(Duration)}. Setting this equal to
+ * {@code prefetchTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
* By default, this is 1 minute.
+ *
+ * @param staleTime the duration before expiration that triggers mandatory (blocking) refresh
*/
Builder staleTime(Duration staleTime);
/**
- * Configure the amount of time, relative to signin token expiration, that the cached credentials are considered close to
- * stale and should be updated.
- *
- * Prefetch updates will occur between the specified time and the stale time of the provider. Prefetch updates may be
- * asynchronous. See {@link #asyncCredentialUpdateEnabled}.
+ * Configure the amount of time, relative to credential expiration, that defines the advisory refresh window. When
+ * the cached credentials are within this window (i.e., their remaining lifetime is less than this duration), the
+ * provider will attempt to refresh them proactively. If the refresh fails, the provider returns the existing cached
+ * credentials without error and will not attempt another refresh until a backoff period has elapsed.
+ *
+ * When {@link #asyncCredentialUpdateEnabled(Boolean)} is true, advisory refreshes happen in a background thread
+ * and callers immediately receive the current cached credentials. When it is false, one caller will block to perform
+ * the refresh while other callers receive the current cached credentials.
*
- * By default, this is 5 minutes.
+ * This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
+ * {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
+ *
+ * If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
+ * remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
+ * minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
+ * successful refresh.
+ *
+ * @param prefetchTime the duration before expiration that triggers advisory (proactive) refresh
*/
Builder prefetchTime(Duration prefetchTime);
@@ -337,6 +408,17 @@ public interface Builder extends CopyableBuilder By default, this is disabled. This value must be less than or equal to {@link #prefetchTime(Duration)}. Setting this equal to
+ * {@code prefetchTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
* By default, this is 1 minute. When {@link #asyncCredentialUpdateEnabled(Boolean)} is true, advisory refreshes happen in a background thread
+ * and callers immediately receive the current cached credentials. When it is false, one caller will block to perform
+ * the refresh while other callers receive the current cached credentials.
+ *
+ * This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
+ * {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
- * Prefetch updates will occur between the specified time and the stale time of the provider. Prefetch updates may be
- * asynchronous. See {@link #asyncCredentialUpdateEnabled}.
+ * If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
+ * remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
+ * minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
+ * successful refresh. By default, this is 5 minutes. Regardless of this setting, callers will block if credentials enter the mandatory refresh window (defined by
+ * {@link #staleTime(Duration)}).
*
* By default, this is disabled. This value must be less than or equal to {@link #prefetchTime(Duration)}. Setting this equal to
+ * {@code prefetchTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
* By default, this is 1 minute. When {@link #asyncCredentialUpdateEnabled(Boolean)} is true, advisory refreshes happen in a background thread
+ * and callers immediately receive the current cached credentials. When it is false, one caller will block to perform
+ * the refresh while other callers receive the current cached credentials.
+ *
+ * This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
+ * {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
- * Prefetch updates will occur between the specified time and the stale time of the provider. Prefetch updates may be
- * asynchronous. See {@link #asyncCredentialUpdateEnabled}.
+ * If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
+ * remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
+ * minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
+ * successful refresh. By default, this is 5 minutes. Dynamic window selection:
+ * This field is NEVER modified by {@code invalidate()} — backoff remains independently tracked.
+ */
+ private volatile Instant nextAllowedRefreshTime;
+
private CachedSupplier(Builder This method MUST NOT discard the cached value.
+ * This method MUST NOT clear or modify {@code nextAllowedRefreshTime}.
+ *
+ * If there is no cached value, this method is a no-op — the predicate will not be called.
+ *
+ * If the lock is not immediately available, this method returns without invalidating.
+ * A held lock means either a refresh or another invalidation is already in progress.
+ * In either case it is safe to skip: a refresh will replace the cached value shortly,
+ * and a concurrent invalidation will mark it stale.
+ *
+ * @param matchesCachedValue A predicate that returns true if the cached value
+ * is the one that should be invalidated. The value passed
+ * to the predicate is guaranteed to be non-null.
+ */
+ public void invalidate(Predicate This is used for errors where the credential source has definitively indicated that the current
+ * authentication state is invalid and requires user intervention (e.g., expired SSO tokens,
+ * changed user credentials).
+ *
+ * By default, no exceptions are considered non-recoverable (all failures trigger static stability
+ * backoff when {@link StaleValueBehavior#ALLOW} is configured).
+ */
+ public Builder If a {@link Builder#nonRecoverableErrorPredicate(Predicate)} is configured and returns {@code true}
+ * for the exception, it is re-thrown immediately without extending the stale time.
+ *
+ * Value retrieval will never fail as long as the cache has succeeded at least once,
+ * unless the error is non-recoverable.
*/
ALLOW
}
diff --git a/utils/src/test/java/software/amazon/awssdk/utils/cache/CacheRefreshUtilsTest.java b/utils/src/test/java/software/amazon/awssdk/utils/cache/CacheRefreshUtilsTest.java
new file mode 100644
index 000000000000..adb6ae4bb98e
--- /dev/null
+++ b/utils/src/test/java/software/amazon/awssdk/utils/cache/CacheRefreshUtilsTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.utils.cache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link CacheRefreshUtils}.
+ */
+public class CacheRefreshUtilsTest {
+
+ private static final Instant NOW = Instant.parse("2024-01-01T00:00:00Z");
+
+ @Test
+ public void remainingLifetimeUnder20Minutes_returns5MinuteWindow() {
+ // 19 minutes remaining
+ Instant expiration = NOW.plus(Duration.ofMinutes(19));
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(5));
+ }
+
+ @Test
+ public void remainingLifetimeExactly0_returns5MinuteWindow() {
+ // 0 minutes remaining (already expired)
+ Duration window = CacheRefreshUtils.computePrefetchWindow(NOW, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(5));
+ }
+
+ @Test
+ public void remainingLifetime5Minutes_returns5MinuteWindow() {
+ Instant expiration = NOW.plus(Duration.ofMinutes(5));
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(5));
+ }
+
+ @Test
+ public void remainingLifetimeExactly20Minutes_returns15MinuteWindow() {
+ Instant expiration = NOW.plus(Duration.ofMinutes(20));
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(15));
+ }
+
+ @Test
+ public void remainingLifetime45Minutes_returns15MinuteWindow() {
+ Instant expiration = NOW.plus(Duration.ofMinutes(45));
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(15));
+ }
+
+ @Test
+ public void remainingLifetime89Minutes_returns15MinuteWindow() {
+ Instant expiration = NOW.plus(Duration.ofMinutes(89));
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(15));
+ }
+
+ @Test
+ public void remainingLifetimeExactly90Minutes_returns60MinuteWindow() {
+ Instant expiration = NOW.plus(Duration.ofMinutes(90));
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(60));
+ }
+
+ @Test
+ public void remainingLifetime6Hours_returns60MinuteWindow() {
+ Instant expiration = NOW.plus(Duration.ofHours(6));
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(60));
+ }
+
+ @Test
+ public void remainingLifetime12Hours_returns60MinuteWindow() {
+ Instant expiration = NOW.plus(Duration.ofHours(12));
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(60));
+ }
+
+ @Test
+ public void remainingLifetimeNegative_returns5MinuteWindow() {
+ // Expiration is in the past
+ Instant expiration = NOW.minus(Duration.ofMinutes(5));
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, null, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(5));
+ }
+
+ @Test
+ public void explicitPrefetchTime_returnsExplicitValue() {
+ Instant expiration = NOW.plus(Duration.ofHours(6));
+ Duration explicitPrefetch = Duration.ofMinutes(30);
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, explicitPrefetch, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(30));
+ }
+
+ @Test
+ public void explicitPrefetchTime_ignoresRemainingLifetime() {
+ // Even with short remaining lifetime, explicit value is used
+ Instant expiration = NOW.plus(Duration.ofMinutes(10));
+ Duration explicitPrefetch = Duration.ofMinutes(60);
+ Duration window = CacheRefreshUtils.computePrefetchWindow(expiration, explicitPrefetch, NOW);
+ assertThat(window).isEqualTo(Duration.ofMinutes(60));
+ }
+}
diff --git a/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierTest.java b/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierTest.java
index 159e2d69b6e9..e6bb3bbf1bdc 100644
--- a/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierTest.java
+++ b/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierTest.java
@@ -35,11 +35,13 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -364,25 +366,228 @@ public void throwIsHiddenIfValueIsStaleInAllowMode() throws InterruptedException
}
@Test
- public void maxStaleFailureJitter_shouldNotReturnNegativeOrCycleLowValues() {
- CachedSupplierAvailable settings:
*
- *
*/
@SdkPublicApi
@@ -71,9 +87,9 @@ public final class ProcessCredentialsProvider
private static final JsonNodeParser PARSER = JsonNodeParser.builder()
.removeErrorLocations(true)
.build();
+ private static final Duration DEFAULT_STALE_TIME = Duration.ofMinutes(1);
private final List void accept(SignerProperty key, S value) {
existingAuthScheme.authSchemeOption().forEachIdentityProperty(mergedOption::putIdentityPropertyIfAbsent);
- return new SelectedAuthScheme<>(
- selectedAuthScheme.identity(),
- selectedAuthScheme.signer(),
- mergedOption.build()
- );
+ return SelectedAuthScheme. void accept(SignerProperty key, S value) {
// Only update SELECTED_AUTH_SCHEME if at least one property was re-applied.
if (changed[0]) {
attrs.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME,
- new SelectedAuthScheme<>(currentScheme.identity(),
- currentScheme.signer(),
- mergedOption.build()));
+ SelectedAuthScheme.
+ *
+ */
+ private static boolean isNonRecoverableError(RuntimeException e) {
+ if (e instanceof InvalidTokenException) {
+ return true;
+ }
+
+ AccessDeniedException ade = extractAccessDeniedException(e);
+ if (ade == null) {
+ return false;
+ }
+ return ade.error() == OAuth2ErrorCode.TOKEN_EXPIRED
+ || ade.error() == OAuth2ErrorCode.USER_CREDENTIALS_CHANGED
+ || ade.error() == OAuth2ErrorCode.INSUFFICIENT_PERMISSIONS;
+ }
+
+ private static AccessDeniedException extractAccessDeniedException(RuntimeException e) {
+ if (e instanceof AccessDeniedException) {
+ return (AccessDeniedException) e;
+ }
+ if (e.getCause() instanceof AccessDeniedException) {
+ return (AccessDeniedException) e.getCause();
+ }
+ return null;
+ }
+
+ /**
+ * The amount of time, relative to credential expiration, that defines the mandatory refresh window. When credentials are
+ * within this window, all threads will block until the credentials are updated.
*/
public Duration staleTime() {
return staleTime;
}
/**
- * The amount of time, relative to session token expiration, that the cached credentials are considered close to stale and
- * should be updated.
+ * The amount of time, relative to credential expiration, that defines the advisory refresh window. When credentials are
+ * within this window, the provider proactively attempts to refresh them.
*/
public Duration prefetchTime() {
return prefetchTime;
@@ -255,6 +296,13 @@ public AwsCredentials resolveCredentials() {
return credentialCache.get();
}
+ @Override
+ public CompletableFuture
+ *
+ *
+ * @param expiration the credential's expiration time
+ * @param prefetchTime the explicitly configured prefetch window, or {@code null} to compute dynamically
+ * @param now the current time
+ * @return the Duration to use as the advisory refresh window
+ */
+ public static Duration computePrefetchWindow(Instant expiration, Duration prefetchTime, Instant now) {
+ if (prefetchTime != null) {
+ return prefetchTime;
+ }
+
+ Duration remainingLifetime = Duration.between(now, expiration);
+ long remainingMinutes = remainingLifetime.toMinutes();
+
+ if (remainingMinutes < THRESHOLD_MEDIUM_MINUTES) {
+ return WINDOW_SHORT;
+ } else if (remainingMinutes < THRESHOLD_LONG_MINUTES) {
+ return WINDOW_MEDIUM;
+ } else {
+ return WINDOW_LONG;
+ }
+ }
+}
diff --git a/utils/src/main/java/software/amazon/awssdk/utils/cache/CachedSupplier.java b/utils/src/main/java/software/amazon/awssdk/utils/cache/CachedSupplier.java
index e8ecc4d741d1..3700ecbc0142 100644
--- a/utils/src/main/java/software/amazon/awssdk/utils/cache/CachedSupplier.java
+++ b/utils/src/main/java/software/amazon/awssdk/utils/cache/CachedSupplier.java
@@ -23,9 +23,9 @@
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Predicate;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
@@ -54,6 +54,16 @@ public class CachedSupplier