Skip to content

Commit fb06ca7

Browse files
authored
feat: Added a generic LRUCache interface and a default implementation (#482)
## Summary Added an interface for a generic LRUCache and a default Implementation. ## Test plan - All existing tests pass. - Added unit tests for the default cache implementation. ## Issues [OASIS-8385](https://optimizely.atlassian.net/browse/OASIS-8385)
1 parent 618377d commit fb06ca7

File tree

3 files changed

+293
-0
lines changed

3 files changed

+293
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
*
3+
* Copyright 2022, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.internal;
18+
19+
public interface Cache<T> {
20+
int DEFAULT_MAX_SIZE = 10000;
21+
int DEFAULT_TIMEOUT_SECONDS = 600;
22+
void save(String key, T value);
23+
T lookup(String key);
24+
void reset();
25+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
*
3+
* Copyright 2022, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.internal;
18+
19+
import com.optimizely.ab.annotations.VisibleForTesting;
20+
21+
import java.util.*;
22+
23+
public class DefaultLRUCache<T> implements Cache<T> {
24+
25+
private final Object lock = new Object();
26+
27+
private final Integer maxSize;
28+
29+
private final Long timeoutMillis;
30+
31+
@VisibleForTesting
32+
final LinkedHashMap<String, CacheEntity> linkedHashMap = new LinkedHashMap<String, CacheEntity>(16, 0.75f, true) {
33+
@Override
34+
protected boolean removeEldestEntry(Map.Entry<String, CacheEntity> eldest) {
35+
return this.size() > maxSize;
36+
}
37+
};
38+
39+
public DefaultLRUCache() {
40+
this(DEFAULT_MAX_SIZE, DEFAULT_TIMEOUT_SECONDS);
41+
}
42+
43+
public DefaultLRUCache(Integer maxSize, Integer timeoutSeconds) {
44+
this.maxSize = maxSize < 0 ? Integer.valueOf(0) : maxSize;
45+
this.timeoutMillis = (timeoutSeconds < 0) ? 0 : (timeoutSeconds * 1000L);
46+
}
47+
48+
public void save(String key, T value) {
49+
if (maxSize == 0) {
50+
// Cache is disabled when maxSize = 0
51+
return;
52+
}
53+
54+
synchronized (lock) {
55+
linkedHashMap.put(key, new CacheEntity(value));
56+
}
57+
}
58+
59+
public T lookup(String key) {
60+
if (maxSize == 0) {
61+
// Cache is disabled when maxSize = 0
62+
return null;
63+
}
64+
65+
synchronized (lock) {
66+
if (linkedHashMap.containsKey(key)) {
67+
CacheEntity entity = linkedHashMap.get(key);
68+
Long nowMs = new Date().getTime();
69+
70+
// ttl = 0 means entities never expire.
71+
if (timeoutMillis == 0 || (nowMs - entity.timestamp < timeoutMillis)) {
72+
return entity.value;
73+
}
74+
75+
linkedHashMap.remove(key);
76+
}
77+
return null;
78+
}
79+
}
80+
81+
public void reset() {
82+
synchronized (lock) {
83+
linkedHashMap.clear();
84+
}
85+
}
86+
87+
private class CacheEntity {
88+
public T value;
89+
public Long timestamp;
90+
91+
public CacheEntity(T value) {
92+
this.value = value;
93+
this.timestamp = new Date().getTime();
94+
}
95+
}
96+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
*
3+
* Copyright 2022, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.internal;
18+
19+
import org.junit.Test;
20+
21+
import java.util.Arrays;
22+
import java.util.List;
23+
24+
import static org.junit.Assert.*;
25+
26+
public class DefaultLRUCacheTest {
27+
28+
@Test
29+
public void createSaveAndLookupOneItem() {
30+
Cache<String> cache = new DefaultLRUCache<>();
31+
assertNull(cache.lookup("key1"));
32+
cache.save("key1", "value1");
33+
assertEquals("value1", cache.lookup("key1"));
34+
}
35+
36+
@Test
37+
public void saveAndLookupMultipleItems() {
38+
DefaultLRUCache<List<String>> cache = new DefaultLRUCache<>();
39+
40+
cache.save("user1", Arrays.asList("segment1", "segment2"));
41+
cache.save("user2", Arrays.asList("segment3", "segment4"));
42+
cache.save("user3", Arrays.asList("segment5", "segment6"));
43+
44+
String[] itemKeys = cache.linkedHashMap.keySet().toArray(new String[0]);
45+
assertEquals("user1", itemKeys[0]);
46+
assertEquals("user2", itemKeys[1]);
47+
assertEquals("user3", itemKeys[2]);
48+
49+
assertEquals(Arrays.asList("segment1", "segment2"), cache.lookup("user1"));
50+
51+
itemKeys = cache.linkedHashMap.keySet().toArray(new String[0]);
52+
// Lookup should move user1 to bottom of the list and push up others.
53+
assertEquals("user2", itemKeys[0]);
54+
assertEquals("user3", itemKeys[1]);
55+
assertEquals("user1", itemKeys[2]);
56+
57+
assertEquals(Arrays.asList("segment3", "segment4"), cache.lookup("user2"));
58+
59+
itemKeys = cache.linkedHashMap.keySet().toArray(new String[0]);
60+
// Lookup should move user2 to bottom of the list and push up others.
61+
assertEquals("user3", itemKeys[0]);
62+
assertEquals("user1", itemKeys[1]);
63+
assertEquals("user2", itemKeys[2]);
64+
65+
assertEquals(Arrays.asList("segment5", "segment6"), cache.lookup("user3"));
66+
67+
itemKeys = cache.linkedHashMap.keySet().toArray(new String[0]);
68+
// Lookup should move user3 to bottom of the list and push up others.
69+
assertEquals("user1", itemKeys[0]);
70+
assertEquals("user2", itemKeys[1]);
71+
assertEquals("user3", itemKeys[2]);
72+
}
73+
74+
@Test
75+
public void saveShouldReorderList() {
76+
DefaultLRUCache<List<String>> cache = new DefaultLRUCache<>();
77+
78+
cache.save("user1", Arrays.asList("segment1", "segment2"));
79+
cache.save("user2", Arrays.asList("segment3", "segment4"));
80+
cache.save("user3", Arrays.asList("segment5", "segment6"));
81+
82+
String[] itemKeys = cache.linkedHashMap.keySet().toArray(new String[0]);
83+
assertEquals("user1", itemKeys[0]);
84+
assertEquals("user2", itemKeys[1]);
85+
assertEquals("user3", itemKeys[2]);
86+
87+
cache.save("user1", Arrays.asList("segment1", "segment2"));
88+
89+
itemKeys = cache.linkedHashMap.keySet().toArray(new String[0]);
90+
// save should move user1 to bottom of the list and push up others.
91+
assertEquals("user2", itemKeys[0]);
92+
assertEquals("user3", itemKeys[1]);
93+
assertEquals("user1", itemKeys[2]);
94+
95+
cache.save("user2", Arrays.asList("segment3", "segment4"));
96+
97+
itemKeys = cache.linkedHashMap.keySet().toArray(new String[0]);
98+
// save should move user2 to bottom of the list and push up others.
99+
assertEquals("user3", itemKeys[0]);
100+
assertEquals("user1", itemKeys[1]);
101+
assertEquals("user2", itemKeys[2]);
102+
103+
cache.save("user3", Arrays.asList("segment5", "segment6"));
104+
105+
itemKeys = cache.linkedHashMap.keySet().toArray(new String[0]);
106+
// save should move user3 to bottom of the list and push up others.
107+
assertEquals("user1", itemKeys[0]);
108+
assertEquals("user2", itemKeys[1]);
109+
assertEquals("user3", itemKeys[2]);
110+
}
111+
112+
@Test
113+
public void whenCacheIsDisabled() {
114+
DefaultLRUCache<List<String>> cache = new DefaultLRUCache<>(0,Cache.DEFAULT_TIMEOUT_SECONDS);
115+
116+
cache.save("user1", Arrays.asList("segment1", "segment2"));
117+
cache.save("user2", Arrays.asList("segment3", "segment4"));
118+
cache.save("user3", Arrays.asList("segment5", "segment6"));
119+
120+
assertNull(cache.lookup("user1"));
121+
assertNull(cache.lookup("user2"));
122+
assertNull(cache.lookup("user3"));
123+
}
124+
125+
@Test
126+
public void whenItemsExpire() throws InterruptedException {
127+
DefaultLRUCache<List<String>> cache = new DefaultLRUCache<>(Cache.DEFAULT_MAX_SIZE, 1);
128+
cache.save("user1", Arrays.asList("segment1", "segment2"));
129+
assertEquals(Arrays.asList("segment1", "segment2"), cache.lookup("user1"));
130+
assertEquals(1, cache.linkedHashMap.size());
131+
Thread.sleep(1000);
132+
assertNull(cache.lookup("user1"));
133+
assertEquals(0, cache.linkedHashMap.size());
134+
}
135+
136+
@Test
137+
public void whenCacheReachesMaxSize() {
138+
DefaultLRUCache<List<String>> cache = new DefaultLRUCache<>(2, Cache.DEFAULT_TIMEOUT_SECONDS);
139+
140+
cache.save("user1", Arrays.asList("segment1", "segment2"));
141+
cache.save("user2", Arrays.asList("segment3", "segment4"));
142+
cache.save("user3", Arrays.asList("segment5", "segment6"));
143+
144+
assertEquals(2, cache.linkedHashMap.size());
145+
146+
assertEquals(Arrays.asList("segment5", "segment6"), cache.lookup("user3"));
147+
assertEquals(Arrays.asList("segment3", "segment4"), cache.lookup("user2"));
148+
assertNull(cache.lookup("user1"));
149+
}
150+
151+
@Test
152+
public void whenCacheIsReset() {
153+
DefaultLRUCache<List<String>> cache = new DefaultLRUCache<>();
154+
cache.save("user1", Arrays.asList("segment1", "segment2"));
155+
cache.save("user2", Arrays.asList("segment3", "segment4"));
156+
cache.save("user3", Arrays.asList("segment5", "segment6"));
157+
158+
assertEquals(Arrays.asList("segment1", "segment2"), cache.lookup("user1"));
159+
assertEquals(Arrays.asList("segment3", "segment4"), cache.lookup("user2"));
160+
assertEquals(Arrays.asList("segment5", "segment6"), cache.lookup("user3"));
161+
162+
assertEquals(3, cache.linkedHashMap.size());
163+
164+
cache.reset();
165+
166+
assertNull(cache.lookup("user1"));
167+
assertNull(cache.lookup("user2"));
168+
assertNull(cache.lookup("user3"));
169+
170+
assertEquals(0, cache.linkedHashMap.size());
171+
}
172+
}

0 commit comments

Comments
 (0)