/** * Copyright 2012 Comcast Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.cmb.common.util; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import org.apache.log4j.Logger; /** * This class is a highly concurrent cache used to cache key, value pairs with an expiration * on the value. * The class is thread-safe * K - the key type * V - the Value type * * @author aseem * Class ia thread-safe */ public final class ExpiringCache<K, V> { private static final Logger logger = Logger.getLogger(ExpiringCache.class); private final int cacheKeysLimit; /** * @param cacheKeysLimit The maximum number of keys in the cache */ public ExpiringCache(int cacheKeysLimit) { this.cacheKeysLimit = cacheKeysLimit; } private class ValueContainer { final FutureTask<V> future; final long createdTimestamp; final int exp; public ValueContainer(FutureTask<V> val, long ts, int expP) { future = val; createdTimestamp = ts; exp = expP; } } private ConcurrentHashMap<K, ValueContainer> cache = new ConcurrentHashMap<K, ValueContainer>(); /** * Clean up the expired key values. * This operation has complexity O(n) where n is the number of key, value pairs stored in the * cache. * Note: THis method is not synchronized with the getAndSetIfNotPresent. Its possible for a race condition * where we evict something from the cache that was just recently set in cache. That's an acceptable tradeoff * This is why we try and minimize calls to cleanup */ private void cleanup() { for (Map.Entry<K, ValueContainer> entry : cache.entrySet()) { if (entry.getValue().createdTimestamp + entry.getValue().exp < System.currentTimeMillis()) { cache.remove(entry.getKey()); } } } /** * @param key * @return true if cache contains key whose value has not yet expired, false otherwise */ public boolean containsKey(K key) { ValueContainer existingValContainer = cache.get(key); if (existingValContainer != null) { if (existingValContainer.createdTimestamp + existingValContainer.exp > System.currentTimeMillis()) { return true; } } return false; } /** * Remove a key from the cache * @param key */ public void remove(K key) { if (key == null) { return; } cache.remove(key); } /** * Thrown when cache reaches its configured limit */ @SuppressWarnings("serial") public static class CacheFullException extends Exception {} /** * * @param key THe key * @param valueGetter The Caller that will get the V value if none is cached or if previous one expired * @param exp THe expiration time in milliseconds * @return The Value V that was cached ir that just got cached. * @throws InterruptedException * @throws ExecutionException * Note: method will block if we need to call valueGetter * Note: THere is a window where t1 could add a new future-task and t2 could be removing it */ public V getAndSetIfNotPresent(K key, Callable<V> valueGetter, int exp) throws CacheFullException { ValueContainer existingValContainer = cache.get(key); if (existingValContainer != null) { if (existingValContainer.createdTimestamp + existingValContainer.exp < System.currentTimeMillis()) { //expired FutureTask<V> ft = new FutureTask<V>(valueGetter); ValueContainer valContainer = new ValueContainer(ft, System.currentTimeMillis(), exp); if (cache.replace(key, existingValContainer, valContainer)) { //was replaced valContainer.future.run(); try { return valContainer.future.get(); } catch (Exception e) { logger.error("event=no_value_getter", e); throw new IllegalStateException("Could not get value from valueGetter", e); } } else { //someone beat us to it. Lets do this again return getAndSetIfNotPresent(key, valueGetter, exp); } } else { try { return existingValContainer.future.get(); } catch (Exception e) { logger.error("event=no_value_getter_from_cache", e); throw new IllegalStateException("Could not get value from existing cache entry", e); } } } //Below is code when no ValueGetter exists for key if (cache.size() >= cacheKeysLimit) { cleanup(); throw new CacheFullException(); } FutureTask<V> ft = new FutureTask<V>(valueGetter); ValueContainer valContainer = new ValueContainer(ft, System.currentTimeMillis(), exp); existingValContainer = cache.putIfAbsent(key, valContainer); if (existingValContainer == null) { //none previously existing valContainer.future.run(); existingValContainer = valContainer; } try { return existingValContainer.future.get(); //will block till valueGetter returns a value } catch (Exception e) { logger.error("event=no_value_getter_from_callable", e); throw new IllegalStateException("Could not get value from user passed Callable:" + valueGetter, e); } } }