Skip to content

Commit d170e9f

Browse files
committed
Update BoundedCache
1 parent ba1b63f commit d170e9f

File tree

3 files changed

+54
-33
lines changed

3 files changed

+54
-33
lines changed

utils/src/main/java/software/amazon/awssdk/utils/cache/bounded/BoundedCache.java

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,108 +17,128 @@
1717

1818
import java.util.Iterator;
1919
import java.util.concurrent.ConcurrentHashMap;
20+
import java.util.concurrent.atomic.AtomicInteger;
2021
import java.util.function.Function;
2122
import software.amazon.awssdk.annotations.SdkProtectedApi;
2223
import software.amazon.awssdk.annotations.ThreadSafe;
23-
import software.amazon.awssdk.utils.Logger;
2424
import software.amazon.awssdk.utils.Validate;
2525

2626
/**
27-
* A thread-safe cache implementation that returns the value for a specified key,
28-
* retrieving it by either getting the stored value from the cache or using a supplied function to calculate that value
29-
* and add it to the cache.
27+
* A thread-safe cache implementation that returns the value for a specified key, retrieving it by either getting the stored
28+
* value from the cache or using a supplied function to calculate that value and add it to the cache.
3029
* <p>
31-
* When the cache is full, a new value will push out an unspecified value.
30+
* When the cache is full, batch eviction of random values will be performed, with a default evictionBatchSize of 10.
3231
* <p>
33-
* The user can configure the maximum size of the cache, which is set to a default of 100.
32+
* The user can configure the maximum size of the cache, which is set to a default of 150.
3433
* <p>
35-
* Null values are not cached.
34+
* Keys must not be null, otherwise an error will be thrown. Null values are not cached.
3635
*/
3736
@SdkProtectedApi
3837
@ThreadSafe
3938
public final class BoundedCache<K, V> {
40-
41-
private static final Logger log = Logger.loggerFor(BoundedCache.class);
42-
43-
private static final int DEFAULT_SIZE = 100;
39+
private static final int DEFAULT_CACHE_SIZE = 150;
40+
private static final int DEFAULT_EVICTION_BATCH_SIZE = 10;
4441

4542
private final ConcurrentHashMap<K, V> cache;
46-
private final Function<K, V> valueSupplier;
43+
private final Function<K, V> valueMappingFunction;
4744
private final int maxCacheSize;
45+
private final int evictionBatchSize;
4846
private final Object cacheLock;
49-
50-
private BoundedCache(Builder<K, V> builder) {
51-
this.valueSupplier = builder.supplier;
52-
this.maxCacheSize = builder.maxSize != null ?
53-
Validate.isPositive(builder.maxSize, "maxSize")
54-
: DEFAULT_SIZE;
47+
private final AtomicInteger cacheSize;
48+
49+
private BoundedCache(Builder<K, V> b) {
50+
this.valueMappingFunction = b.mappingFunction;
51+
this.maxCacheSize = b.maxSize != null ? Validate.isPositive(b.maxSize, "maxSize") : DEFAULT_CACHE_SIZE;
52+
this.evictionBatchSize = b.evictionBatchSize != null ?
53+
Validate.isPositive(b.evictionBatchSize, "evictionBatchSize") :
54+
DEFAULT_EVICTION_BATCH_SIZE;
5555
this.cache = new ConcurrentHashMap<>();
5656
this.cacheLock = new Object();
57+
this.cacheSize = new AtomicInteger();
5758
}
5859

5960
/**
60-
* Get a value based on the key. If the value exists in the cache, it's returned.
61+
* Get a value based on the key. The key must not be null, otherwise an error is thrown.
62+
* If the value exists in the cache, it's returned.
6163
* Otherwise, the value is calculated based on the supplied function {@link Builder#builder(Function)}.
6264
*/
6365
public V get(K key) {
66+
Validate.paramNotNull(key, "key");
6467
V value = cache.get(key);
6568
if (value != null) {
6669
return value;
6770
}
6871

69-
V newValue = valueSupplier.apply(key);
72+
V newValue = valueMappingFunction.apply(key);
73+
74+
// If the value is null, just return it without caching
7075
if (newValue == null) {
7176
return null;
7277
}
7378

7479
synchronized (cacheLock) {
80+
// Check again inside the synchronized block in case another thread added the value
7581
value = cache.get(key);
7682
if (value != null) {
7783
return value;
7884
}
7985

80-
if (cache.size() >= maxCacheSize) {
86+
if (cacheSize.get() >= maxCacheSize) {
8187
cleanup();
8288
}
8389

8490
cache.put(key, newValue);
91+
cacheSize.incrementAndGet();
8592
return newValue;
8693
}
8794
}
8895

8996
/**
90-
* Clean up the cache by removing an unspecified entry
97+
* Clean up the cache by batch removing random entries of evictionBatchSize
9198
*/
9299
private void cleanup() {
93100
Iterator<K> iterator = cache.keySet().iterator();
94-
if (iterator.hasNext()) {
95-
K key = iterator.next();
96-
cache.remove(key);
101+
int count = 0;
102+
while (iterator.hasNext() && count < evictionBatchSize) {
103+
iterator.next();
104+
iterator.remove();
105+
count++;
106+
cacheSize.decrementAndGet();
97107
}
98108
}
99109

100110
public int size() {
101-
return cache.size();
111+
return cacheSize.get();
102112
}
103113

104-
public static <K, V> BoundedCache.Builder<K, V> builder(Function<K, V> supplier) {
105-
return new Builder<>(supplier);
114+
public boolean containsKey(K key) {
115+
return cache.containsKey(key);
116+
}
117+
118+
public static <K, V> BoundedCache.Builder<K, V> builder(Function<K, V> mappingFunction) {
119+
return new Builder<>(mappingFunction);
106120
}
107121

108122
public static final class Builder<K, V> {
109123

110-
private final Function<K, V> supplier;
124+
private final Function<K, V> mappingFunction;
111125
private Integer maxSize;
126+
private Integer evictionBatchSize;
112127

113-
private Builder(Function<K, V> supplier) {
114-
this.supplier = supplier;
128+
private Builder(Function<K, V> mappingFunction) {
129+
this.mappingFunction = mappingFunction;
115130
}
116131

117132
public Builder<K, V> maxSize(Integer maxSize) {
118133
this.maxSize = maxSize;
119134
return this;
120135
}
121136

137+
public Builder<K, V> evictionBatchSize(Integer evictionBatchSize) {
138+
this.evictionBatchSize = evictionBatchSize;
139+
return this;
140+
}
141+
122142
public BoundedCache<K, V> build() {
123143
return new BoundedCache<>(this);
124144
}

utils/src/main/java/software/amazon/awssdk/utils/uri/SdkUri.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public final class SdkUri {
3737
private static final int MAX_INT_DIGITS_BASE_10 = 10;
3838

3939
/*
40-
* The default BoundedCache size is 100, but for a single service call we cache at least 3 different URIs so the cache size is
41-
* increased a bit to account for the different URIs.
40+
* Same value as default BoundedCache size. This contrasts to the default LruCache size of 100, since for a single service
41+
* call we cache at least 3 different URIs, so the cache size is increased a bit to account for the different URIs.
4242
*/
4343
private static final int CACHE_SIZE = 150;
4444

utils/src/test/java/software/amazon/awssdk/utils/SdkUriTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ void resetCache() throws IllegalAccessException {
4141
cacheField.setAccessible(true);
4242
cacheField.set(SdkUri.getInstance(), BoundedCache.builder(UriConstructorArgs::newInstance)
4343
.maxSize(100)
44+
.evictionBatchSize(5)
4445
.build());
4546
}
4647

0 commit comments

Comments
 (0)