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(); } }