diff --git a/pom.xml b/pom.xml index d6479d9..5e8f849 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ software.amazon.awssdk secretsmanager - 2.29.6 + 2.37.1 org.testng @@ -58,13 +58,13 @@ org.mockito mockito-core - 5.17.0 + 5.20.0 test com.github.spotbugs spotbugs-annotations - 4.8.6 + 4.9.8 compile diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCacheConfiguration.java b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCacheConfiguration.java index 744ebbd..09f58b0 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCacheConfiguration.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCacheConfiguration.java @@ -13,11 +13,11 @@ package com.amazonaws.secretsmanager.caching; -import java.util.concurrent.TimeUnit; - import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import java.time.Duration; + /** * Cache configuration options such as max cache size, ttl for cached items, etc. @@ -28,17 +28,36 @@ public class SecretCacheConfiguration { /** The default cache size. */ public static final int DEFAULT_MAX_CACHE_SIZE = 1024; - /** The default TTL for an item stored in cache before access causing a refresh. */ - public static final long DEFAULT_CACHE_ITEM_TTL = TimeUnit.HOURS.toMillis(1); + /** + * The default TTL for an item stored in cache before access causing a refresh. + */ + public static final Duration DEFAULT_CACHE_ITEM_TTL_DURATION = Duration.ofHours(1); + + /** + * The default TTL for an item stored in cache before access causing a refresh. + * + * @deprecated use DEFAULT_CACHE_ITEM_TTL_DURATION instead. + */ + @Deprecated + public static final long DEFAULT_CACHE_ITEM_TTL = DEFAULT_CACHE_ITEM_TTL_DURATION.toMillis(); /** The default version stage to use when retrieving secret values. */ public static final String DEFAULT_VERSION_STAGE = "AWSCURRENT"; - /** - * The default maximum jitter value in milliseconds to use when forcing a refresh. + /** + * The default maximum jitter value to use when forcing a refresh. * This prevents continuous refreshNow() calls by adding a random sleep. */ - public static final long DEFAULT_FORCE_REFRESH_JITTER = 100; + public static final Duration DEFAULT_FORCE_REFRESH_JITTER_DURATION = Duration.ofMillis(100); + + /** + * The default maximum jitter value to use when forcing a refresh. + * This prevents continuous refreshNow() calls by adding a random sleep. + * + * @deprecated use DEFAULT_FORCE_REFRESH_JITTER_DURATION instead + */ + @Deprecated + public static final long DEFAULT_FORCE_REFRESH_JITTER = DEFAULT_FORCE_REFRESH_JITTER_DURATION.toMillis(); /** The client this cache instance will use for accessing AWS Secrets Manager. */ private SecretsManagerClient client = null; @@ -53,12 +72,12 @@ public class SecretCacheConfiguration { private int maxCacheSize = DEFAULT_MAX_CACHE_SIZE; /** - * The number of milliseconds that a cached item is considered valid before - * requiring a refresh of the secret state. Items that have exceeded this - * TTL will be refreshed synchronously when requesting the secret value. If + * The duration that a cached item is considered valid before + * requiring a refresh of the secret state. Items that have exceeded this + * TTL will be refreshed synchronously when requesting the secret value. If * the synchronous refresh failed, the stale secret will be returned. */ - private long cacheItemTTL = DEFAULT_CACHE_ITEM_TTL; + private Duration cacheItemTTL = DEFAULT_CACHE_ITEM_TTL_DURATION; /** * The version stage that will be used when requesting the secret values for @@ -66,12 +85,7 @@ public class SecretCacheConfiguration { */ private String versionStage = DEFAULT_VERSION_STAGE; - /** - * When forcing a refresh using the refreshNow method, a random sleep - * will be performed using this value. This helps prevent code from - * executing a refreshNow in a continuous loop without waiting. - */ - private long forceRefreshJitterMillis = DEFAULT_FORCE_REFRESH_JITTER; + private Duration forceRefreshJitter = DEFAULT_FORCE_REFRESH_JITTER_DURATION; /** * Default constructor for the SecretCacheConfiguration object. @@ -186,10 +200,21 @@ public SecretCacheConfiguration withMaxCacheSize(int maxCacheSize) { /** * Returns the TTL for the cached items. + * @deprecated use getCacheItemTTLDuration() instead * * @return The TTL in milliseconds before refreshing cached items. */ + @Deprecated public long getCacheItemTTL() { + return this.cacheItemTTL.toMillis(); + } + + /** + * Returns the TTL for the cached items. + * + * @return The TTL in milliseconds before refreshing cached items. + */ + public Duration getCacheItemTTLDuration() { return this.cacheItemTTL; } @@ -197,10 +222,23 @@ public long getCacheItemTTL() { * Sets the TTL in milliseconds for the cached items. Once cached items exceed this * TTL, the item will be refreshed using the AWS Secrets Manager client. * + * @deprecated use setCacheItemTTL(Duration cacheItemTTL) instead * @param cacheItemTTL * The TTL for cached items before requiring a refresh. */ + @Deprecated public void setCacheItemTTL(long cacheItemTTL) { + this.cacheItemTTL = Duration.ofMillis(cacheItemTTL); + } + + /** + * Sets the TTL for the cached items. Once cached items exceed this + * TTL, the item will be refreshed using the AWS Secrets Manager client. + * + * @param cacheItemTTL + * The TTL for cached items before requiring a refresh. + */ + public void setCacheItemTTL(Duration cacheItemTTL) { this.cacheItemTTL = cacheItemTTL; } @@ -208,15 +246,29 @@ public void setCacheItemTTL(long cacheItemTTL) { * Sets the TTL in milliseconds for the cached items. Once cached items exceed this * TTL, the item will be refreshed using the AWS Secrets Manager client. * + * @deprecated use withCacheItemTTL(Duration cacheItemTTL) instead * @param cacheItemTTL * The TTL for cached items before requiring a refresh. * @return The updated ClientConfiguration object with the new TTL setting. */ + @Deprecated public SecretCacheConfiguration withCacheItemTTL(long cacheItemTTL) { this.setCacheItemTTL(cacheItemTTL); return this; } + /** + * Sets the TTL for the cached items. Once cached items exceed this + * TTL, the item will be refreshed using the AWS Secrets Manager client. + * + * @param cacheItemTTL The TTL for cached items before requiring a refresh. + * @return The updated ClientConfiguration object with the new TTL setting. + */ + public SecretCacheConfiguration withCacheItemTTL(Duration cacheItemTTL) { + this.setCacheItemTTL(cacheItemTTL); + return this; + } + /** * Returns the version stage that is used for requesting secret values. * @@ -252,43 +304,94 @@ public SecretCacheConfiguration withVersionStage(String versionStage) { /** * Returns the refresh jitter that is used when force refreshing secrets. + * + * @deprecated use getForceRefreshJitter() instead * - * @return The maximum jitter sleep time in milliseconds used with refreshing secrets. + * @return The maximum jitter sleep time in milliseconds used with refreshing + * secrets. */ + @Deprecated public long getForceRefreshJitterMillis() { - return this.forceRefreshJitterMillis; + return this.forceRefreshJitter.toMillis(); + } + + /** + * Returns the refresh jitter that is used when force refreshing secrets. + * + * @return The jitter sleep time used with refreshing secrets. + */ + public Duration getForceRefreshJitter() { + return this.forceRefreshJitter; } /** * Sets the maximum sleep time in milliseconds between force refresh calls. * This value is used to prevent continuous refreshNow() calls in tight loops - * by adding a random sleep between half the configured value and the full value. + * by adding a random sleep between half the configured value and the full + * value. * The value must be greater than or equal to zero. + * + * @deprecated use setForceRefreshJitter(Duration forceRefreshJitter) instead * - * @param forceRefreshJitterMillis - * The maximum sleep time in milliseconds between force refresh calls. + * @param forceRefreshJitterMillis The maximum sleep time in milliseconds + * between force refresh calls. * @throws IllegalArgumentException if the value is negative */ + @Deprecated public void setForceRefreshJitterMillis(long forceRefreshJitterMillis) { - if (forceRefreshJitterMillis < 0) { + this.setForceRefreshJitter(Duration.ofMillis(forceRefreshJitterMillis)); + } + + /** + * Sets the maximum sleep time between force refresh calls. + * This value is used to prevent continuous refreshNow() calls in tight loops + * by adding a random sleep between half the configured value and the full + * value. + * The value must be greater than or equal to zero. + * + * @param forceRefreshJitter The maximum sleep time between force refresh calls. + * @throws IllegalArgumentException if the value is negative + */ + public void setForceRefreshJitter(Duration forceRefreshJitter) { + if (forceRefreshJitter.isNegative()) { throw new IllegalArgumentException("Force refresh jitter must be greater than or equal to zero"); } - this.forceRefreshJitterMillis = forceRefreshJitterMillis; + this.forceRefreshJitter = forceRefreshJitter; } /** * Sets the maximum sleep time in milliseconds between force refresh calls. * This value is used to prevent continuous refreshNow() calls in tight loops - * by adding a random sleep between half the configured value and the full value. + * by adding a random sleep between half the configured value and the full + * value. + * + * @deprecated use withForceRefreshJitter(Duration forceRefreshJitter) instead * - * @param forceRefreshJitterMillis - * The maximum sleep time in milliseconds between force refresh calls. - * @return The updated ClientConfiguration object with the new refresh sleep time. + * @param forceRefreshJitterMillis The maximum sleep time in milliseconds + * between force refresh calls. + * @return The updated ClientConfiguration object with the new refresh sleep + * time. * @throws IllegalArgumentException if the value is negative */ + @Deprecated public SecretCacheConfiguration withForceRefreshJitterMillis(long forceRefreshJitterMillis) { this.setForceRefreshJitterMillis(forceRefreshJitterMillis); return this; } + /** + * Sets the maximum sleep time between force refresh calls. + * This value is used to prevent continuous refreshNow() calls in tight loops + * by adding a random sleep between half the configured value and the full + * value. + * + * @param forceRefreshJitter The maximum sleep time between force refresh calls. + * @return The updated ClientConfiguration object with the new refresh sleep + * time. + * @throws IllegalArgumentException if the value is negative + */ + public SecretCacheConfiguration withForceRefreshJitter(Duration forceRefreshJitter) { + this.setForceRefreshJitter(forceRefreshJitter); + return this; + } } diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java index 9c0ed78..a2e54e6 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java @@ -13,17 +13,18 @@ package com.amazonaws.secretsmanager.caching.cache; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ThreadLocalRandom; - import com.amazonaws.secretsmanager.caching.SecretCacheConfiguration; - +import software.amazon.awssdk.retries.api.internal.backoff.FixedDelayWithJitter; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRequest; import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretResponse; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; +import java.time.Instant; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; + /** * The cached secret item which contains information from the DescribeSecret * request to AWS Secrets Manager along with any associated GetSecretValue @@ -39,7 +40,13 @@ public class SecretCacheItem extends SecretCacheObject { * The next scheduled refresh time for this item. Once the item is accessed * after this time, the item will be synchronously refreshed. */ - private long nextRefreshTime = 0; + private Instant nextRefreshTime = Instant.ofEpochMilli(0); + + /** + * Fixed delay with jitter strategy used to set the next DescribeSecret call time. + * Returns a value from 0 to the cache item TTL. + */ + private FixedDelayWithJitter fixedDelayWithJitter; /** * Construct a new cached item for the secret. @@ -56,6 +63,8 @@ public SecretCacheItem(final String secretId, final SecretsManagerClient client, final SecretCacheConfiguration config) { super(secretId, client, config); + this.fixedDelayWithJitter = new FixedDelayWithJitter(ThreadLocalRandom::current, + config.getCacheItemTTLDuration()); } @Override @@ -87,10 +96,7 @@ public String toString() { protected boolean isRefreshNeeded() { if (super.isRefreshNeeded()) { return true; } if (null != this.exception) { return false; } - if (System.currentTimeMillis() >= this.nextRefreshTime) { - return true; - } - return false; + return Instant.now().isAfter(nextRefreshTime); } /** @@ -101,9 +107,8 @@ protected boolean isRefreshNeeded() { @Override protected DescribeSecretResponse executeRefresh() { DescribeSecretResponse describeSecretResponse = client.describeSecret(DescribeSecretRequest.builder().secretId(this.secretId).build()); - long ttl = this.config.getCacheItemTTL(); - this.nextRefreshTime = System.currentTimeMillis() + - ThreadLocalRandom.current().nextLong(ttl / 2,ttl + 1) ; + // Attempt count is irrelevant for fixed delay refresh strategy + this.nextRefreshTime = Instant.now().plus(this.fixedDelayWithJitter.computeDelay(1)); return describeSecretResponse; } diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java index 3d626c4..1fac592 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java @@ -13,29 +13,36 @@ package com.amazonaws.secretsmanager.caching.cache; -import java.util.concurrent.ThreadLocalRandom; - import com.amazonaws.secretsmanager.caching.SecretCacheConfiguration; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import software.amazon.awssdk.retries.api.internal.backoff.ExponentialDelayWithJitter; +import software.amazon.awssdk.retries.api.internal.backoff.FixedDelayWithJitter; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ThreadLocalRandom; + /** * Basic secret caching object. */ public abstract class SecretCacheObject { - /** The number of milliseconds to wait after an exception. */ - private static final long EXCEPTION_BACKOFF = 1000; + /** The duration to wait after an exception. */ + private static final Duration BACKOFF_MIN = Duration.ofSeconds(1); - /** The growth factor of the backoff duration. */ - private static final long EXCEPTION_BACKOFF_GROWTH_FACTOR = 2; + /** + * The maximum duration to back off to. + */ + private static final Duration BACKOFF_MAX = Duration.ofSeconds(20); /** - * The maximum number of milliseconds to wait before retrying a failed - * request. + * Backoff strategy with jitter for retries on errors. + * Mimics the backoff strategy of the AWS SDK for Java with unlimited attempts. */ - private static final long BACKOFF_PLATEAU = EXCEPTION_BACKOFF * 128; + private static ExponentialDelayWithJitter refreshStrategy = new ExponentialDelayWithJitter(ThreadLocalRandom::current, + BACKOFF_MIN, BACKOFF_MAX); /** The secret identifier for this cached object. */ protected final String secretId; @@ -63,16 +70,23 @@ public abstract class SecretCacheObject { protected RuntimeException exception = null; /** - * The number of exceptions encountered since the last successfully - * AWS Secrets Manager request. This is used to calculate an exponential - * backoff. + * The number of attempts encountered since the last successful + * AWS Secrets Manager request. This is used to calculate an exponential + * backoff. Starts at 1. + */ + private int attempts = 1; + + /** + * When forcing a refresh, always sleep with a random jitter + * to prevent coding errors that could be calling refreshNow + * in a loop. */ - private long exceptionBackoffPower = 0; + private FixedDelayWithJitter refreshNowRetryStrategy; /** * The time to wait before retrying a failed AWS Secrets Manager request. */ - private long nextRetryTime = 0; + private Instant nextRetryTime = Instant.ofEpochMilli(0); /** * Construct a new cached item for the secret. @@ -92,6 +106,8 @@ public SecretCacheObject(final String secretId, this.secretId = secretId; this.client = client; this.config = config; + refreshNowRetryStrategy = new FixedDelayWithJitter(ThreadLocalRandom::current, + this.config.getForceRefreshJitter()); } /** @@ -153,12 +169,9 @@ protected boolean isRefreshNeeded() { // // If we have exceeded our backoff time we will refresh // the secret now. - if (System.currentTimeMillis() >= this.nextRetryTime) { - return true; - } // Don't keep trying to refresh a secret that previously threw // an exception. - return false; + return Instant.now().isAfter(this.nextRetryTime); } return false; } @@ -172,27 +185,13 @@ private void refresh() { try { this.setResult(this.executeRefresh()); this.exception = null; - this.exceptionBackoffPower = 0; + this.attempts = 1; } catch (RuntimeException ex) { this.exception = ex; - // Determine the amount of growth in exception backoff time based on the growth - // factor and default backoff duration. - Long growth = 1L; - if (this.exceptionBackoffPower > 0) { - growth = (long)Math.pow(EXCEPTION_BACKOFF_GROWTH_FACTOR, this.exceptionBackoffPower); - } - growth *= EXCEPTION_BACKOFF; - // Add in EXCEPTION_BACKOFF time to make sure the random jitter will not reduce - // the wait time too low. - Long retryWait = Math.min(EXCEPTION_BACKOFF + growth, BACKOFF_PLATEAU); - if ( retryWait < BACKOFF_PLATEAU ) { - // Only increase the backoff power if we haven't hit the backoff plateau yet. - this.exceptionBackoffPower += 1; - } - - // Use random jitter with the wait time - retryWait = ThreadLocalRandom.current().nextLong(retryWait / 2, retryWait + 1); - this.nextRetryTime = System.currentTimeMillis() + retryWait; + // Increment before computing delay. Otherwise the retry for attempts = 1 is immediate. + this.attempts++; + Duration retryWait = refreshStrategy.computeDelay(this.attempts); + this.nextRetryTime = Instant.now().plus(retryWait); } } @@ -205,23 +204,21 @@ private void refresh() { */ public boolean refreshNow() throws InterruptedException { this.refreshNeeded = true; - // When forcing a refresh, always sleep with a random jitter - // to prevent coding errors that could be calling refreshNow - // in a loop. - long jitter = this.config.getForceRefreshJitterMillis(); - long sleep = ThreadLocalRandom.current() - .nextLong( - jitter / 2, - jitter + 1); - // Make sure we are not waiting for the next refresh after an - // exception. If we are, sleep based on the retry delay of - // the refresh to prevent a hard loop in attempting to refresh a - // secret that continues to throw an exception such as AccessDenied. + + // Attempts is irrelevant for a fixed delay retry strategy + Duration sleep = this.refreshNowRetryStrategy.computeDelay(1); if (null != this.exception) { - long wait = this.nextRetryTime - System.currentTimeMillis(); - sleep = Math.max(wait, sleep); + // Make sure we are not waiting for the next refresh after an + // exception. If we are, sleep based on the retry delay of + // the refresh to prevent a hard loop in attempting to refresh a + // secret that continues to throw an exception such as AccessDenied. + Duration wait = Duration.between( + this.nextRetryTime, + java.time.Instant.now()); + // pick the max. + sleep = sleep.compareTo(wait) >= 0 ? sleep : wait; } - Thread.sleep(sleep); + Thread.sleep(sleep.toMillis()); // Perform the requested refresh synchronized (lock) { diff --git a/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheConfigurationTest.java b/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheConfigurationTest.java new file mode 100644 index 0000000..b1e79d7 --- /dev/null +++ b/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheConfigurationTest.java @@ -0,0 +1,13 @@ +package com.amazonaws.secretsmanager.caching; + +import org.testng.Assert; +import org.testng.annotations.Test; + +public class SecretCacheConfigurationTest { + @Deprecated + @Test + public void getCacheItemTTLIsPositive() { + SecretCacheConfiguration c = new SecretCacheConfiguration(); + Assert.assertTrue(c.getCacheItemTTL() > 0, c.getCacheItemTTL() + " is not greater than zero"); + } +} diff --git a/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java b/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java index 4f4e93b..a0f17d5 100644 --- a/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java +++ b/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java @@ -15,6 +15,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -22,9 +23,16 @@ import java.util.Map; import java.util.function.IntConsumer; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.testng.Assert; @@ -32,6 +40,8 @@ import org.testng.annotations.Test; import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder; import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRequest; @@ -83,31 +93,95 @@ public void secretCacheConstructorTest() { } catch (Exception e) { } } - + + @Test + public void secretCacheConstructorTestDefault() { + try (MockedStatic mockSmc = Mockito.mockStatic(SecretsManagerClient.class)) { + SecretsManagerClientBuilder mock = Mockito.mock(SecretsManagerClientBuilder.class); + mockSmc.when(SecretsManagerClient::builder).thenReturn(mock); + ClientOverrideConfiguration overrideConfiguration = Mockito.mock(ClientOverrideConfiguration.class); + ClientOverrideConfiguration.Builder builder = Mockito.mock(ClientOverrideConfiguration.Builder.class); + when(overrideConfiguration.toBuilder()).thenReturn(builder); + when(builder.putAdvancedOption(any(), anyString())).thenReturn(builder); + when(builder.build()).thenReturn(overrideConfiguration); + when(mock.overrideConfiguration(Mockito.any(ClientOverrideConfiguration.class))).thenReturn(mock); + when(mock.overrideConfiguration()).thenReturn(overrideConfiguration); + when(mock.build()).thenReturn(asm); + SecretCache sc1 = new SecretCache(); + sc1.close(); + + verify(builder).putAdvancedOption(eq(SdkAdvancedClientOption.USER_AGENT_SUFFIX), anyString()); + } + } + + @Test + public void secretCacheConstructorTestCustomClient() { + SecretsManagerClientBuilder mock = Mockito.mock(SecretsManagerClientBuilder.class); + ClientOverrideConfiguration overrideConfiguration = Mockito.mock(ClientOverrideConfiguration.class); + ClientOverrideConfiguration.Builder builder = Mockito.mock(ClientOverrideConfiguration.Builder.class); + when(overrideConfiguration.toBuilder()).thenReturn(builder); + when(builder.putAdvancedOption(any(), anyString())).thenReturn(builder); + when(builder.build()).thenReturn(overrideConfiguration); + when(mock.overrideConfiguration(Mockito.any(ClientOverrideConfiguration.class))).thenReturn(mock); + when(mock.overrideConfiguration()).thenReturn(overrideConfiguration); + when(mock.build()).thenReturn(asm); + + SecretCache sc = new SecretCache(mock); + sc.close(); + + verify(builder).putAdvancedOption(eq(SdkAdvancedClientOption.USER_AGENT_SUFFIX), anyString()); + } + + @Deprecated @Test public void testForceRefreshJitterConfiguration() { // Test default value SecretCacheConfiguration config = new SecretCacheConfiguration(); - Assert.assertEquals(config.getForceRefreshJitterMillis(), SecretCacheConfiguration.DEFAULT_FORCE_REFRESH_JITTER); - + Assert.assertEquals(config.getForceRefreshJitterMillis(), + SecretCacheConfiguration.DEFAULT_FORCE_REFRESH_JITTER); + // Test setting a custom value long customJitter = 250L; config.setForceRefreshJitterMillis(customJitter); Assert.assertEquals(config.getForceRefreshJitterMillis(), customJitter); - + // Test zero is valid config.setForceRefreshJitterMillis(0); Assert.assertEquals(config.getForceRefreshJitterMillis(), 0); } - - @Test(expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = "Force refresh jitter must be greater than or equal to zero") + + @Test + public void testForceRefreshJitterDurationConfiguration() { + // Test default value + SecretCacheConfiguration config = new SecretCacheConfiguration(); + Assert.assertEquals(config.getForceRefreshJitter(), + SecretCacheConfiguration.DEFAULT_FORCE_REFRESH_JITTER_DURATION); + + // Test setting a custom value + Duration customJitter = Duration.ofMillis(250); + config.setForceRefreshJitter(customJitter); + Assert.assertEquals(config.getForceRefreshJitter(), customJitter); + + // Test zero is valid + config.setForceRefreshJitter(Duration.ZERO); + Assert.assertEquals(config.getForceRefreshJitter(), Duration.ZERO); + } + + @Deprecated + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Force refresh jitter must be greater than or equal to zero") public void testForceRefreshJitterValidation() { // Test that negative values throw an exception SecretCacheConfiguration config = new SecretCacheConfiguration(); config.setForceRefreshJitterMillis(-1); } + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Force refresh jitter must be greater than or equal to zero") + public void testForceRefreshJitterDurationValidation() { + // Test that negative values throw an exception + SecretCacheConfiguration config = new SecretCacheConfiguration(); + config.setForceRefreshJitter(Duration.ofMillis(-1)); + } + @Test public void basicSecretCacheTest() { final String secret = "basicSecretCacheTest"; @@ -328,7 +402,7 @@ public void basicSecretCacheRefreshTest() throws Throwable { SecretCache sc = new SecretCache(new SecretCacheConfiguration() .withClient(asm) - .withCacheItemTTL(500)); + .withCacheItemTTL(Duration.ofMillis(500))); // Request the secret multiple times and verify the correct result repeat(10, n -> Assert.assertEquals(sc.getSecretString(""), secret)); @@ -343,6 +417,58 @@ public void basicSecretCacheRefreshTest() throws Throwable { // Verify that the refresh occurred after the ttl Mockito.verify(asm, Mockito.times(2)).describeSecret(Mockito.any(DescribeSecretRequest.class)); Mockito.verify(asm, Mockito.times(1)).getSecretValue(Mockito.any(GetSecretValueRequest.class)); + sc.close(); + } + + @Test + public void basicSecretCacheRefreshNullVersionIdsToStagesReturnsNull() throws Throwable { + Mockito.when(describeSecretResponse.versionIdsToStages()).thenReturn(null); + Mockito.when(asm.describeSecret(Mockito.any(DescribeSecretRequest.class))).thenReturn(describeSecretResponse); + + SecretCache sc = new SecretCache(new SecretCacheConfiguration() + .withClient(asm)); + + Assert.assertNull(sc.getSecretString("")); + + Mockito.verify(asm, Mockito.times(1)).describeSecret(Mockito.any(DescribeSecretRequest.class)); + sc.close(); + } + + @Deprecated + @Test + public void secretCacheRefreshAfterVersionChangeTestDeprecated() throws Throwable { + // Kept around to cover .withCacheItemTTL(long cacheItemTTL) + final String secret = "secretCacheRefreshAfterVersionChangeTestDeprecated"; + Map> versionMap = new HashMap>(); + versionMap.put("versionId", Arrays.asList("AWSCURRENT")); + Mockito.when(describeSecretResponse.versionIdsToStages()).thenReturn(versionMap); + + getSecretValueResponse = GetSecretValueResponse.builder().secretString(secret).build(); + + Mockito.when(asm.describeSecret(Mockito.any(DescribeSecretRequest.class))).thenReturn(describeSecretResponse); + Mockito.when(asm.getSecretValue(Mockito.any(GetSecretValueRequest.class))).thenReturn(getSecretValueResponse); + SecretCache sc = new SecretCache(new SecretCacheConfiguration() + .withClient(asm) + .withForceRefreshJitterMillis(1) + .withCacheItemTTL(500)); + + // Request the secret multiple times and verify the correct result + repeat(5, n -> Assert.assertEquals(sc.getSecretString(""), secret)); + + // Verify that multiple requests did not call the API + Mockito.verify(asm, Mockito.times(1)).describeSecret(Mockito.any(DescribeSecretRequest.class)); + Mockito.verify(asm, Mockito.times(1)).getSecretValue(Mockito.any(GetSecretValueRequest.class)); + + // Wait long enough to expire the TTL on the cached item. + Thread.sleep(502); + versionMap.clear(); + // Simulate a change in secret version values + versionMap.put("versionIdNew", Arrays.asList("AWSCURRENT")); + repeat(5, n -> Assert.assertEquals(sc.getSecretString(""), secret)); + // Verify that the refresh occurred after the ttl + Mockito.verify(asm, Mockito.times(2)).describeSecret(Mockito.any(DescribeSecretRequest.class)); + Mockito.verify(asm, Mockito.times(2)).getSecretValue(Mockito.any(GetSecretValueRequest.class)); + sc.close(); } @Test @@ -358,7 +484,10 @@ public void secretCacheRefreshAfterVersionChangeTest() throws Throwable { Mockito.when(asm.getSecretValue(Mockito.any(GetSecretValueRequest.class))).thenReturn(getSecretValueResponse); SecretCache sc = new SecretCache(new SecretCacheConfiguration() .withClient(asm) - .withCacheItemTTL(500)); + .withMaxCacheSize(10) + .withVersionStage("AWSCURRENT") + .withForceRefreshJitter(Duration.ofMillis(1)) + .withCacheItemTTL(Duration.ofMillis(500))); // Request the secret multiple times and verify the correct result repeat(5, n -> Assert.assertEquals(sc.getSecretString(""), secret)); @@ -368,7 +497,7 @@ public void secretCacheRefreshAfterVersionChangeTest() throws Throwable { Mockito.verify(asm, Mockito.times(1)).getSecretValue(Mockito.any(GetSecretValueRequest.class)); // Wait long enough to expire the TTL on the cached item. - Thread.sleep(600); + Thread.sleep(502); versionMap.clear(); // Simulate a change in secret version values versionMap.put("versionIdNew", Arrays.asList("AWSCURRENT")); @@ -376,6 +505,7 @@ public void secretCacheRefreshAfterVersionChangeTest() throws Throwable { // Verify that the refresh occurred after the ttl Mockito.verify(asm, Mockito.times(2)).describeSecret(Mockito.any(DescribeSecretRequest.class)); Mockito.verify(asm, Mockito.times(2)).getSecretValue(Mockito.any(GetSecretValueRequest.class)); + sc.close(); } @Test diff --git a/src/test/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItemTest.java b/src/test/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItemTest.java index 4d25c04..16f2baf 100644 --- a/src/test/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItemTest.java +++ b/src/test/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItemTest.java @@ -13,15 +13,17 @@ package com.amazonaws.secretsmanager.caching.cache; +import com.amazonaws.secretsmanager.caching.SecretCacheConfiguration; import org.testng.Assert; import org.testng.annotations.Test; public class SecretCacheItemTest { @Test public void cacheItemEqualsTest() { - SecretCacheItem i1 = new SecretCacheItem("test", null, null); - SecretCacheItem i2 = new SecretCacheItem("test", null, null); - SecretCacheItem i3 = new SecretCacheItem("test3", null, null); + SecretCacheConfiguration config = new SecretCacheConfiguration(); + SecretCacheItem i1 = new SecretCacheItem("test", null, config); + SecretCacheItem i2 = new SecretCacheItem("test", null, config); + SecretCacheItem i3 = new SecretCacheItem("test3", null, config); Assert.assertEquals(i1, i2); Assert.assertNotEquals(i1, null); Assert.assertNotEquals(i1, i3); diff --git a/src/test/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersionTest.java b/src/test/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersionTest.java index 760f50a..fe676e9 100644 --- a/src/test/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersionTest.java +++ b/src/test/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersionTest.java @@ -13,16 +13,18 @@ package com.amazonaws.secretsmanager.caching.cache; +import com.amazonaws.secretsmanager.caching.SecretCacheConfiguration; import org.testng.Assert; import org.testng.annotations.Test; public class SecretCacheVersionTest { @Test public void cacheVersionEqualsTest() { - SecretCacheVersion i1 = new SecretCacheVersion("test", "version", null, null); - SecretCacheVersion i2 = new SecretCacheVersion("test", "version", null, null); - SecretCacheVersion i3 = new SecretCacheVersion("test3", "version", null, null); - SecretCacheVersion i4 = new SecretCacheVersion("test", "version4", null, null); + SecretCacheConfiguration config = new SecretCacheConfiguration(); + SecretCacheVersion i1 = new SecretCacheVersion("test", "version", null, config); + SecretCacheVersion i2 = new SecretCacheVersion("test", "version", null, config); + SecretCacheVersion i3 = new SecretCacheVersion("test3", "version", null, config); + SecretCacheVersion i4 = new SecretCacheVersion("test", "version4", null, config); Assert.assertEquals(i1, i2); Assert.assertNotEquals(i1, null); Assert.assertNotEquals(i1, i3);