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);