package edu.brown.hstore.util; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.pool.BasePoolableObjectFactory; import org.apache.log4j.Logger; import org.voltdb.ParameterSet; import org.voltdb.VoltTable; import org.voltdb.utils.EstTime; import edu.brown.logging.LoggerUtil; import edu.brown.logging.LoggerUtil.LoggerBoolean; import edu.brown.pools.FastObjectPool; import edu.brown.utils.StringUtil; public class QueryCache { private static final Logger LOG = Logger.getLogger(QueryCache.class); private static final LoggerBoolean debug = new LoggerBoolean(); private static final LoggerBoolean trace = new LoggerBoolean(); static { LoggerUtil.attachObserver(LOG, debug, trace); } /** * The default number of CacheEntry offsets to include in the txnCache's * pool of List<Integer>. Note that these are just the entries at the * PartitionExecutor that this QueryCache is attached to */ private static final int TXNCACHE_POOL_DEFAULT_SIZE = 3; /** * The number of List<Integer> we're allowed to keep idle at this PartitionExecutor. */ private static final int TXNCACHE_POOL_MAXIDLE = 200; // ---------------------------------------------------------------------------- // INTERNAL CACHE MEMBERS // ---------------------------------------------------------------------------- private static class CacheEntry { final Integer idx; Long txnId; int fragmentId; int partitionId; int paramsHash; VoltTable result; int accessCounter = 0; long accessTimestamp = 0; public CacheEntry(int idx) { this.idx = Integer.valueOf(idx); } public void init(int fragmentId, int partitionId, int paramsHash, VoltTable result) { this.fragmentId = fragmentId; this.partitionId = partitionId; this.paramsHash = paramsHash; this.result = result; } @Override public String toString() { Class<?> confClass = this.getClass(); Map<String, Object> m = new LinkedHashMap<String, Object>(); for (Field f : confClass.getDeclaredFields()) { Object obj = null; try { obj = f.get(this); } catch (IllegalAccessException ex) { throw new RuntimeException(ex); } if (obj instanceof VoltTable) { obj = "{Rows: " + ((VoltTable)obj).getRowCount() + "}"; } // System.err.printf("[%d] %s -> %s\n", this.hashCode(), f.getName(), obj); m.put(f.getName().toUpperCase(), obj); } // FOR // System.err.println(); return (StringUtil.formatMaps(m)); } } // CLASS /** * Simple circular buffer cache */ private static class Cache { private final CacheEntry buffer[]; private int current = 0; public Cache(int bufferSize) { this.buffer = new CacheEntry[bufferSize]; for (int i = 0; i < bufferSize; i++) { this.buffer[i] = new CacheEntry(i); } } public CacheEntry getNext(Long txnId) { if (this.current == this.buffer.length) { this.current = 0; } CacheEntry next = this.buffer[this.current++]; next.txnId = txnId; next.accessCounter = 0; return (next); } public CacheEntry get(Integer idx) { return (this.buffer[idx.intValue()]); } } // CLASS /** * List<Integer> pool used by txnCache * TODO: Switch to a better object pool */ private final FastObjectPool<ArrayList<Integer>> listPool = new FastObjectPool<ArrayList<Integer>>(new BasePoolableObjectFactory() { @Override public Object makeObject() throws Exception { return (new ArrayList<Integer>(TXNCACHE_POOL_DEFAULT_SIZE)); } public void passivateObject(Object obj) throws Exception { @SuppressWarnings("unchecked") ArrayList<Integer> l = (ArrayList<Integer>) obj; l.clear(); }; }, TXNCACHE_POOL_MAXIDLE); // ---------------------------------------------------------------------------- // INITIALIZATION // ---------------------------------------------------------------------------- private final Cache globalCache; private final Cache txnCache; /** * TransactionId -> List of CacheEntry Offsets */ private final Map<Long, List<Integer>> txnCacheXref = new HashMap<Long, List<Integer>>(); /** * Constructor */ public QueryCache(int globalBufferSize, int txnBufferSize) { this.globalCache = new Cache(globalBufferSize); this.txnCache = new Cache(txnBufferSize); } // ---------------------------------------------------------------------------- // API // ---------------------------------------------------------------------------- public void addGlobalQueryResult(int fragmentId, ParameterSet params, VoltTable result) { } /** * Store a new cache entry for a query that is specific to a transaction * This cached result is not be available to other transactions * @param txnId * @param fragmentId * @param params * @param result */ public void addResult(Long txnId, int fragmentId, int partitionId, ParameterSet params, VoltTable result) { if (debug.val) LOG.debug(String.format("#%d - Storing query result for FragmentId %d - %s", txnId, fragmentId, params)); this.addResult(txnId, fragmentId, partitionId, params.hashCode(), result); } /** * Store a new cache entry for a query that is specific to a transaction * This cached result is not be available to other transactions * @param txnId * @param fragmentId * @param partitionId * @param paramsHash * @param result */ public void addResult(Long txnId, int fragmentId, int partitionId, int paramsHash, VoltTable result) { if (debug.val) LOG.debug(String.format("#%d - Storing query result for FragmentId %d / paramsHash:%d", txnId, fragmentId, paramsHash)); List<Integer> entries = this.txnCacheXref.get(txnId); if (entries == null) { synchronized (this) { entries = this.txnCacheXref.get(txnId); if (entries == null) { try { entries = this.listPool.borrowObject(); } catch (Exception ex) { throw new RuntimeException("Failed to initialize list from object pool", ex); } this.txnCacheXref.put(txnId, entries); } } // SYNCH } CacheEntry entry = this.txnCache.getNext(txnId); assert(entry != null); entry.init(fragmentId, partitionId, paramsHash, result); entries.add(entry.idx); if (debug.val) LOG.debug(String.format("#%d - CacheEntry\n%s", txnId, entry.toString())); } /** * This assumes that we only have own thread accessing the cache * @param txnId * @param fragmentId * @param params * @return */ public VoltTable getResult(Long txnId, int fragmentId, int partitionId, ParameterSet params) { if (debug.val) LOG.debug(String.format("#%d - Retrieving query cache for FragmentId %d - %s", txnId, fragmentId, params)); List<Integer> entries = this.txnCacheXref.get(txnId); if (entries != null) { if (trace.val) LOG.trace(String.format("Txn #%d has %d cache entries", txnId, entries.size())); int paramsHash = -1; for (int i = 0, cnt = entries.size(); i < cnt; i++) { CacheEntry entry = this.txnCache.get(entries.get(i)); assert(entry != null); // Check whether somebody took our place in the cache or that // we don't even have the same fragmentId or partitionId if (entry.txnId.equals(txnId) == false || entry.fragmentId != fragmentId || entry.partitionId != partitionId) { continue; } // Then check whether our ParameterSet has the proper hashCode // We do this down here to allow us to only to compute the hashCode // unless we really have to. if (paramsHash == -1) paramsHash = params.hashCode(); if (entry.paramsHash != paramsHash) { continue; } // Bingo! entry.accessCounter++; entry.accessTimestamp = EstTime.currentTimeMillis(); return (entry.result); } // FOR } return (null); } /** * Remove all the cached query results that are specific for this transaction * @param txn_id */ public void purgeTransaction(Long txnId) { List<Integer> entries = this.txnCacheXref.get(txnId); if (entries != null) { try { this.listPool.returnObject(entries); } catch (Exception ex) { throw new RuntimeException("Failed to return list to object pool", ex); } } } // ---------------------------------------------------------------------------- // UTILITY CODE // ---------------------------------------------------------------------------- @Override public String toString() { @SuppressWarnings("unchecked") LinkedHashMap<String, Object> m[] = (LinkedHashMap<String, Object>[])new LinkedHashMap<?,?>[2]; int idx = 0; // Global Cache m[idx] = new LinkedHashMap<String, Object>(); m[idx].put("Global Cache", null); // TxnCache m[++idx] = new LinkedHashMap<String, Object>(); List<CacheEntry> entries = new ArrayList<CacheEntry>(); for (CacheEntry entry : this.txnCache.buffer) { if (entry.txnId != null) entries.add(entry); } // FOR m[idx].put(String.format("TxnCache[%d]", entries.size()), StringUtil.join("\n", entries).trim()); m[idx].put("Current Transactions", this.txnCacheXref); return StringUtil.formatMaps(m); } }