Skip to content

Commit e5475ca

Browse files
author
rafwin
committed
Fixed bug when loading entries via a Callable was overseen when a CacheLoader was used. Added further tests to cover this bug for the future.
1 parent 96d6651 commit e5475ca

12 files changed

+674
-31
lines changed

README

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,19 @@
11
A Guava cache extension that allows caches to persist cache entries when they cannot longer be stored in memory.
2-
An implementation that overflows to the file system is provided.
2+
An implementation that overflows to the file system is provided including a corresponding CacheBuilder with similar semantics than the Guava CacheBuilder.
3+
4+
For creating a cache that overflows to disk, just proceed as when using the Guava CacheBuilder:
5+
6+
Cache<String, String> stringCache =
7+
FileSystemCacheBuilder.newBuilder()
8+
.maximumSize(100L)
9+
.softValues()
10+
.build();
11+
12+
Note: This cache implementation has slightly different semantics than the Cache / LoadingCache interface contracts specify:
13+
* Any limits set for this cache do only concern the cache's memory size. Cache entries exceeding this limit will overflow to disk.
14+
* When calling the non-argument invalidateAll() method, the RemovalListener is only informed about the expiration of entries that
15+
are still stored in memory.
16+
* When the cache is not longer in use, its invalidateAll() method should be called if the cache's overflow folder is not cleared by
17+
the operating system.
18+
19+
Licensed under the Apache Software License, Version 2.0

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
<url>http://www.kantega.no</url>
1616
</organization>
1717

18-
<name>Limber web framework</name>
19-
<description>A web framework based on DOM manipulation.</description>
18+
<name>Guava cache overflow extension</name>
19+
<description>An extension to Guava caches that allows cache entries to overflow to disk.</description>
2020
<url>http://www.kantega.no</url>
2121

2222
<properties>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package no.kantega.lab.guava.cache;
2+
3+
import com.google.common.cache.CacheBuilder;
4+
import com.google.common.cache.CacheLoader;
5+
import com.google.common.cache.LoadingCache;
6+
import com.google.common.cache.RemovalListener;
7+
import com.google.common.collect.ImmutableMap;
8+
9+
import java.util.concurrent.Callable;
10+
import java.util.concurrent.ExecutionException;
11+
12+
public abstract class AbstractLoadingPersistingCache<K, V> extends AbstractPersistingCache<K, V> implements LoadingCache<K, V> {
13+
14+
private final CacheLoader<K, V> cacheLoader;
15+
16+
protected AbstractLoadingPersistingCache(CacheBuilder<Object, Object> cacheBuilder, CacheLoader<K, V> cacheLoader) {
17+
this(cacheBuilder, cacheLoader, null);
18+
}
19+
20+
protected AbstractLoadingPersistingCache(CacheBuilder<Object, Object> cacheBuilder, CacheLoader<K, V> cacheLoader, RemovalListener<K, V> removalListener) {
21+
super(cacheBuilder, removalListener);
22+
this.cacheLoader = cacheLoader;
23+
}
24+
25+
private class ValueLoaderFromCacheLoader implements Callable<V> {
26+
27+
private final K key;
28+
private final CacheLoader<K, V> cacheLoader;
29+
30+
private ValueLoaderFromCacheLoader(CacheLoader<K, V> cacheLoader, K key) {
31+
this.key = key;
32+
this.cacheLoader = cacheLoader;
33+
}
34+
35+
@Override
36+
public V call() throws Exception {
37+
return cacheLoader.load(key);
38+
}
39+
}
40+
41+
@Override
42+
public V get(K key) throws ExecutionException {
43+
return get(key, new ValueLoaderFromCacheLoader(cacheLoader, key));
44+
}
45+
46+
@Override
47+
public V getUnchecked(K key) {
48+
try {
49+
return get(key, new ValueLoaderFromCacheLoader(cacheLoader, key));
50+
} catch (ExecutionException e) {
51+
throw new RuntimeException(e.getCause());
52+
}
53+
}
54+
55+
@Override
56+
public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
57+
ImmutableMap.Builder<K, V> all = ImmutableMap.builder();
58+
for (K key : keys) {
59+
all.put(key, get(key));
60+
}
61+
return all.build();
62+
}
63+
64+
@Override
65+
public V apply(K key) {
66+
try {
67+
return cacheLoader.load(key);
68+
} catch (Exception e) {
69+
throw new RuntimeException(String.format("Could not apply cache on key %s", key), e);
70+
}
71+
}
72+
73+
@Override
74+
public void refresh(K key) {
75+
try {
76+
getUnderlyingCache().put(key, cacheLoader.load(key));
77+
} catch (Exception e) {
78+
throw new RuntimeException(String.format("Could not refresh value for key %s", key), e);
79+
}
80+
}
81+
}

