package freenet.store.caching; import java.io.IOException; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import freenet.keys.KeyVerifyException; import freenet.node.SemiOrderedShutdownHook; import freenet.store.BlockMetadata; import freenet.store.FreenetStore; import freenet.store.KeyCollisionException; import freenet.store.ProxyFreenetStore; import freenet.store.StorableBlock; import freenet.store.StoreCallback; import freenet.support.ByteArrayWrapper; import freenet.support.LRUMap; import freenet.support.Logger; import freenet.support.Ticker; import freenet.support.io.NativeThread; /** * CachingFreenetStore * * @author Simon Vocella <voxsim@gmail.com> * */ public class CachingFreenetStore<T extends StorableBlock> extends ProxyFreenetStore<T> { private static volatile boolean logMINOR; private boolean shuttingDown; /* If this flag is true, we don't accept puts anymore */ private final LRUMap<ByteArrayWrapper, Block<T>> blocksByRoutingKey; private final StoreCallback<T> callback; private final boolean collisionPossible; private final ReadWriteLock configLock = new ReentrantReadWriteLock(); private final CachingFreenetStoreTracker tracker; private final int sizeBlock; static { Logger.registerClass(CachingFreenetStore.class); } private final static class Block<T> { T block; byte[] data; byte[] header; boolean overwrite; boolean isOldBlock; } public CachingFreenetStore(StoreCallback<T> callback, FreenetStore<T> backDatastore, CachingFreenetStoreTracker tracker) { super(backDatastore); this.callback = callback; SemiOrderedShutdownHook shutdownHook = SemiOrderedShutdownHook.get(); this.blocksByRoutingKey = LRUMap.createSafeMap(ByteArrayWrapper.FAST_COMPARATOR); this.collisionPossible = callback.collisionPossible(); this.shuttingDown = false; this.tracker = tracker; this.sizeBlock = callback.getTotalBlockSize(); callback.setStore(this); shutdownHook.addEarlyJob(new NativeThread("Close CachingFreenetStore", NativeThread.HIGH_PRIORITY, true) { @Override public void realRun() { innerClose(); // SaltedHashFS has its own shutdown job. } }); } @Override public T fetch(byte[] routingKey, byte[] fullKey, boolean dontPromote, boolean canReadClientCache, boolean canReadSlashdotCache, boolean ignoreOldBlocks, BlockMetadata meta) throws IOException { ByteArrayWrapper key = new ByteArrayWrapper(routingKey); Block<T> block = null; configLock.readLock().lock(); try { block = blocksByRoutingKey.get(key); } finally { configLock.readLock().unlock(); } if(block != null) { try { return this.callback.construct(block.data, block.header, routingKey, block.block.getFullKey(), canReadClientCache, canReadSlashdotCache, meta, null); } catch (KeyVerifyException e) { Logger.error(this, "Error in fetching for CachingFreenetStore: "+e, e); } } return backDatastore.fetch(routingKey, fullKey, dontPromote, canReadClientCache, canReadSlashdotCache, ignoreOldBlocks, meta); } @Override public boolean probablyInStore(byte[] routingKey) { ByteArrayWrapper key = new ByteArrayWrapper(routingKey); Block<T> block = null; configLock.readLock().lock(); try { block = blocksByRoutingKey.get(key); } finally { configLock.readLock().unlock(); } return block != null || backDatastore.probablyInStore(routingKey); } @Override public void put(T block, byte[] data, byte[] header, boolean overwrite, boolean isOldBlock) throws IOException, KeyCollisionException { byte[] routingKey = block.getRoutingKey(); final ByteArrayWrapper key = new ByteArrayWrapper(routingKey); Block<T> storeBlock = new Block<T>(); storeBlock.block = block; storeBlock.data = data; storeBlock.header = header; storeBlock.overwrite = overwrite; storeBlock.isOldBlock = isOldBlock; boolean cacheIt = true; //Case cache it configLock.writeLock().lock(); try { if(!shuttingDown) { Block<T> previousBlock = blocksByRoutingKey.get(key); if(!collisionPossible || overwrite) { if(previousBlock == null) { cacheIt = tracker.add(sizeBlock); } if(cacheIt) { blocksByRoutingKey.push(key, storeBlock); } } else { //Case cache it but is it in blocksByRoutingKey? If so, throw a KCE if(previousBlock != null) { if(block.equals(previousBlock.block)) return; throw new KeyCollisionException(); } //Is probablyInStore()? If so, remove it from blocksByRoutingKey, and set a flag so we don't call put() if(backDatastore.probablyInStore(routingKey)) { cacheIt = false; } else { cacheIt = tracker.add(sizeBlock); if(cacheIt) { blocksByRoutingKey.push(key, storeBlock); } } } } else { cacheIt = false; } } finally { configLock.writeLock().unlock(); } //Case don't cache it if(!cacheIt) { backDatastore.put(block, data, header, overwrite, isOldBlock); return; } } /** Try to write one block to disk. * @return The number of bytes written to disk if we successfully wrote a block, 0 if we wrote * a block but can't remove it because it changed while we were writing it, and -1 if there * were no blocks to write because the cache is empty. */ long pushLeastRecentlyBlock() { Block<T> block = null; ByteArrayWrapper key = null; configLock.writeLock().lock(); try { block = blocksByRoutingKey.peekValue(); if(block == null) return -1; key = blocksByRoutingKey.peekKey(); } finally { configLock.writeLock().unlock(); } try { backDatastore.put(block.block, block.data, block.header, block.overwrite, block.isOldBlock); } catch (IOException e) { Logger.error(this, "Error in pushAll for CachingFreenetStore: "+e, e); } catch (KeyCollisionException e) { if(logMINOR) Logger.minor(this, "KeyCollisionException in pushAll for CachingFreenetStore: "+e, e); } configLock.writeLock().lock(); try { Block<T> currentVersionOfBlock = blocksByRoutingKey.get(key); /** it might have changed if there was a put() with overwrite=true. * If it has changed, return 0 , i.e. don't remove it*/ if(currentVersionOfBlock != null && currentVersionOfBlock.block.equals(block.block)) { if(blocksByRoutingKey.removeKey(key)) return sizeBlock; } } finally { configLock.writeLock().unlock(); } return 0; } @Override public boolean start(Ticker ticker, boolean longStart) throws IOException { tracker.registerCachingFS(this); return this.backDatastore.start(ticker, longStart); } @Override public void close() { innerClose(); backDatastore.close(); } /** Close this store but not the underlying store. */ private void innerClose() { configLock.writeLock().lock(); try { shuttingDown = true; tracker.unregisterCachingFS(this); } finally { configLock.writeLock().unlock(); } } /** Only for unit tests */ boolean isEmpty() { boolean isEmpty; configLock.readLock().lock(); try { isEmpty = this.blocksByRoutingKey.isEmpty(); } finally { configLock.readLock().unlock(); } return isEmpty; } }