Skip to content

Commit a773ba1

Browse files
authored
Add LRU Cache to utilities (#3466)
* Add LRU Cache to utilities * Updated LruCache with better synchronization and pointer handling * Updating cache tests * Adding evict marker to cache entry and rewriting concurrency test
1 parent fa1ac02 commit a773ba1

File tree

3 files changed

+513
-0
lines changed

3 files changed

+513
-0
lines changed

utils/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@
7979
<artifactId>mockito-core</artifactId>
8080
<scope>test</scope>
8181
</dependency>
82+
<dependency>
83+
<groupId>org.mockito</groupId>
84+
<artifactId>mockito-junit-jupiter</artifactId>
85+
<scope>test</scope>
86+
</dependency>
8287
<dependency>
8388
<groupId>org.assertj</groupId>
8489
<artifactId>assertj-core</artifactId>
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.utils.cache.lru;
17+
18+
import java.util.Map;
19+
import java.util.Objects;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
import java.util.function.Function;
22+
import software.amazon.awssdk.annotations.SdkProtectedApi;
23+
import software.amazon.awssdk.annotations.ThreadSafe;
24+
import software.amazon.awssdk.utils.Validate;
25+
26+
/**
27+
* A thread-safe LRU (Least Recently Used) 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.
30+
* <p>
31+
* When the cache is full, a new value will push out the least recently used value.
32+
* When the cache is queried for an already stored value (cache hit), this value is moved to the back of the queue
33+
* before it's returned so that the order of most recently used to least recently used can be maintained.
34+
* <p>
35+
* The user can configure the maximum size of the cache, which is set to a default of 100.
36+
* <p>
37+
* Null values are accepted.
38+
*/
39+
@SdkProtectedApi
40+
@ThreadSafe
41+
public final class LruCache<K, V> {
42+
43+
private static final int DEFAULT_SIZE = 100;
44+
45+
private final Map<K, CacheEntry<K, V>> cache;
46+
private final Function<K, V> valueSupplier;
47+
private final Object listLock = new Object();
48+
private final int maxCacheSize;
49+
50+
private CacheEntry<K, V> leastRecentlyUsed = null;
51+
private CacheEntry<K, V> mostRecentlyUsed = null;
52+
53+
private LruCache(Builder<K, V> b) {
54+
this.valueSupplier = b.supplier;
55+
Integer customSize = Validate.isPositiveOrNull(b.maxSize, "size");
56+
this.maxCacheSize = customSize != null ? customSize : DEFAULT_SIZE;
57+
this.cache = new ConcurrentHashMap<>();
58+
}
59+
60+
/**
61+
* Get a value based on the key. If the value exists in the cache, it's returned, and it's position in the cache is updated.
62+
* Otherwise, the value is calculated based on the supplied function {@link Builder#builder(Function)}.
63+
*/
64+
public V get(K key) {
65+
while (true) {
66+
CacheEntry<K, V> cachedEntry = cache.computeIfAbsent(key, this::newEntry);
67+
synchronized (listLock) {
68+
if (cachedEntry.evicted()) {
69+
continue;
70+
}
71+
moveToBackOfQueue(cachedEntry);
72+
return cachedEntry.value();
73+
}
74+
}
75+
}
76+
77+
private CacheEntry<K, V> newEntry(K key) {
78+
V value = valueSupplier.apply(key);
79+
return new CacheEntry<>(key, value);
80+
}
81+
82+
/**
83+
* Moves an entry to the back of the queue and sets it as the most recently used. If the entry is already the
84+
* most recently used, do nothing.
85+
* <p>
86+
* Summary of cache update:
87+
* <ol>
88+
* <li>Detach the entry from its current place in the double linked list.</li>
89+
* <li>Add it to the back of the queue (most recently used)</li>
90+
*</ol>
91+
*/
92+
private void moveToBackOfQueue(CacheEntry<K, V> entry) {
93+
if (entry.equals(mostRecentlyUsed)) {
94+
return;
95+
}
96+
removeFromQueue(entry);
97+
addToQueue(entry);
98+
}
99+
100+
/**
101+
* Detaches an entry from its neighbors in the cache. Remove the entry from its current place in the double linked list
102+
* by letting its previous neighbor point to its next neighbor, and vice versa, if those exist.
103+
* <p>
104+
* The least-recently-used and most-recently-used pointers are reset if needed.
105+
* <p>
106+
* <b>Note:</b> Detaching an entry does not delete it from the cache hash map.
107+
*/
108+
private void removeFromQueue(CacheEntry<K, V> entry) {
109+
CacheEntry<K, V> previousEntry = entry.previous();
110+
if (previousEntry != null) {
111+
previousEntry.setNext(entry.next());
112+
}
113+
CacheEntry<K, V> nextEntry = entry.next();
114+
if (nextEntry != null) {
115+
nextEntry.setPrevious(entry.previous());
116+
}
117+
if (entry.equals(leastRecentlyUsed)) {
118+
leastRecentlyUsed = entry.previous();
119+
}
120+
if (entry.equals(mostRecentlyUsed)) {
121+
mostRecentlyUsed = entry.next();
122+
}
123+
}
124+
125+
/**
126+
* Adds an entry to the queue as the most recently used, adjusts all pointers and triggers an evict
127+
* event if the cache is now full.
128+
*/
129+
private void addToQueue(CacheEntry<K, V> entry) {
130+
if (mostRecentlyUsed != null) {
131+
mostRecentlyUsed.setPrevious(entry);
132+
entry.setNext(mostRecentlyUsed);
133+
}
134+
entry.setPrevious(null);
135+
mostRecentlyUsed = entry;
136+
if (leastRecentlyUsed == null) {
137+
leastRecentlyUsed = entry;
138+
}
139+
if (size() > maxCacheSize) {
140+
evict();
141+
}
142+
}
143+
144+
/**
145+
* Removes the least recently used entry from the cache, marks it as evicted and removes it from the queue.
146+
*/
147+
private void evict() {
148+
leastRecentlyUsed.isEvicted(true);
149+
cache.remove(leastRecentlyUsed.key());
150+
removeFromQueue(leastRecentlyUsed);
151+
}
152+
153+
public int size() {
154+
return cache.size();
155+
}
156+
157+
public static <K, V> LruCache.Builder<K, V> builder(Function<K, V> supplier) {
158+
return new Builder<>(supplier);
159+
}
160+
161+
public static final class Builder<K, V> {
162+
163+
private final Function<K, V> supplier;
164+
private Integer maxSize;
165+
166+
private Builder(Function<K, V> supplier) {
167+
this.supplier = supplier;
168+
}
169+
170+
public Builder<K, V> maxSize(Integer maxSize) {
171+
this.maxSize = maxSize;
172+
return this;
173+
}
174+
175+
public LruCache<K, V> build() {
176+
return new LruCache<>(this);
177+
}
178+
}
179+
180+
private static final class CacheEntry<K, V> {
181+
182+
private final K key;
183+
private final V value;
184+
185+
private boolean evicted = false;
186+
187+
private CacheEntry<K, V> previous;
188+
private CacheEntry<K, V> next;
189+
190+
private CacheEntry(K key, V value) {
191+
this.key = key;
192+
this.value = value;
193+
}
194+
195+
K key() {
196+
return key;
197+
}
198+
199+
V value() {
200+
return value;
201+
}
202+
203+
boolean evicted() {
204+
return evicted;
205+
}
206+
207+
void isEvicted(boolean evicted) {
208+
this.evicted = evicted;
209+
}
210+
211+
CacheEntry<K, V> next() {
212+
return next;
213+
}
214+
215+
void setNext(CacheEntry<K, V> next) {
216+
this.next = next;
217+
}
218+
219+
CacheEntry<K, V> previous() {
220+
return previous;
221+
}
222+
223+
void setPrevious(CacheEntry<K, V> previous) {
224+
this.previous = previous;
225+
}
226+
227+
@Override
228+
@SuppressWarnings("unchecked")
229+
public boolean equals(Object o) {
230+
if (this == o) {
231+
return true;
232+
}
233+
if ((o == null) || getClass() != o.getClass()) {
234+
return false;
235+
}
236+
CacheEntry<?, ?> that = (CacheEntry<?, ?>) o;
237+
return Objects.equals(key, that.key)
238+
&& Objects.equals(value, that.value);
239+
}
240+
241+
@Override
242+
public int hashCode() {
243+
int result = key != null ? key.hashCode() : 0;
244+
result = 31 * result + (value != null ? value.hashCode() : 0);
245+
return result;
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)