package org.solbase.cache; import java.io.IOException; import java.io.Serializable; import java.util.HashMap; import java.util.Iterator; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import net.rubyeye.xmemcached.exception.MemcachedException; import org.apache.log4j.Logger; import org.solbase.lucenehbase.IndexWriter; public class SolbaseCache<K extends Serializable & Comparable<?>, V extends Serializable, Z extends Serializable, M extends Serializable> { private final static Logger logger = Logger.getLogger(SolbaseCache.class); private static final int BACKGROUND_THREAD_POOL_SIZE = 50; public static enum ModificationType { UPDATE, ADD, DELETE } private CachedObjectLoader<K, V, Z, M> _loader = null; private Long cacheTimeout; private VersionedCache<K, V, Z> cache; private VersionedCache<K, V, Z> threadLocalCache; private ThreadPoolExecutor executor = new ThreadPoolExecutor(BACKGROUND_THREAD_POOL_SIZE, BACKGROUND_THREAD_POOL_SIZE, 5, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10)); private class LockedResult { ReentrantLock lock = new ReentrantLock(); CachedObjectWrapper<V, Z> result = null; int refCount = 0; } private HashMap<K, LockedResult> currentlyAccesingMap = new HashMap<K, LockedResult>(); public SolbaseCache(Long cacheTimeout, VersionedCache<K, V, Z> cache, VersionedCache<K, V, Z> threadLocalCache) { this.cache = cache; this.threadLocalCache = threadLocalCache; cache.setTimeout(cacheTimeout); this.cacheTimeout = cacheTimeout; } public SolbaseCache(CachedObjectLoader<K, V, Z, M> loader, Long cacheTimeout, VersionedCache<K, V, Z> cache, VersionedCache<K, V, Z> threadLocalCache) { this(cacheTimeout, cache, threadLocalCache); this._loader = loader; } public CachedObjectWrapper<V, Z> getCachedObject(K key, String indexName, int start, int end) throws IOException, InterruptedException, MemcachedException, TimeoutException { return getCachedObject(key, _loader, indexName, start, end); } public CachedObjectWrapper<V, Z> getCachedObject(K key, CachedObjectLoader<K, V, Z, M> loader, String indexName, int start, int end) throws IOException, InterruptedException, MemcachedException, TimeoutException { // let's see if we have a data in thread local cache first if so return it CachedObjectWrapper<V, Z> tmp = this.threadLocalCache.get(key); if (tmp != null) { return tmp; } LockedResult lockedResult; synchronized (currentlyAccesingMap) { lockedResult = currentlyAccesingMap.get(key); if (lockedResult == null) { lockedResult = new LockedResult(); currentlyAccesingMap.put(key, lockedResult); } lockedResult.refCount++; } try { tmp = this.cache.get(key); if (tmp == null) { lockedResult.lock.lock(); try { if (lockedResult.result == null) { // other thread hasn't fetched data from hbase, so I'm going to hit hbase now tmp = loader.loadObject(key, start, end, this); if(tmp != null){ tmp.setCacheTime(System.currentTimeMillis()); lockedResult.result = tmp; } else { logger.debug("empty object loaded for this key: " + key.toString()); } } else { // other thread has finished fetching from database, so return a restul from other thread tmp = lockedResult.result; } if (tmp != null) { // put it into cache this.cache.put(key, tmp); } } finally { lockedResult.lock.unlock(); } } else { long currentTime = System.currentTimeMillis(); long cacheTime = tmp.getCacheTime(); long diff = currentTime - cacheTime; if (cacheTime != -1l && diff > cacheTimeout) { tmp.setCacheTime(-1l); checkAndReloadCachedObjectAsynch(this.cache, cacheTime, tmp, key, loader, currentTime, indexName, start, end, lockedResult, true); } } // put object in thread local cache so it doesn't have to go thru above logic again within single request this.threadLocalCache.put(key, tmp); return tmp; } finally { synchronized (currentlyAccesingMap) { lockedResult.refCount--; if (lockedResult.refCount == 0) { currentlyAccesingMap.remove(key); } } } } public void updateCachedObject(K key, M modificationData, String indexName, IndexWriter writer, ModificationType modType, boolean updateStore, int startDocId, int endDocId) throws IOException, InterruptedException, MemcachedException, TimeoutException { updateCachedObject(key, modificationData, _loader, indexName, writer, modType, updateStore, startDocId, endDocId); } public void updateCachedObject(K key, M modificationData, CachedObjectLoader<K, V, Z, M> loader, String indexName, IndexWriter writer, ModificationType modType, boolean updateStore, int startDocId, int endDocId) throws IOException, InterruptedException, MemcachedException, TimeoutException { LockedResult lockedResult; synchronized (currentlyAccesingMap) { lockedResult = currentlyAccesingMap.get(key); if (lockedResult == null) { lockedResult = new LockedResult(); currentlyAccesingMap.put(key, lockedResult); } lockedResult.refCount++; } try { CachedObjectWrapper<V, Z> tmp = null; tmp = this.cache.get(key); if (tmp == null) { lockedResult.lock.lock(); try { if (lockedResult.result != null) { // are we good without locking CompactedTermDocMetadata? tmp = lockedResult.result; loader.updateObject(tmp, modificationData, this, modType, startDocId, endDocId); } if (updateStore) { loader.updateObjectStore(key, modificationData, writer, this, modType, startDocId, endDocId); } } finally { lockedResult.lock.unlock(); } } else { loader.acquireObjectLock(key); try { lockedResult.lock.lock(); try { if (lockedResult.result != null) { tmp = lockedResult.result; } loader.updateObject(tmp, modificationData, this, modType, startDocId, endDocId); if (updateStore) { loader.updateObjectStore(key, modificationData, writer, this, modType, startDocId, endDocId); } } finally { lockedResult.lock.unlock(); } } finally { loader.releaseObjectLock(key); } } } finally { synchronized (currentlyAccesingMap) { lockedResult.refCount--; if (lockedResult.refCount == 0) { currentlyAccesingMap.remove(key); } } } } private void modifyObjectRecursively(K key, M modificationData, Iterator<VersionedCache<K, V, Z>> cacheIterator, CachedObjectLoader<K, V, Z, M> loader, long currentTime, String indexName, LockedResult lockedResult, IndexWriter writer, ModificationType modType, boolean updateStore, int startDocId, int endDocId) throws IOException { } private CachedObjectWrapper<V, Z> refreshObject(VersionedCache<K, V, Z> vc, long currentCacheTime, final CachedObjectWrapper<V, Z> objectWrapper, K key, CachedObjectLoader<K, V, Z, M> loader, long currentTime, String indexName, int start, int end, LockedResult lockedResult, boolean loadFromStore) throws IOException { CachedObjectWrapper<V, Z> tmp = null; lockedResult.lock.lock(); try { if (lockedResult.result == null) { tmp = loader.loadObject(key, start, end, this); tmp.setCacheTime(System.currentTimeMillis()); lockedResult.result = tmp; } else { tmp = lockedResult.result; } } finally { lockedResult.lock.unlock(); } vc.put(key, tmp); return tmp; } private void checkAndReloadCachedObjectAsynch(final VersionedCache<K, V, Z> vc, final long currentCacheTime, final CachedObjectWrapper<V, Z> objectWrapper, final K key, final CachedObjectLoader<K, V, Z, M> loader, final long currentTime, final String indexName, final int start, final int end, final LockedResult lockedResult, final boolean loadFromStore) throws IOException { Runnable runnable = new Runnable() { @Override public void run() { Z loadedIdentifier; try { loadedIdentifier = loader.getVersionIdentifier(key, start, end); Z versionIdentifier = objectWrapper.getVersionIdentifier(); if (loadedIdentifier == null || versionIdentifier == null || (!versionIdentifier.equals(loadedIdentifier))) { refreshObject(vc, currentCacheTime, objectWrapper, key, loader, currentTime, indexName, start, end, lockedResult, loadFromStore); } else { resetCacheTime(key); } } catch (Throwable e) { logger.error(e); } } }; try { executor.execute(runnable); } catch (RejectedExecutionException ree) { logger.warn("Async check and reload cached object failed, ignoring ", ree); } } public void resetCacheTime(K key) throws IOException { this.cache.resetCacheTime(key, System.currentTimeMillis()); } public boolean isCacheFull() { return cache.isCacheFull(); } }