2525import java .util .concurrent .atomic .AtomicBoolean ;
2626import java .util .concurrent .locks .Lock ;
2727import java .util .concurrent .locks .ReentrantLock ;
28+ import java .util .function .Predicate ;
2829import java .util .function .Supplier ;
2930import software .amazon .awssdk .annotations .SdkProtectedApi ;
3031import software .amazon .awssdk .annotations .SdkTestInternalApi ;
@@ -54,14 +55,14 @@ public class CachedSupplier<T> implements Supplier<T>, SdkAutoCloseable {
5455 private static final Duration BLOCKING_REFRESH_MAX_WAIT = Duration .ofSeconds (5 );
5556
5657 /**
57- * Minimum backoff duration in seconds when a refresh fails (inclusive).
58+ * Minimum backoff duration when a refresh fails (inclusive).
5859 */
59- private static final int STATIC_STABILITY_BACKOFF_MIN_SECONDS = 300 ;
60+ private static final Duration STATIC_STABILITY_BACKOFF_MIN = Duration . ofMinutes ( 5 ) ;
6061
6162 /**
62- * Maximum backoff duration in seconds when a refresh fails (inclusive).
63+ * Maximum backoff duration when a refresh fails (inclusive).
6364 */
64- private static final int STATIC_STABILITY_BACKOFF_MAX_SECONDS = 600 ;
65+ private static final Duration STATIC_STABILITY_BACKOFF_MAX = Duration . ofMinutes ( 10 ) ;
6566
6667
6768 /**
@@ -112,6 +113,12 @@ public class CachedSupplier<T> implements Supplier<T>, SdkAutoCloseable {
112113 */
113114 private final Random jitterRandom = new Random ();
114115
116+ /**
117+ * Predicate that determines whether an exception represents a non-recoverable refresh failure
118+ * that should bypass static stability (i.e., be re-thrown immediately without extending expiration).
119+ */
120+ private final Predicate <RuntimeException > cacheInvalidatingPredicate ;
121+
115122 private CachedSupplier (Builder <T > builder ) {
116123 Validate .notNull (builder .supplier , "builder.supplier" );
117124 Validate .notNull (builder .jitterEnabled , "builder.jitterEnabled" );
@@ -121,6 +128,7 @@ private CachedSupplier(Builder<T> builder) {
121128 this .staleValueBehavior = Validate .notNull (builder .staleValueBehavior , "builder.staleValueBehavior" );
122129 this .clock = Validate .notNull (builder .clock , "builder.clock" );
123130 this .cachedValueName = Validate .notNull (builder .cachedValueName , "builder.cachedValueName" );
131+ this .cacheInvalidatingPredicate = builder .cacheInvalidatingPredicate ;
124132 }
125133
126134 /**
@@ -276,14 +284,15 @@ private RefreshResult<T> handleFetchFailure(RuntimeException e) {
276284 throw e ;
277285 case ALLOW :
278286 // Cache-invalidating errors bypass static stability
279- if (e instanceof CacheInvalidatingError ) {
287+ if (cacheInvalidatingPredicate != null && cacheInvalidatingPredicate . test ( e ) ) {
280288 throw e ;
281289 }
282290
283291 // Uniform random backoff: 5-10 minutes
284- long backoffSeconds = STATIC_STABILITY_BACKOFF_MIN_SECONDS
292+ long backoffSeconds = STATIC_STABILITY_BACKOFF_MIN . getSeconds ()
285293 + jitterRandom .nextInt (
286- STATIC_STABILITY_BACKOFF_MAX_SECONDS - STATIC_STABILITY_BACKOFF_MIN_SECONDS + 1 );
294+ (int ) (STATIC_STABILITY_BACKOFF_MAX .getSeconds ()
295+ - STATIC_STABILITY_BACKOFF_MIN .getSeconds () + 1 ));
287296 Instant extendedStaleTime = now .plusSeconds (backoffSeconds );
288297
289298 log .warn (() -> "(" + cachedValueName + ") Credential refresh failed: " + e .getMessage ()
@@ -301,13 +310,14 @@ private RefreshResult<T> handleFetchFailure(RuntimeException e) {
301310
302311 // Not yet stale — we're in the prefetch window. Handle failure based on mode.
303312 if (staleValueBehavior == StaleValueBehavior .ALLOW ) {
304- if (e instanceof CacheInvalidatingError ) {
313+ if (cacheInvalidatingPredicate != null && cacheInvalidatingPredicate . test ( e ) ) {
305314 throw e ;
306315 }
307316 // During prefetch window failure: extend prefetchTime to suppress further attempts
308- long backoffSeconds = STATIC_STABILITY_BACKOFF_MIN_SECONDS
317+ long backoffSeconds = STATIC_STABILITY_BACKOFF_MIN . getSeconds ()
309318 + jitterRandom .nextInt (
310- STATIC_STABILITY_BACKOFF_MAX_SECONDS - STATIC_STABILITY_BACKOFF_MIN_SECONDS + 1 );
319+ (int ) (STATIC_STABILITY_BACKOFF_MAX .getSeconds ()
320+ - STATIC_STABILITY_BACKOFF_MIN .getSeconds () + 1 ));
311321 Instant extendedPrefetchTime = now .plusSeconds (backoffSeconds );
312322
313323 log .warn (() -> "(" + cachedValueName + ") Credential refresh failed: " + e .getMessage ()
@@ -406,6 +416,7 @@ public static final class Builder<T> {
406416 private StaleValueBehavior staleValueBehavior = StaleValueBehavior .STRICT ;
407417 private Clock clock = Clock .systemUTC ();
408418 private String cachedValueName = "unknown" ;
419+ private Predicate <RuntimeException > cacheInvalidatingPredicate ;
409420
410421 private Builder (Supplier <RefreshResult <T >> supplier ) {
411422 this .supplier = supplier ;
@@ -445,6 +456,23 @@ public Builder<T> cachedValueName(String cachedValueName) {
445456 return this ;
446457 }
447458
459+ /**
460+ * Configure a predicate that determines whether an exception represents a non-recoverable refresh failure
461+ * that should bypass static stability. When the predicate returns {@code true} for a given exception,
462+ * the exception will be re-thrown immediately without extending the cached value's expiration.
463+ *
464+ * <p>This is used for errors where the credential source has definitively indicated that the current
465+ * authentication state is invalid and requires user intervention (e.g., expired SSO tokens,
466+ * changed user credentials).</p>
467+ *
468+ * <p>By default, no exceptions are considered cache-invalidating (all failures trigger static stability
469+ * backoff when {@link StaleValueBehavior#ALLOW} is configured).</p>
470+ */
471+ public Builder <T > cacheInvalidatingPredicate (Predicate <RuntimeException > cacheInvalidatingPredicate ) {
472+ this .cacheInvalidatingPredicate = cacheInvalidatingPredicate ;
473+ return this ;
474+ }
475+
448476 /**
449477 * Configure the clock used for this cached supplier. Configurable for testing.
450478 */
@@ -523,8 +551,8 @@ public enum StaleValueBehavior {
523551 * Allow stale values to be returned from the cache with static stability semantics. On refresh failure,
524552 * extends the stale time by a uniformly random backoff between 5 and 10 minutes (300-600 seconds).
525553 *
526- * <p>If the failure is a {@link CacheInvalidatingError}, the exception is re-thrown immediately
527- * without extending the stale time.</p>
554+ * <p>If a {@link Builder#cacheInvalidatingPredicate(Predicate)} is configured and returns {@code true}
555+ * for the exception, it is re-thrown immediately without extending the stale time.</p>
528556 *
529557 * <p>Value retrieval will never fail as long as the cache has succeeded at least once,
530558 * unless the error is cache-invalidating.</p>
0 commit comments