package freenet.store.caching; import java.util.ArrayList; import freenet.support.Logger; import freenet.support.Ticker; /** * Tracks the memory used by a bunch of CachingFreenetStore's, and writes blocks to disk when full or * after 5 minutes. One major objective here is we should not do disk I/O inside a lock, all methods * should be non-blocking, even if it means the caller needs to do a blocking disk write. * * @author Simon Vocella <voxsim@gmail.com> * */ public class CachingFreenetStoreTracker { private static volatile boolean logMINOR; /** Number of keys that it's pushed to the *underlying* store in the add function. * FIXME make this configurable??? */ private static int numberOfKeysToWrite = 20; /** Lower threshold, when it will start a write job, but still accept the data. */ private static double lowerThreshold = 0.9; private final long maxSize; private final long period; private final ArrayList<CachingFreenetStore<?>> cachingStores; private final Ticker ticker; /** Is a write job queued for some point in the next period? There should only be one such job * queued. However if we then run out of memory we will run a job immediately. */ private boolean queuedJob; /** Is a write job running right now? This prevents us from running multiple pushAllCachingStores() * in parallel and thus wasting memory, even if we run out of memory and so have to run a job * straight away. */ private boolean runningJob; private long size; static { Logger.registerClass(CachingFreenetStore.class); } public CachingFreenetStoreTracker(long maxSize, long period, Ticker ticker) { if(ticker == null) throw new IllegalArgumentException(); this.size = 0; this.maxSize = maxSize; this.period = period; this.queuedJob = false; this.cachingStores = new ArrayList<CachingFreenetStore<?>>(); this.ticker = ticker; } /** register a CachingFreenetStore to be called when we get full or to flush all after a setted period. */ public void registerCachingFS(CachingFreenetStore<?> fs) { synchronized (cachingStores) { cachingStores.add(fs); } } public void unregisterCachingFS(CachingFreenetStore<?> fs) { long sizeBlock = 0; while(true) { sizeBlock = fs.pushLeastRecentlyBlock(); synchronized(this) { if(sizeBlock == -1) break; else size -= sizeBlock; } } synchronized (cachingStores) { cachingStores.remove(fs); } } /** If we are close to the limit, we will schedule an off-thread job to flush ALL the caches. * Even if we are not, we schedule one after period. If we are at the limit, we will return * false, and the caller should write directly to the underlying store. */ public synchronized boolean add(long sizeBlock) { /** Here have a lower threshold, say 90% of maxSize, when it will start a write job, but * still accept the data. */ boolean justStartedPush = false; if(this.size + sizeBlock > this.maxSize*lowerThreshold) { pushOffThreadNow(); justStartedPush = true; } //Check max size if(this.size + sizeBlock > this.maxSize) { // Over the limit, caller must write directly. // A delayed write is probably scheduled already. This is not a problem. // FIXME maybe we should remove it? return false; } else { this.size += sizeBlock; if(!justStartedPush) { // Write everything to disk after the maximum delay (period), unless there is already // a job scheduled to write to disk before that. pushOffThreadDelayed(); } // Else will be written anyway. return true; } } private synchronized void pushOffThreadNow() { if(runningJob) return; runningJob = true; this.ticker.queueTimedJob(new Runnable() { @Override public void run() { try { pushAllCachingStores(); } finally { runningJob = false; } } }, 0); } private void pushOffThreadDelayed() { if(queuedJob) return; queuedJob = true; this.ticker.queueTimedJob(new Runnable() { @Override public void run() { synchronized(this) { if(runningJob) return; runningJob = true; } try { pushAllCachingStores(); } finally { synchronized(this) { queuedJob = false; runningJob = false; } } } }, period); } void pushAllCachingStores() { CachingFreenetStore<?>[] cachingStoresSnapshot = null; while(true) { // Need to re-check occasionally in case new stores have been added. synchronized (cachingStores) { cachingStoresSnapshot = this.cachingStores.toArray(new CachingFreenetStore[cachingStores.size()]); } for(CachingFreenetStore<?> cfs : cachingStoresSnapshot) { int k=0; while(k < numberOfKeysToWrite) { long sizeBlock = cfs.pushLeastRecentlyBlock(); if(sizeBlock == -1) break; synchronized(this) { size -= sizeBlock; assert(size >= 0); // Break immediately if in unit testing. if(size < 0) { Logger.error(this, "Cache broken: Size = "+size); size = 0; } if(size == 0) return; } k++; } } } } public long getSizeOfCache() { long sizeReturned; synchronized(this) { sizeReturned = size; } return sizeReturned; } }