package com.netflix.evcache; import java.util.Collections; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.CacheStats; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFutureTask; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.netflix.config.ChainedDynamicProperty; import com.netflix.config.DynamicIntProperty; import com.netflix.evcache.metrics.EVCacheMetricsFactory; import com.netflix.evcache.util.EVCacheConfig; import com.netflix.servo.DefaultMonitorRegistry; import com.netflix.servo.MonitorRegistry; import com.netflix.servo.annotations.DataSourceType; import com.netflix.servo.monitor.Monitor; import com.netflix.servo.monitor.MonitorConfig; import com.netflix.servo.monitor.MonitorConfig.Builder; import com.netflix.servo.monitor.StepCounter; import com.netflix.servo.tag.Tag; import net.spy.memcached.transcoders.Transcoder; /** * An In Memory cache that can be used to hold data for short duration. This is * helpful when the same key is repeatedly requested from EVCache within a short * duration. This can be turned on dynamically and can relive pressure on * EVCache Server instances. */ public class EVCacheInMemoryCache<T> { private static final Logger log = LoggerFactory.getLogger(EVCacheInMemoryCache.class); private final ChainedDynamicProperty.IntProperty _cacheDuration; // The key will be cached for this long private final DynamicIntProperty _refreshDuration, _exireAfterAccessDuration; private final DynamicIntProperty _cacheSize; // This many items will be cached private final DynamicIntProperty _poolSize; // This many threads will be initialized to fetch data from evcache async private final String appName; private LoadingCache<String, T> cache; private ExecutorService pool = null; private final Transcoder<T> tc; private final EVCacheImpl impl; public EVCacheInMemoryCache(String appName, Transcoder<T> tc, EVCacheImpl impl) { this.appName = appName; this.tc = tc; this.impl = impl; this._cacheDuration = EVCacheConfig.getInstance().getChainedIntProperty(appName + ".inmemory.cache.duration.ms", appName + ".inmemory.expire.after.write.duration.ms", 0); this._cacheDuration.addCallback(new Runnable() { public void run() { setupCache(); } }); this._exireAfterAccessDuration = EVCacheConfig.getInstance().getDynamicIntProperty(appName + ".inmemory.expire.after.access.duration.ms", 0); this._exireAfterAccessDuration.addCallback(new Runnable() { public void run() { setupCache(); } }); this._refreshDuration = EVCacheConfig.getInstance().getDynamicIntProperty(appName + ".inmemory.refresh.after.write.duration.ms", 0); this._refreshDuration.addCallback(new Runnable() { public void run() { setupCache(); } }); this._cacheSize = EVCacheConfig.getInstance().getDynamicIntProperty(appName + ".inmemory.cache.size", 100); this._cacheSize.addCallback(new Runnable() { public void run() { setupCache(); } }); this._poolSize = EVCacheConfig.getInstance().getDynamicIntProperty(appName + ".thread.pool.size", 5); this._poolSize.addCallback(new Runnable() { public void run() { initRefreshPool(); } }); setupCache(); setupMonitoring(appName); } private WriteLock writeLock = new ReentrantReadWriteLock().writeLock(); private void initRefreshPool() { final ExecutorService oldPool = pool; writeLock.lock(); try { final ThreadFactory factory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat( "EVCacheInMemoryCache-%d").build(); pool = Executors.newFixedThreadPool(_poolSize.get(), factory); if(oldPool != null) oldPool.shutdown(); } finally { writeLock.unlock(); } } private void register(Monitor<?> monitor) { final MonitorRegistry registry = DefaultMonitorRegistry.getInstance(); if (registry.isRegistered(monitor)) registry.unregister(monitor); // This will ensure old cache values are unregistered registry.register(monitor); } private MonitorConfig getMonitorConfig(String appName, String metric, Tag tag) { final Builder builder = MonitorConfig.builder("EVCacheInMemoryCache" + "-" + appName + "-" + metric).withTag(tag); return builder.build(); } private void setupCache() { try { CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder().recordStats(); if(_cacheSize.get() > 0) { builder = builder.maximumSize(_cacheSize.get()); } if(_exireAfterAccessDuration.get() > 0) { builder = builder.expireAfterAccess(_exireAfterAccessDuration.get(), TimeUnit.MILLISECONDS); } else if(_cacheDuration.get().intValue() > 0) { builder = builder.expireAfterWrite(_cacheDuration.get(), TimeUnit.MILLISECONDS); } if(_refreshDuration.get() > 0) { builder = builder.refreshAfterWrite(_refreshDuration.get(), TimeUnit.MILLISECONDS); } initRefreshPool(); final LoadingCache<String, T> newCache = builder.build( new CacheLoader<String, T>() { public T load(String key) throws EVCacheException { try { final T t = impl.doGet(key, tc); if(t == null) throw new DataNotFoundException("Data for key : " + key + " could not be loaded as it was not found in EVCache"); return t; } catch (DataNotFoundException e) { throw e; } catch (EVCacheException e) { log.error("EVCacheException while loading key -> "+ key, e); throw e; } catch (Exception e) { log.error("EVCacheException while loading key -> "+ key, e); throw new EVCacheException("key : " + key + " could not be loaded", e); } } public ListenableFuture<T> reload(final String key, T prev) { ListenableFutureTask<T> task = ListenableFutureTask.create(new Callable<T>() { public T call() { try { T t = load(key); if(t == null) { EVCacheMetricsFactory.increment(appName, null, null, "EVCacheInMemoryCache" + "-" + appName + "-Reload-NotFound"); return prev; } else { EVCacheMetricsFactory.increment(appName, null, null, "EVCacheInMemoryCache" + "-" + appName + "-Reload-Success"); } return t; } catch (EVCacheException e) { log.error("EVCacheException while reloading key -> "+ key, e); EVCacheMetricsFactory.increment(appName, null, null, "EVCacheInMemoryCache" + "-" + appName + "-Reload-Fail"); return prev; } } }); pool.execute(task); return task; } }); if(cache != null) newCache.putAll(cache.asMap()); final Cache<String, T> currentCache = this.cache; this.cache = newCache; if(currentCache != null) { currentCache.invalidateAll(); currentCache.cleanUp(); } } catch (Exception e) { log.error(e.getMessage(), e); } } private LoadingCache<String, T> getCache() { return cache; } private CacheStats getStats() { return cache.stats(); } private void setupMonitoring(final String appName) { final StepCounter sizeCounter = new StepCounter(getMonitorConfig(appName, "size", DataSourceType.GAUGE)) { @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); log.debug("Current size is : " + getCache().size()); return Long.valueOf(getCache().size()); } @Override public Number getValue(int pollerIndex) { return getValue(); } }; register(sizeCounter); register(new Monitor<Number>() { final MonitorConfig config; { config = getMonitorConfig(appName, "requests", DataSourceType.COUNTER); } @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Long.valueOf(getStats().requestCount()); } @Override public Number getValue(int pollerIndex) { return getValue(); } @Override public MonitorConfig getConfig() { return config; } }); final StepCounter hitrateCounter = new StepCounter(getMonitorConfig(appName, "hitrate", DataSourceType.GAUGE)) { @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().hitRate()); } @Override public Number getValue(int pollerIndex) { return getValue(); } }; register(hitrateCounter); register(new Monitor<Number>() { final MonitorConfig config; { config = getMonitorConfig(appName, "hits", DataSourceType.COUNTER); } @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().hitCount()); } @Override public Number getValue(int pollerIndex) { return getValue(); } @Override public MonitorConfig getConfig() { return config; } }); register(new Monitor<Number>() { final MonitorConfig config; { config = getMonitorConfig(appName, "miss", DataSourceType.COUNTER); } @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().missCount()); } @Override public Number getValue(int pollerIndex) { return getValue(); } @Override public MonitorConfig getConfig() { return config; } }); register(new Monitor<Number>() { final MonitorConfig config; { config = getMonitorConfig(appName, "evictions", DataSourceType.COUNTER); } @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().evictionCount()); } @Override public Number getValue(int pollerIndex) { return getValue(); } @Override public MonitorConfig getConfig() { return config; } }); register(new Monitor<Number>() { final MonitorConfig config; { config = getMonitorConfig(appName, "loadExceptionCount", DataSourceType.COUNTER); } @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().loadExceptionCount()); } @Override public Number getValue(int pollerIndex) { return getValue(); } @Override public MonitorConfig getConfig() { return config; } }); register(new Monitor<Number>() { final MonitorConfig config; { config = getMonitorConfig(appName, "loadCount", DataSourceType.COUNTER); } @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().loadCount()); } @Override public Number getValue(int pollerIndex) { return getValue(); } @Override public MonitorConfig getConfig() { return config; } }); register(new Monitor<Number>() { final MonitorConfig config; { config = getMonitorConfig(appName, "loadSuccessCount", DataSourceType.COUNTER); } @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().loadSuccessCount()); } @Override public Number getValue(int pollerIndex) { return getValue(); } @Override public MonitorConfig getConfig() { return config; } }); register(new Monitor<Number>() { final MonitorConfig config; { config = getMonitorConfig(appName, "totalLoadTime-ms", DataSourceType.COUNTER); } @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().totalLoadTime()/1000000); } @Override public Number getValue(int pollerIndex) { return getValue(); } @Override public MonitorConfig getConfig() { return config; } }); final StepCounter loadExceptionRate = new StepCounter(getMonitorConfig(appName, "loadExceptionRate", DataSourceType.GAUGE)) { @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().loadExceptionRate()); } @Override public Number getValue(int pollerIndex) { return getValue(); } }; register(loadExceptionRate); final StepCounter averageLoadTime = new StepCounter(getMonitorConfig(appName, "averageLoadTime-ms", DataSourceType.GAUGE)) { @Override public Number getValue() { if (getCache() == null) return Long.valueOf(0); return Double.valueOf(getStats().averageLoadPenalty()/1000000); } @Override public Number getValue(int pollerIndex) { return getValue(); } }; register(averageLoadTime); } public T get(String key) throws ExecutionException { if (cache == null) return null; final T val = cache.get(key); if (log.isDebugEnabled()) log.debug("GET : appName : " + appName + "; Key : " + key + "; val : " + val); return val; } public void put(String key, T value) { if (cache == null) return; cache.put(key, value); if (log.isDebugEnabled()) log.debug("PUT : appName : " + appName + "; Key : " + key + "; val : " + value); } public void delete(String key) { if (cache == null) return; cache.invalidate(key); if (log.isDebugEnabled()) log.debug("DEL : appName : " + appName + "; Key : " + key); } public Map<String, T> getAll() { if (cache == null) return Collections.<String, T>emptyMap(); return cache.asMap(); } public static final class DataNotFoundException extends EVCacheException { private static final long serialVersionUID = 1800185311509130263L; public DataNotFoundException(String message) { super(message); } } }