001: package ri.cache;
002:
003: import ri.cache.eviction.EvictionStrategy;
004: import ri.cache.eviction.LRUChainEvictionStrategy;
005: import ri.cache.loader.NullCacheLoader;
006:
007: import javax.cache.Cache;
008: import javax.cache.CacheEntry;
009: import javax.cache.CacheException;
010: import javax.cache.CacheStatistics;
011: import javax.cache.spi.CacheLoader;
012: import java.util.*;
013: import java.util.concurrent.ConcurrentHashMap;
014: import java.util.concurrent.ConcurrentMap;
015: import java.util.concurrent.ExecutionException;
016: import java.util.concurrent.Future;
017:
018: /**
019: * BasicCache
020: *
021: * @author Brian Goetz
022: */
023: public class ReferenceCache<K, V> extends AbstractCache<K, V> implements
024: Cache<K, V>, AsyncLoader.AsyncLoaderCallback<K, V> {
025:
026: private static final long DEFAULT_TTL = -1;
027: private static final int DEFAULT_CAPACITY = 100;
028: private static final long DEFAULT_LOAD_TIMEOUT = 5000;
029:
030: protected final CacheLoader<K, V> loader;
031: protected final EvictionStrategy<K, V> evictionStrategy;
032: protected final ConcurrentMap<K, CacheEntry<K, V>> map = new ConcurrentHashMap<K, CacheEntry<K, V>>();
033: protected final long timeToLive, loadTimeout;
034: protected final int capacity;
035: protected final ReferenceStatistics statistics = new ReferenceStatistics();
036: protected final AsyncLoader<K, V> warmer;
037: protected final Set<Map.Entry<K, V>> entrySet;
038:
039: public ReferenceCache(String name) {
040: this (name, DEFAULT_CAPACITY);
041: }
042:
043: public ReferenceCache(String name, int capacity) {
044: this (name, capacity, DEFAULT_TTL, DEFAULT_LOAD_TIMEOUT);
045: }
046:
047: public ReferenceCache(String name, CacheLoader<K, V> loader) {
048: this (name, DEFAULT_CAPACITY, DEFAULT_TTL, DEFAULT_LOAD_TIMEOUT,
049: new LRUChainEvictionStrategy<K, V>(), loader);
050: }
051:
052: public ReferenceCache(String name, int capacity, long timeToLive,
053: long loadTimeout) {
054: this (name, capacity, timeToLive, loadTimeout,
055: new LRUChainEvictionStrategy<K, V>(),
056: new NullCacheLoader<K, V>());
057: }
058:
059: public ReferenceCache(String name,
060: EvictionStrategy<K, V> evictionStrategy,
061: CacheLoader<K, V> loader) {
062: this (name, DEFAULT_CAPACITY, DEFAULT_TTL, DEFAULT_LOAD_TIMEOUT,
063: evictionStrategy, loader);
064: }
065:
066: public ReferenceCache(String name, int capacity, long timeToLive,
067: long loadTimeout, EvictionStrategy<K, V> evictionStrategy,
068: CacheLoader<K, V> loader) {
069: super (name);
070: this .loader = loader;
071: this .evictionStrategy = evictionStrategy;
072: this .timeToLive = timeToLive;
073: this .loadTimeout = loadTimeout;
074: this .capacity = capacity;
075: warmer = new AsyncLoader<K, V>(loader, loadTimeout);
076: entrySet = new EntrySet();
077: }
078:
079: private static boolean isValid(CacheEntry ce) {
080: return ce != null && ce.isValid();
081: }
082:
083: private void filterExpiredValues() {
084: // This can, and should, be optimized substantially, because it gets called a lot
085: for (Iterator<CacheEntry<K, V>> iter = map.values().iterator(); iter
086: .hasNext();) {
087: CacheEntry<K, V> entry = iter.next();
088: if (!entry.isValid()) {
089: iter.remove();
090: evictionStrategy.discardEntry(entry);
091: listeners.onExpiry(entry.getKey());
092: }
093: }
094: }
095:
096: // Don't forget to notify either onLoad or onPut listeners after calling doPut
097: protected V doPut(K key, V value, long timeToLive) {
098: if (map.size() >= capacity)
099: evict();
100: CacheEntry<K, V> ce = evictionStrategy.createEntry(key, value,
101: timeToLive);
102: CacheEntry<K, V> oldEntry = map.put(key, ce);
103: if (oldEntry != null) {
104: evictionStrategy.discardEntry(oldEntry);
105: listeners.onUpdate(key);
106: }
107: if (isValid(oldEntry))
108: return oldEntry.getValue();
109: return null;
110: }
111:
112: public Set<Map.Entry<K, V>> entrySet() {
113: return entrySet;
114: }
115:
116: private CacheEntry<K, V> getCacheEntry(K key, boolean updateStats) {
117: CacheEntry<K, V> ce = map.get(key);
118:
119: if (isValid(ce)) {
120: if (updateStats) {
121: evictionStrategy.touchEntry(ce);
122: statistics.incrementHits();
123: }
124: return ce;
125: } else {
126: if (ce != null) {
127: evictionStrategy.discardEntry(ce);
128: listeners.onExpiry(key);
129: }
130: if (updateStats)
131: statistics.incrementMisses();
132: return null;
133: }
134: }
135:
136: public CacheEntry<K, V> getCacheEntry(K key) {
137: return getCacheEntry(key, true);
138: }
139:
140: public V peek(K key) {
141: CacheEntry<K, V> ce = getCacheEntry(key);
142: return ce != null ? ce.getValue() : null;
143: }
144:
145: @SuppressWarnings("unchecked")
146: public V get(Object key) {
147: CacheEntry<K, V> ce = getCacheEntry((K) key);
148: if (ce != null)
149: return ce.getValue();
150:
151: Future<V> future = warmer.load((K) key);
152: return doLoadComplete((K) key, future);
153: }
154:
155: public Map<K, V> getAll(Collection<? extends K> keys)
156: throws CacheException {
157: Map<K, V> results = new HashMap<K, V>(keys.size());
158: List<K> toLoad = new ArrayList<K>();
159:
160: for (K key : keys) {
161: CacheEntry<K, V> ce = getCacheEntry(key);
162: if (isValid(ce))
163: results.put(ce.getKey(), ce.getValue());
164: else
165: toLoad.add(key);
166: }
167:
168: if (toLoad.size() > 0) {
169: Map<K, Future<V>> m = warmer.loadAll(toLoad);
170: for (K key : toLoad) {
171: V result = doLoadComplete(key, m.get(key));
172: results.put(key, result);
173: }
174: }
175:
176: return results;
177: }
178:
179: // Potentially blocks until the result is ready
180: private V doLoadComplete(K key, Future<V> future) {
181: try {
182: V result = future.get();
183: doPut(key, result, timeToLive);
184: listeners.onLoad(key);
185: return result;
186: } catch (ExecutionException e) {
187: Throwable t = e.getCause();
188: Exception underlying = (t instanceof Exception) ? (Exception) t
189: : new RuntimeException(t);
190: listeners.onLoadException((K) key, underlying);
191: return null;
192: } catch (Exception e) {
193: // Subsumes runtime exceptions
194: listeners.onLoadException((K) key, e);
195: return null;
196: }
197: }
198:
199: // Theoretically could block until the result is ready, but guaranteed to not be called until task is done
200: public void loadComplete(K key, Future<V> future) {
201: doLoadComplete(key, future);
202: }
203:
204: public void load(K key) throws CacheException {
205: warmer.load(key, this );
206: }
207:
208: public void loadAll(Collection<? extends K> keys)
209: throws CacheException {
210: warmer.loadAll(keys);
211: }
212:
213: @SuppressWarnings("unchecked")
214: public V remove(Object key) {
215: CacheEntry<K, V> ce = map.remove(key);
216: if (ce != null) {
217: evictionStrategy.discardEntry(ce);
218: if (isValid(ce)) {
219: listeners.onRemove((K) key);
220: return ce.getValue();
221: }
222: }
223: return null;
224: }
225:
226: public V put(K key, V value) {
227: V oldValue = doPut(key, value, timeToLive);
228: listeners.onPut(key);
229: return oldValue;
230: }
231:
232: public V put(K key, V value, long timeToLive) {
233: V oldValue = doPut(key, value, timeToLive);
234: listeners.onPut(key);
235: return oldValue;
236: }
237:
238: @SuppressWarnings("unchecked")
239: public boolean containsKey(Object key) {
240: CacheEntry ce = getCacheEntry((K) key, false);
241: return ce != null;
242: }
243:
244: public void evict() {
245: Map<K, V> m = evictionStrategy.evict(this );
246: for (K key : m.keySet()) {
247: CacheEntry<K, V> ce = map.remove(key);
248: if (ce != null)
249: listeners.onEvict(key);
250: }
251: }
252:
253: public int size() {
254: filterExpiredValues();
255: return map.size();
256: }
257:
258: public void clear() {
259: map.clear();
260: statistics.clearStatistics();
261: evictionStrategy.clear();
262: listeners.onClear();
263: }
264:
265: public CacheStatistics getStatistics() {
266: return statistics;
267: }
268:
269: public void shutdown() {
270: warmer.shutdown();
271: }
272:
273: public V putIfAbsent(K key, V value) {
274: CacheEntry<K, V> ce = getCacheEntry(key, false);
275: if (isValid(ce))
276: return ce.getValue();
277:
278: ce = evictionStrategy.createEntry(key, value, timeToLive);
279: if (map.size() >= capacity)
280: evict();
281: CacheEntry<K, V> oldEntry = map.putIfAbsent(key, ce);
282: if (!isValid(oldEntry)) {
283: listeners.onPut(key);
284: return null;
285: } else
286: return oldEntry.getValue();
287: }
288:
289: @SuppressWarnings("unchecked")
290: public boolean remove(Object key, Object value) {
291: CacheEntry<K, V> ce = getCacheEntry((K) key, false);
292: if (!isValid(ce))
293: return false;
294: boolean removed = map.remove(key, ce);
295: if (removed) {
296: listeners.onRemove((K) key);
297: evictionStrategy.discardEntry(ce);
298: }
299: return removed;
300: }
301:
302: public V replace(K key, V value) {
303: CacheEntry<K, V> ce = getCacheEntry((K) key, false);
304: if (!isValid(ce))
305: return null;
306: ce = evictionStrategy.createEntry(key, value, timeToLive);
307: CacheEntry<K, V> oldEntry = map.replace(key, ce);
308: if (oldEntry == null)
309: return null;
310: listeners.onPut(key);
311: evictionStrategy.discardEntry(oldEntry);
312: return oldEntry.getValue();
313: }
314:
315: public boolean replace(K key, V value, V newValue) {
316: CacheEntry<K, V> ce = getCacheEntry((K) key, false);
317: if (!isValid(ce))
318: return false;
319: CacheEntry<K, V> newCe = evictionStrategy.createEntry(key,
320: value, timeToLive);
321: boolean replaced = map.replace(key, ce, newCe);
322: if (replaced)
323: listeners.onPut(key);
324: return replaced;
325: }
326:
327: public void clearStatistics() {
328: statistics.clearStatistics();
329: }
330:
331: public CacheStatistics getCacheStatistics() {
332: return statistics;
333: }
334:
335: private class EntrySet extends AbstractSet<Entry<K, V>> {
336: public Iterator<Entry<K, V>> iterator() {
337: filterExpiredValues();
338: return new Iterator<Entry<K, V>>() {
339: private final Iterator<CacheEntry<K, V>> iter = map
340: .values().iterator();
341:
342: public boolean hasNext() {
343: return iter.hasNext();
344: }
345:
346: public Entry<K, V> next() {
347: return iter.next();
348: }
349:
350: public void remove() {
351: iter.remove();
352: }
353: };
354: }
355:
356: @SuppressWarnings("unchecked")
357: public boolean contains(Object o) {
358: if (!(o instanceof CacheEntry))
359: return false;
360: Entry<K, V> entry = (Entry<K, V>) o;
361: CacheEntry<K, V> mapEntry = getCacheEntry(entry.getKey(),
362: false);
363: return (mapEntry != null && mapEntry.equals(entry));
364: }
365:
366: public boolean remove(Object o) {
367: if (!(o instanceof Entry))
368: return false;
369: Entry entry = (Entry) o;
370: Object old = ReferenceCache.this .remove(entry.getKey(),
371: entry.getValue());
372: return old != null;
373: }
374:
375: public int size() {
376: return ReferenceCache.this .size();
377: }
378:
379: public void clear() {
380: ReferenceCache.this.clear();
381: }
382: }
383: }
|