src/main/java/no/kantega/lab/guava/cache/AbstractPersistingCache.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import java.io.IOException;
99
import java.io.InputStream;
1010
import java.io.OutputStream;
11-
import java.util.HashMap;
1211
import java.util.List;
1312
import java.util.Map;
1413
import java.util.concurrent.Callable;
@@ -20,13 +19,21 @@ public abstract class AbstractPersistingCache<K, V> implements Cache<K, V> {
2019
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractPersistingCache.class);
2120

2221
private final LoadingCache<K, V> underlyingCache;
22+
private final RemovalListener<K, V> removalListener;
2323

2424
protected AbstractPersistingCache(CacheBuilder<Object, Object> cacheBuilder) {
25+
this(cacheBuilder, null);
26+
}
27+
28+
protected AbstractPersistingCache(CacheBuilder<Object, Object> cacheBuilder, RemovalListener<K, V> removalListener) {
2529
this.underlyingCache = makeCache(cacheBuilder);
30+
this.removalListener = removalListener;
2631
}
2732

2833
private LoadingCache<K, V> makeCache(CacheBuilder<Object, Object> cacheBuilder) {
29-
return cacheBuilder.removalListener(new PersistingRemovalListener()).build(new PersistedStateCacheLoader());
34+
return cacheBuilder
35+
.removalListener(new PersistingRemovalListener())
36+
.build(new PersistedStateCacheLoader());
3037
}
3138

3239
private class PersistingRemovalListener implements RemovalListener<K, V> {
@@ -39,6 +46,8 @@ public void onRemoval(RemovalNotification<K, V> notification) {
3946
LOGGER.warn(String.format("Could not persist value %s to key %s",
4047
notification.getKey(), notification.getValue()), e);
4148
}
49+
} else if (removalListener != null) {
50+
removalListener.onRemoval(notification);
4251
}
4352
}
4453
}
@@ -76,13 +85,19 @@ private PersistedStateValueLoader(K key, Callable<? extends V> valueLoader) {
7685

7786
@Override
7887
public V call() throws Exception {
79-
V value = load(key);
88+
V value;
89+
try {
90+
value = load(key);
91+
} catch (NotPersistedException e) {
92+
value = null;
93+
}
8094
if (value != null) return value;
8195
return valueLoader.call();
8296
}
8397
}
8498

8599
protected boolean isPersistenceRelevant(RemovalCause removalCause) {
100+
// Note: RemovalCause#wasEvicted is package private
86101
return removalCause != RemovalCause.EXPLICIT
87102
&& removalCause != RemovalCause.REPLACED;
88103
}
@@ -136,12 +151,12 @@ public V get(K key, Callable<? extends V> valueLoader) throws ExecutionException
136151
@Override
137152
@SuppressWarnings("unchecked")
138153
public ImmutableMap<K, V> getAllPresent(Iterable<?> keys) {
139-
Map<K, V> allPresent = new HashMap<K, V>();
154+
ImmutableMap.Builder<K, V> allPresent = ImmutableMap.builder();
140155
for (Object key : keys) {
141156
V value = getIfPresent(key);
142157
if (value != null) allPresent.put((K) key, value);
143158
}
144-
return ImmutableMap.copyOf(allPresent);
159+
return allPresent.build();
145160
}
146161

147162
@Override
@@ -172,9 +187,19 @@ public void invalidateAll(Iterable<?> keys) {
172187
private void invalidatePersisted(Object key) {
173188
try {
174189
K castKey = (K) key;
175-
deletePersistedIfExistent(castKey);
190+
if (removalListener == null) {
191+
deletePersistedIfExistent(castKey);
192+
} else {
193+
V value = findPersisted(castKey);
194+
if (value != null) {
195+
removalListener.onRemoval(RemovalNotifications.make(castKey, value));
196+
deletePersistedIfExistent(castKey);
197+
}
198+
}
176199
} catch (ClassCastException e) {
177200
LOGGER.info(String.format("Could not cast key %s to desired type", key), e);
201+
} catch (IOException e) {
202+
e.printStackTrace();
178203
}
179204
}
180205

0 commit comments

Comments
 (0)