package edu.vandy.common;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Timeout cache that uses thread-safe concurrent HashMap to cache
* data and uses a ScheduledExecutorService to execute a Runnable
* after a designated timeout to remove expired cache entries.
*/
public class ExecutorServiceTimeoutCache<K, V>
extends RefCounted
implements TimeoutCache<K, V> {
/**
* Debugging tag used by the Android logger.
*/
protected final String TAG =
getClass().getSimpleName();
/**
* A thread-safe HashMap that supports full concurrency of
* retrievals and high expected concurrency for updates. It store
* CacheValues.
*/
private ConcurrentHashMap<K, CacheValues> mResults =
new ConcurrentHashMap<>();
/**
* Executor service that will execute Runnable after certain
* timeouts to remove expired CacheValues.
*/
private ScheduledExecutorService mScheduledExecutorService =
Executors.newScheduledThreadPool(1);
/**
* Datatype that represents the contents of the cache. It
* contains the value of the cache entity and a future that
* executes a runnable after certain time period elapses to remove
* expired CacheValue objects.
*/
class CacheValues {
/**
* Value of the cache.
*/
final public V mValue;
/**
* Result of an asynchronous computation. It references a
* runnable that has been scheduled to execute after certain
* time period elapses.
*/
public ScheduledFuture<?> mFuture = null;
/**
* Constructor for CacheValue.
*
* @param value The cache entry
*/
public CacheValues(V value) {
mValue = value;
}
/**
* Setter for the ScheduledFuture.
*
* @param future A ScheduledFuture that can be used to cancel a Runnable.
*/
public void setFuture(ScheduledFuture<?> future) {
mFuture = future;
}
}
/**
* Put the @a value into the cache at the designated @a key with a
* certain timeout after which the CacheValue will expire.
*
* @param key The key for the cache entry
* @param value The value of the cache entry
* @param timeout The timeout period in seconds
*/
@Override
public void put(final K key,
V value,
int timeout) {
// Create this object here so it can be referenced in the
// cleanupCacheRunnable below.
final CacheValues cacheValues = new CacheValues(value);
// Runnable that when executed will remove a CacheValues when
// its timeout expires.
final Runnable cleanupCacheRunnable = new Runnable() {
@Override
public void run() {
// Only remove key if it is currently associated
// with cacheValues. This avoid race conditions
// that would otherwise occur since an mFuture to
// a previous CacheValues isn't canceled until
// after the new CacheValues is added to the map.
mResults.remove(key,
cacheValues);
}
};
// Put a new CacheValues object into the ConcurrentHashMap
// associated with the key and return the previous
// CacheValues.
CacheValues prevCacheValues =
mResults.put(key,
cacheValues);
// If there was a previous CacheValues associated with this
// key then cancel the future immediately. Note that there is
// no race condition between the ScheduledExecutorService
// running the cleanupCacheRunnable and canceling the future
// here since the ConcurrentHashMap.remove() call won't
// actually remove the key unless the value is equal to the
// original cacheValues reference.
if (prevCacheValues != null)
prevCacheValues.mFuture.cancel(true);
// Create a ScheduledFuture for the new cacheValues object that
// will execute the cleanupCacheRunnable after the designated
// timeout.
ScheduledFuture<?> future =
mScheduledExecutorService.schedule(cleanupCacheRunnable,
timeout,
TimeUnit.SECONDS);
// Now that we have a future, attach it to the cacheValues object
// that has already been safely added to the cache. The reason we
// do not set the future before adding the cacheValues object to the
// cache is because it is possible (but unlikely) for the future
// to trigger in the small time window between when it is started
// and returned from the ScheduledExecutorService and when the
// put() call is made to add it to the cache.
cacheValues.setFuture(future);
}
/**
* Gets the @a value from the cache at the designated @a key.
*
* @param key The key for the cache entry
* @return value The value associated with the key, Which may be
* null if there's no key in the cache
*/
@Override
public final V get(K key) {
CacheValues cacheValues = mResults.get(key);
return cacheValues != null ? cacheValues.mValue : null;
}
/**
* Removes the value associated with the designated @a key.
*
* @param key The key for the cache entry
* @param expirationTime (ignored)
*/
@Override
public void remove(K key,
long expirationTime) {
mResults.remove(key);
}
/**
* Return the current number of entries in the cache.
*
* @return size
*/
@Override
public final int size() {
return mResults.size();
}
/**
* Shutdown the ScheduledExecutorService.
*/
@Override
protected void close() {
// Cancel all remaining futures.
for (CacheValues cvs : mResults.values())
if (cvs.mFuture != null)
cvs.mFuture.cancel(true);
// Shutdown the ScheduledExecutorService immediately.
mScheduledExecutorService.shutdownNow();
}
}