Skip to content

Instantly share code, notes, and snippets.

@rsrini7
Forked from mblanc/AsynchronousCache.java
Created September 24, 2018 10:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rsrini7/ea80d339e2441e4a56be4b5cd513f3bb to your computer and use it in GitHub Desktop.
Save rsrini7/ea80d339e2441e4a56be4b5cd513f3bb to your computer and use it in GitHub Desktop.
Decorator of net.sf.ehcache.Ehcache. This class is an equivalent of an asynchronous net.sf.ehcache.constructs.blocking.SelfPopulatingCache. All cache entry of this cache will be eternal AND will have a timeToLive. When an element is "expired" (time of cr
package ...;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.constructs.blocking.CacheEntryFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Decorator of net.sf.ehcache.Ehcache. This class is an equivalent of an asynchronous
* net.sf.ehcache.constructs.blocking.SelfPopulatingCache. All cache entry of this cache will be eternal AND will have a timeToLive. When a
* element is "expired" (time of creation + time to live < now) the cache will return this "expired" objet but will asynchronously update
* this cache entry by delegating its creation to a net.sf.ehcache.constructs.blocking.CacheEntryFactory if it is not already being created.
*/
public class AsynchronousCache {
private static final Logger LOGGER = LoggerFactory.getLogger(AsynchronousCache.class);
private static final int DEFAULT_TIME_TO_LIVE = 60;
/**
* ExecutorService responsible of asynchronously delegate the creation of a new cache entry and update the cache.
*/
private ExecutorService executor = Executors.newSingleThreadExecutor();
/**
* Map of semaphore to control that a cache entry is not already being updated.
*/
private Map<Object, Semaphore> semaphoreMap = new HashMap<Object, Semaphore>();
/** The cache decorated by this class. */
private Ehcache cache;
/** Responsible of creating new cache entries. */
private CacheEntryFactory factory;
/**
* Looks up an entry. All cache entry of this cache will be eternal AND will have a timeToLive. When a element is "expired" (time of
* creation + time to live < now) the cache will return this "expired" objet but will asynchronously update this cache entry by
* delegating its creation to a net.sf.ehcache.constructs.blocking.CacheEntryFactory if it is not already being created.
*/
public Element get(final Object key) {
Element element = cache.get(key);
if (element != null && isUpToDate(element)) {
LOGGER.debug("cache hit for key : {}", key);
} else {
LOGGER.debug("entry expired for key : {}", key);
try {
executor.execute(new RefreshElementTask(key));
} catch (final Exception e) {
throw new CacheException("Could not fetch object for cache entry with key " + key, e);
}
}
return element;
}
/**
* Return true if an element is up to date : (creation time + time to live) > now.
*
* @param element the cache element.
* @return true if an element is up to date.
*/
private boolean isUpToDate(Element element) {
long now = System.currentTimeMillis();
long expirationTime = element.getCreationTime() + (TimeUnit.SECONDS.toMillis(element.getTimeToLive()));
return now < expirationTime;
}
private final class RefreshElementTask implements Runnable {
private final Object key;
private RefreshElementTask(Object key) {
this.key = key;
}
@Override
public void run() {
final Semaphore semaphore;
if (semaphoreMap.get(key) == null) {
semaphoreMap.put(key, new Semaphore(1));
}
semaphore = semaphoreMap.get(key);
if (semaphore.tryAcquire()) {
LOGGER.debug("semaphore acquired for key : {}", key);
try {
refreshElement();
} catch (Exception e) {
LOGGER.warn("Error while retrieving entry for key " + key, e);
extendsElementTimeToLive();
} finally {
semaphore.release();
}
} else {
LOGGER.debug("Entry for key {} already being retrieved", key);
}
}
private void refreshElement() throws Exception {
/*
* Another get to avoid this bug :
* Thead-1 : cache miss
* Thead-1 : acquire
* Thead-2 : cache miss
* Thead-1 : release
* Thead-2 : acquire
* Thead-2 : release
*/
Element element = cache.get(key);
if (element != null && isUpToDate(element)) {
LOGGER.debug("cache hit for key : {}", key);
} else {
Object entry = factory.createEntry(key);
element = makeAndCheckElement(key, entry);
if (entry != null) {
putElementInCache(element);
}
}
}
private void extendsElementTimeToLive() {
Element element = cache.get(key);
if (element != null) {
element = new Element(key, element.getValue());
element.setTimeToLive(DEFAULT_TIME_TO_LIVE);
} else {
element = new Element(key, null);
element.setTimeToLive(DEFAULT_TIME_TO_LIVE);
}
putElementInCache(element);
}
private void putElementInCache(Element element) {
// Protection : setEternal set timeToLive to 0 for ehcache 2.0+
int timeToLive = element.getTimeToLive();
element.setEternal(true);
element.setTimeToLive(timeToLive);
cache.put(element);
LOGGER.debug("put in cache key : {}", key);
}
/**
* Create an net.sf.ehcache.Element object from the value return by
* net.sf.ehcache.constructs.blocking.CacheEntryFactory.createEntry(Object).
*
* @param key the cache entry key.
* @param value the cache entry value.
* @return the net.sf.ehcache.Element created.
* @throws CacheException
*/
private Element makeAndCheckElement(Object key, Object value) {
// check if null
if (value == null) {
return new Element(key, null);
}
// simply build a new element using the supplied key
if (!(value instanceof Element)) {
return new Element(key, value);
}
// It is already an element - perform sanity checks
Element element = (Element)value;
if ((element.getObjectKey() == null) && (key == null)) {
return element;
} else if (element.getObjectKey() == null) {
throw new CacheException("CacheEntryFactory returned an Element with a null key");
} else if (!element.getObjectKey().equals(key)) {
throw new CacheException("CacheEntryFactory returned an Element with a different key: " + element.getObjectKey()
+ " compared to the key that was requested: " + key);
} else {
return element;
}
}
}
// GETTERS/SETTERS
public void setCache(Ehcache cache) {
this.cache = cache;
}
public void setFactory(CacheEntryFactory factory) {
this.factory = factory;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment