ShinyCache.java
/*******************************************************************************
* Copyright (c) 2004, 2013 Steve Flasby
* All rights reserved.
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* <ul>
* <li>Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.</li>
* <li>Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.</li>
* </ul>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package org.flasby.util;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* is a cache which handles entry expiry and will ensure that multiple simultaneous requests
* using the same key will only make a single request to get the missing value.
* Simultaneous requests will be queued until the value is returned.
*
* Cached entries may have a NULL value.
*
* @author Steve Flasby
* 21 Sep 2012
*/
public abstract class ShinyCache<Key,Value> implements BlockingCache<Key, Value>{
protected static class ItemHolder<VV> {
VV value;
long createdTimestamp = System.nanoTime()/1000000;
final Semaphore sema = new Semaphore(0, true);
}
protected static class ExpiryPolicy {
protected boolean hasExpired(ItemHolder<?> holder) {
return false;
}
}
private static final class NeverExpires extends ExpiryPolicy {
public NeverExpires() {
}}
private static final class ExpiresAfter extends ExpiryPolicy {
private final long expiresAfter;
ExpiresAfter(long expiresAfter) {
this.expiresAfter = expiresAfter;
}
@Override
protected boolean hasExpired(ItemHolder<?> holder) {
return (holder.createdTimestamp+expiresAfter <= System.nanoTime()/1000000);
}
}
private static final NeverExpires NEVER = new NeverExpires();
public static ExpiryPolicy never() {
return NEVER;
}
public static ExpiryPolicy expireAfterMillis(long periodInMillis) {
return new ExpiresAfter(periodInMillis);
}
public enum EvictionPolicy { LRU, OLDEST }
final LinkedHashMap<Key, ItemHolder<Value>> cache;
private final Semaphore entry = new Semaphore(1, true);
private final ExpiryPolicy expiryPolicy;
/**
* simple, ever growing, cache
*/
public ShinyCache() {
this(never());
}
/**
* cache which expires entries after a fixed lifetime.
*
* @param expiryPolicy
*/
public ShinyCache(ExpiryPolicy expiryPolicy) {
this(expiryPolicy, 0, EvictionPolicy.OLDEST);
}
public ShinyCache(int capacity, EvictionPolicy evictionPolicy) {
this( never(), capacity, evictionPolicy);
}
public ShinyCache(final ExpiryPolicy expiryPolicy, final int capacity, final EvictionPolicy evictionPolicy) {
this.expiryPolicy = expiryPolicy;
cache = new LinkedHashMap<Key, ItemHolder<Value>>( 16, 0.75f, evictionPolicy==EvictionPolicy.LRU){
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry( Map.Entry<Key, ItemHolder<Value>> eldest) {
return ( capacity != 0 && cache.size()>capacity );
}
};
}
/**
* gets the key form the cache or returns NULL if the Thread was interrupted.
*/
@Override
public Value get(Key key) {
try {
return getAndWait(key);
} catch (InterruptedException e) {
return null;
}
}
@Override
public Value getAndWait(Key key) throws InterruptedException {
return getAndWait(key, Integer.MAX_VALUE);
}
@Override
public Value getAndWait(Key key, long maxWaitMillis) throws InterruptedException {
long t = System.nanoTime();
if ( ! entry.tryAcquire(maxWaitMillis, TimeUnit.MILLISECONDS) ) {
return null;
}
// See if weve already waited too long & calculate how much longer to wait
t = maxWaitMillis - (System.nanoTime()-t)/1000000;
if ( t<=0 ) return null;
ItemHolder<Value> holder = getCache().get(key);
if ( holder != null ) {
boolean acquired = false;
try {
acquired = holder.sema.tryAcquire(t, TimeUnit.MILLISECONDS);
if ( expiryPolicy.hasExpired(holder) ) {
// Re-acquire the value
holder.value = getValueFor(key);
holder.createdTimestamp = System.nanoTime()/1000000;
}
return holder.value;
} finally {
entry.release();
if ( acquired ) {
holder.sema.release();
}
}
}
holder = new ItemHolder<Value>();
getCache().put(key, holder );
holder.value = getValueFor(key);
holder.sema.release();
entry.release();
return holder.value;
}
@Override
public int size() {
return getCache().size();
}
@Override
public Iterator<Value> iterator() {
return new Iterator<Value>() {
private final Iterator<ItemHolder<Value>> iter = getCache().values().iterator();
@Override
public boolean hasNext() {
return iter.hasNext();
}
@Override
public Value next() {
return iter.next().value;
}
@Override
public void remove() {
iter.remove();
}
};
}
protected abstract Value getValueFor(Key key);
LinkedHashMap<Key, ItemHolder<Value>> getCache() {
return cache;
}
}