/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.store.saltedhash; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Arrays; import java.util.Deque; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Random; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.tanukisoftware.wrapper.WrapperManager; import freenet.crypt.BlockCipher; import freenet.crypt.DSAPublicKey; import freenet.crypt.UnsupportedCipherException; import freenet.crypt.ciphers.Rijndael; import freenet.keys.KeyVerifyException; import freenet.keys.SSKBlock; import freenet.l10n.NodeL10n; import freenet.node.FastRunnable; import freenet.node.SemiOrderedShutdownHook; import freenet.node.stats.StoreAccessStats; import freenet.node.useralerts.AbstractUserAlert; import freenet.node.useralerts.UserAlert; import freenet.node.useralerts.UserAlertManager; import freenet.store.BlockMetadata; import freenet.store.FreenetStore; import freenet.store.KeyCollisionException; import freenet.store.StorableBlock; import freenet.store.StoreCallback; import freenet.support.Fields; import freenet.support.HTMLNode; import freenet.support.HexUtil; import freenet.support.Logger; import freenet.support.Logger.LogLevel; import freenet.support.Ticker; import freenet.support.io.Closer; import freenet.support.io.FileUtil; import freenet.support.io.NativeThread; import freenet.support.math.MersenneTwister; /** * Index-less data store based on salted hash. * * Provide a pseudo-random replacement based on a salt value generated on create. Keys are check * against a bloom filter before probing. Data are encrypted using the route key and the salt, so * there is no way to recover the data without holding the route key. (For debugging, you can set * OPTION_SAVE_PLAINKEY=true in source code) * * @author sdiz */ public class SaltedHashFreenetStore<T extends StorableBlock> implements FreenetStore<T> { /** Option for saving plainkey. * SECURITY: This should NEVER be enabled for a client-cache! */ private static final boolean OPTION_SAVE_PLAINKEY = false; static final int OPTION_MAX_PROBE = 5; private static final byte FLAG_DIRTY = 0x1; private static final byte FLAG_REBUILD_BLOOM = 0x2; /** Alternative to a Bloom filter which allows us to know exactly which slots to check, * so radically reduces disk I/O even when there is a hit. * * Each slot in a 4 byte integer. * bit 31 - Must be 1. 0 indicates we have not checked this slot so must read the entry. * bit 30 - ENTRY_FLAG_OCCUPIED: 0 = Slot is free, 1 = slot is occupied. * bit 29 - ENTRY_NEW_BLOCK: 0 = Old (pre-1224) or should not be in store, 1 = New and should be in store. * bit 28 - ENTRY_WRONG_STORE: 0 = Stored in correct store, 1 = stored in wrong store. * bit 0...23 - The first 3 bytes of the salted key. */ private final ResizablePersistentIntBuffer slotFilter; /** If true, don't create a slot filter, don't keep it up to date, don't * do anything with it. */ private boolean slotFilterDisabled; /** If true, then treat the slot filter as authoritative. If the slot filter * gives a certain content for a particular slot, assume it is right. This * saves a lot of seeks, both when reading and when writing. Note that the * slot filter will indicate when it doesn't have any information about a * slot, which is the default, which is why it has to be rebuilt on * conversion from an old store. We normally also check slotFilterDisabled * to see whether there *is* a slot filter. */ private static final boolean USE_SLOT_FILTER = true; private static final int SLOT_CHECKED = 1 << 31; private static final int SLOT_OCCUPIED = 1 << 30; private static final int SLOT_NEW_BLOCK = 1 << 29; private static final int SLOT_WRONG_STORE = 1 << 28; private static boolean logMINOR; private static boolean logDEBUG; private final File baseDir; private final String name; private final StoreCallback<T> callback; private final boolean collisionPossible; private final int headerBlockLength; private final int fullKeyLength; private final int dataBlockLength; private final Random random; private final File bloomFile; private long storeSize; private int generation; private int flags; private boolean preallocate = true; public static boolean NO_CLEANER_SLEEP = false; /** If we have no space in this store, try writing it to the alternate store, * with the wrong store flag set. Note that we do not *read from* it, the caller * must do that. IMPORTANT LOCKING NOTE: This must only happen in one direction! * If two stores have altStore set to each other, deadlock is likely! (Infinite * recursion is also possible). However, fortunately we don't need to do it * bidirectionally - the cache needs more space from the store, but the store * grows so slowly it will hardly ever need more space from the cache. */ private SaltedHashFreenetStore<T> altStore; public void setAltStore(SaltedHashFreenetStore<T> store) { if(store.altStore != null) throw new IllegalStateException("Target must not have an altStore - deadlock can result"); altStore = store; } public static <T extends StorableBlock> SaltedHashFreenetStore<T> construct(File baseDir, String name, StoreCallback<T> callback, Random random, long maxKeys, boolean useSlotFilter, SemiOrderedShutdownHook shutdownHook, boolean preallocate, boolean resizeOnStart, Ticker exec, byte[] masterKey) throws IOException { return new SaltedHashFreenetStore<T>(baseDir, name, callback, random, maxKeys, useSlotFilter, shutdownHook, preallocate, resizeOnStart, masterKey); } private SaltedHashFreenetStore(File baseDir, String name, StoreCallback<T> callback, Random random, long maxKeys, boolean enableSlotFilters, SemiOrderedShutdownHook shutdownHook, boolean preallocate, boolean resizeOnStart, byte[] masterKey) throws IOException { logMINOR = Logger.shouldLog(LogLevel.MINOR, this); logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this); this.baseDir = baseDir; this.name = name; this.callback = callback; collisionPossible = callback.collisionPossible(); headerBlockLength = callback.headerLength(); fullKeyLength = callback.fullKeyLength(); dataBlockLength = callback.dataLength(); hdPadding = ((headerBlockLength + dataBlockLength + 512 - 1) & ~(512-1)) - (headerBlockLength + dataBlockLength); this.random = random; storeSize = maxKeys; this.preallocate = preallocate; lockManager = new LockManager(); // Create a directory it not exist this.baseDir.mkdirs(); if(storeSize > Integer.MAX_VALUE) // FIXME 64-bit. throw new IllegalArgumentException("Store size over MAXINT not supported due to ResizablePersistentIntBuffer limitations."); configFile = new File(this.baseDir, name + ".config"); boolean newStore = loadConfigFile(masterKey); if(storeSize != 0 && storeSize != maxKeys && prevStoreSize == 0) { // If not already resizing, start resizing to the new store size. prevStoreSize = storeSize; storeSize = maxKeys; writeConfigFile(); } newStore |= openStoreFiles(baseDir, name); bloomFile = new File(this.baseDir, name + ".bloom"); if(bloomFile.exists()) { bloomFile.delete(); System.err.println("Deleted old bloom filter for "+name+" - obsoleted by slot filter"); System.err.println("We will need to rebuild the slot filters, it will take a while and there will be a lot of disk access, but once it's done there should be a lot less disk access."); } File slotFilterFile = new File(this.baseDir, name + ".slotfilter"); int size = (int)Math.max(storeSize, prevStoreSize); slotFilterDisabled = !enableSlotFilters; if(!slotFilterDisabled) { slotFilter = new ResizablePersistentIntBuffer(slotFilterFile, size); System.err.println("Slot filter (" + slotFilterFile + ") for " + name + " is loaded (new="+slotFilter.isNew()+")."); if(newStore && slotFilter.isNew()) slotFilter.fill(SLOT_CHECKED); } else { if(slotFilterFile.exists()) { if(slotFilterFile.delete()) { System.err.println("Old slot filter file deleted as slot filters are disabled, keeping it might cause data loss when they are turned back on."); } else { System.err.println("Old slot filter file "+slotFilterFile+" could not be deleted. If you turn on slot filters later you might lose data from your datastore. Please delete it manually."); } } slotFilter = null; } if ((flags & FLAG_DIRTY) != 0) System.err.println("Datastore(" + name + ") is dirty."); flags |= FLAG_DIRTY; // datastore is now dirty until flushAndClose() writeConfigFile(); callback.setStore(this); shutdownHook.addEarlyJob(new NativeThread(new ShutdownDB(), "Shutdown salted hash store", NativeThread.HIGH_PRIORITY, true)); cleanerThread = new Cleaner(); cleanerStatusUserAlert = new CleanerStatusUserAlert(cleanerThread); // finish all resizing before continue if (resizeOnStart && prevStoreSize != 0 && cleanerGlobalLock.tryLock()) { System.out.println("Resizing datastore (" + name + ")"); try { cleanerThread.resizeStore(prevStoreSize, false); } finally { cleanerGlobalLock.unlock(); } writeConfigFile(); } if(((!slotFilterDisabled) && slotFilter.isNew()) && !newStore) { flags |= FLAG_REBUILD_BLOOM; System.out.println("Rebuilding slot filter because new"); } else if((flags & FLAG_REBUILD_BLOOM) != 0) System.out.println("Slot filter still needs rebuilding"); } private boolean started = false; /** If start can be completed quickly, or longStart is true, then do it. * If longStart is false and start cannot be completed quickly, return * true. Don't start twice. * @throws IOException */ public boolean start(Ticker ticker, boolean longStart) throws IOException { if(started) return true; if(!slotFilterDisabled) slotFilter.start(ticker); long curStoreFileSize = hdRAF.length(); long curMetaFileSize = metaRAF.length(); // If prevStoreSize is nonzero, that means that we are either shrinking or // growing. Either way, the file size should be between the old size and the // new size. If it is not, we should pad it until it is. long smallerSize = storeSize; if(prevStoreSize < storeSize && prevStoreSize > 0) smallerSize = prevStoreSize; if((smallerSize * (headerBlockLength + dataBlockLength + hdPadding) > curStoreFileSize) || (smallerSize * Entry.METADATA_LENGTH > curMetaFileSize)) { // Pad it up to the minimum size before proceeding. if(longStart) { setStoreFileSize(storeSize, true); curStoreFileSize = hdRAF.length(); curMetaFileSize = metaRAF.length(); } else return true; } // Otherwise the resize will be completed by the Cleaner thread. // However, we do still need to set storeFileOffsetReady storeFileOffsetReady = Math.min(curStoreFileSize / (headerBlockLength + dataBlockLength + hdPadding), curMetaFileSize / Entry.METADATA_LENGTH); if(ticker == null) { cleanerThread.start(); } else ticker.queueTimedJob(new FastRunnable() { @Override public void run() { cleanerThread.start(); } }, "Start cleaner thread", 0, true, false); started = true; return false; } @Override public T fetch(byte[] routingKey, byte[] fullKey, boolean dontPromote, boolean canReadClientCache, boolean canReadSlashdotCache, boolean ignoreOldBlocks, BlockMetadata meta) throws IOException { if (logMINOR) Logger.minor(this, "Fetch " + HexUtil.bytesToHex(routingKey) + " for " + callback); try { int retry = 0; while (!configLock.readLock().tryLock(2, TimeUnit.SECONDS)) { if (shutdown) return null; if (retry++ > 10) throw new IOException("lock timeout (20s)"); } } catch(InterruptedException e) { throw new IOException("interrupted: " +e); } byte[] digestedKey = cipherManager.getDigestedKey(routingKey); try { Map<Long, Condition> lockMap = lockDigestedKey(digestedKey, true); if (lockMap == null) { if (logDEBUG) Logger.debug(this, "cannot lock key: " + HexUtil.bytesToHex(routingKey) + ", shutting down?"); return null; } try { Entry entry = probeEntry(digestedKey, routingKey, true); if (entry == null) { misses.incrementAndGet(); return null; } if((entry.flag & Entry.ENTRY_NEW_BLOCK) == 0) { if(ignoreOldBlocks) { Logger.normal(this, "Ignoring old block"); return null; } if(meta != null) meta.setOldBlock(); } try { T block = entry.getStorableBlock(routingKey, fullKey, canReadClientCache, canReadSlashdotCache, meta, null); if (block == null) { misses.incrementAndGet(); return null; } hits.incrementAndGet(); return block; } catch (KeyVerifyException e) { Logger.minor(this, "key verification exception", e); misses.incrementAndGet(); return null; } } finally { unlockDigestedKey(digestedKey, true, lockMap); } } finally { configLock.readLock().unlock(); } } /** * Find and lock an entry with a specific routing key. This function would <strong>not</strong> * lock the entries. * * @param routingKey * @param withData * @return <code>Entry</code> object * @throws IOException */ private Entry probeEntry(byte[] digestedKey, byte[] routingKey, boolean withData) throws IOException { Entry entry = probeEntry0(digestedKey, routingKey, storeSize, withData); if (entry == null && prevStoreSize != 0) entry = probeEntry0(digestedKey, routingKey, prevStoreSize, withData); return entry; } private Entry probeEntry0(byte[] digestedKey, byte[] routingKey, long probeStoreSize, boolean withData) throws IOException { Entry entry = null; long[] offset = getOffsetFromDigestedKey(digestedKey, probeStoreSize); for (int i = 0; i < offset.length; i++) { if (logDEBUG) Logger.debug(this, "probing for i=" + i + ", offset=" + offset[i]); try { if(storeFileOffsetReady == -1 || offset[i] < this.storeFileOffsetReady) { entry = readEntry(offset[i], digestedKey, routingKey, withData); if (entry != null) return entry; } } catch (EOFException e) { if (prevStoreSize == 0) // may occur on store shrinking Logger.error(this, "EOFException on probeEntry", e); continue; } } return null; } @Override public void put(T block, byte[] data, byte[] header, boolean overwrite, boolean isOldBlock) throws IOException, KeyCollisionException { put(block, data, header, overwrite, isOldBlock, false); } public boolean put(T block, byte[] data, byte[] header, boolean overwrite, boolean isOldBlock, boolean wrongStore) throws IOException, KeyCollisionException { byte[] routingKey = block.getRoutingKey(); byte[] fullKey = block.getFullKey(); if (logMINOR) Logger.minor(this, "Putting " + HexUtil.bytesToHex(routingKey) + " (" + name + ")"); try { int retry = 0; while (!configLock.readLock().tryLock(2, TimeUnit.SECONDS)) { if (shutdown) return true; if (retry++ > 10) throw new IOException("lock timeout (20s)"); } } catch(InterruptedException e) { throw new IOException("interrupted: " +e); } byte[] digestedKey = cipherManager.getDigestedKey(routingKey); try { Map<Long, Condition> lockMap = lockDigestedKey(digestedKey, false); if (lockMap == null) { if (logDEBUG) Logger.debug(this, "cannot lock key: " + HexUtil.bytesToHex(routingKey) + ", shutting down?"); return false; } try { /* * Use lazy loading here. This may lost data if digestedRoutingKey collide but * collisionPossible is false. Should be very rare as digestedRoutingKey is a * SHA-256 hash. */ Entry oldEntry = probeEntry(digestedKey, routingKey, false); if (oldEntry != null && !oldEntry.isFree()) { long oldOffset = oldEntry.curOffset; try { if (!collisionPossible) { if((oldEntry.flag & Entry.ENTRY_NEW_BLOCK) == 0 && !isOldBlock) { oldEntry = readEntry(oldEntry.curOffset, digestedKey, routingKey, true); // Currently flagged as an old block oldEntry.flag |= Entry.ENTRY_NEW_BLOCK; if(logMINOR) Logger.minor(this, "Setting old block to new block"); oldEntry.storeSize = storeSize; writeEntry(oldEntry, digestedKey, oldOffset); } return true; } oldEntry.setHD(readHD(oldOffset)); // read from disk T oldBlock = oldEntry.getStorableBlock(routingKey, fullKey, false, false, null, (block instanceof SSKBlock) ? ((SSKBlock)block).getPubKey() : null); if (block.equals(oldBlock)) { if(logDEBUG) Logger.debug(this, "Block already stored"); if((oldEntry.flag & Entry.ENTRY_NEW_BLOCK) == 0 && !isOldBlock) { // Currently flagged as an old block oldEntry.flag |= Entry.ENTRY_NEW_BLOCK; if(logMINOR) Logger.minor(this, "Setting old block to new block"); oldEntry.storeSize = storeSize; writeEntry(oldEntry, digestedKey, oldOffset); } return false; // already in store } else if (!overwrite) { throw new KeyCollisionException(); } } catch (KeyVerifyException e) { // ignore } // Overwrite old offset with same key Entry entry = new Entry(routingKey, header, data, !isOldBlock, wrongStore); writeEntry(entry, digestedKey, oldOffset); if (oldEntry.generation != generation) keyCount.incrementAndGet(); return true; } Entry entry = new Entry(routingKey, header, data, !isOldBlock, wrongStore); long[] offset = entry.getOffset(); int firstWrongStoreIndex = -1; int wrongStoreCount = 0; for (int i = 0; i < offset.length; i++) { if(offset[i] < storeFileOffsetReady) { long flag = getFlag(offset[i], false); if((flag & Entry.ENTRY_FLAG_OCCUPIED) == 0) { // write to free block if (logDEBUG) Logger.debug(this, "probing, write to i=" + i + ", offset=" + offset[i]); writeEntry(entry, digestedKey, offset[i]); keyCount.incrementAndGet(); onWrite(); return true; } else if(((flag & Entry.ENTRY_WRONG_STORE) == Entry.ENTRY_WRONG_STORE)) { if (wrongStoreCount == 0) firstWrongStoreIndex = i; wrongStoreCount++; } } } if((!wrongStore) && altStore != null) { if(altStore.put(block, data, header, overwrite, isOldBlock, true)) { if(logMINOR) Logger.minor(this, "Successfully wrote block to wrong store "+altStore+" on "+this); return true; } else { if(logMINOR) Logger.minor(this, "Writing to wrong store "+altStore+" on "+this+" failed"); } } // There are no free slots for this Entry, so some slot will have to get overwritten. int indexToOverwrite = -1; if(wrongStore) { // Distribute overwrites evenly between the right store and the wrong store. int a = OPTION_MAX_PROBE; int b = wrongStoreCount; if(random.nextInt(a+b) < b) // Allow the overwrite to happen in the wrong store. indexToOverwrite = firstWrongStoreIndex; else // Force the overwrite to happen in the right store. return false; } else { // By default, overwrite offset[0] when not writing to wrong store. indexToOverwrite = 0; } // Do the overwriting. if (logDEBUG) Logger.debug(this, "collision, write to i=" + indexToOverwrite + ", offset=" + offset[indexToOverwrite]); oldEntry = readEntry(offset[indexToOverwrite], null, null, false); writeEntry(entry, digestedKey, offset[indexToOverwrite]); if (oldEntry.generation != generation) keyCount.incrementAndGet(); onWrite(); return true; } finally { unlockDigestedKey(digestedKey, false, lockMap); } } finally { configLock.readLock().unlock(); } } private boolean onWrite() { return (writes.incrementAndGet() % (storeSize*2) == 0); } // ------------- Entry I/O // meta-data file private File metaFile; private RandomAccessFile metaRAF; private FileChannel metaFC; // header+data file private File hdFile; private RandomAccessFile hdRAF; private FileChannel hdFC; private final int hdPadding; /** * Data entry * * <pre> * META-DATA BLOCK * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F| * +----+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |0000| | * +----+ Digested Routing Key | * |0010| | * +----+-------------------------------+ * |0020| Data Encrypt IV | * +----+---------------+---------------+ * |0030| Flag | Store Size | * +----+---------------+---------------+ * |0040| Plain Routing Key | * |0050| (Only if ENTRY_FLAG_PLAINKEY) | * +----+-------+-----------------------+ * |0060| Gen | Reserved | * +----+-------+-----------------------+ * |0070| Reserved | * +----+-------------------------------+ * * Gen = Generation * </pre> */ class Entry { /** Flag for occupied space */ private final static long ENTRY_FLAG_OCCUPIED = 0x00000001L; /** Flag for plain key available */ private final static long ENTRY_FLAG_PLAINKEY = 0x00000002L; /** Flag for block added after we stopped caching local (and high htl) requests */ private final static long ENTRY_NEW_BLOCK = 0x00000004L; /** Flag set if the block was stored in the wrong datastore i.e. store instead of cache */ private final static long ENTRY_WRONG_STORE = 0x00000008L; /** Control block length */ private static final int METADATA_LENGTH = 0x80; byte[] plainRoutingKey; byte[] digestedRoutingKey; byte[] dataEncryptIV; private long flag; private long storeSize; private int generation; byte[] header; byte[] data; boolean isEncrypted; private long curOffset = -1; private Entry() { } private Entry(ByteBuffer metaDataBuf, ByteBuffer hdBuf) { assert metaDataBuf.remaining() == METADATA_LENGTH; digestedRoutingKey = new byte[0x20]; metaDataBuf.get(digestedRoutingKey); dataEncryptIV = new byte[0x10]; metaDataBuf.get(dataEncryptIV); flag = metaDataBuf.getLong(); storeSize = metaDataBuf.getLong(); if ((flag & ENTRY_FLAG_PLAINKEY) != 0) { plainRoutingKey = new byte[0x20]; metaDataBuf.get(plainRoutingKey); } metaDataBuf.position(0x60); generation = metaDataBuf.getInt(); isEncrypted = true; if (hdBuf != null) setHD(hdBuf); } /** * Set header/data after construction. * * @param storeBuf * @param store */ private void setHD(ByteBuffer hdBuf) { assert hdBuf.remaining() == headerBlockLength + dataBlockLength + hdPadding; assert isEncrypted; header = new byte[headerBlockLength]; hdBuf.get(header); data = new byte[dataBlockLength]; hdBuf.get(data); } /** * Create a new entry * * @param plainRoutingKey * @param header * @param data */ private Entry(byte[] plainRoutingKey, byte[] header, byte[] data, boolean newBlock, boolean wrongStore) { this.plainRoutingKey = plainRoutingKey; flag = ENTRY_FLAG_OCCUPIED; if(newBlock) flag |= ENTRY_NEW_BLOCK; if(wrongStore) flag |= ENTRY_WRONG_STORE; this.storeSize = SaltedHashFreenetStore.this.storeSize; this.generation = SaltedHashFreenetStore.this.generation; // header/data will be overwritten in encrypt()/decrypt(), // let's make a copy here this.header = Arrays.copyOf(header, headerBlockLength); this.data = Arrays.copyOf(data, dataBlockLength); if (OPTION_SAVE_PLAINKEY) { flag |= ENTRY_FLAG_PLAINKEY; } isEncrypted = false; } private ByteBuffer toMetaDataBuffer() { ByteBuffer out = ByteBuffer.allocate(METADATA_LENGTH); cipherManager.encrypt(this, random); out.put(getDigestedRoutingKey()); out.put(dataEncryptIV); out.putLong(flag); out.putLong(storeSize); if ((flag & ENTRY_FLAG_PLAINKEY) != 0 && plainRoutingKey != null) { assert plainRoutingKey.length == 0x20; out.put(plainRoutingKey); } out.position(0x60); out.putInt(generation); out.position(0); return out; } private ByteBuffer toHDBuffer() { assert isEncrypted; // should have encrypted to get dataEncryptIV in control buffer assert header.length == headerBlockLength; assert data.length == dataBlockLength; if (header == null || data == null) return null; ByteBuffer out = ByteBuffer.allocate(headerBlockLength + dataBlockLength + hdPadding); out.put(header); out.put(data); out.position(0); return out; } private T getStorableBlock(byte[] routingKey, byte[] fullKey, boolean canReadClientCache, boolean canReadSlashdotCache, BlockMetadata meta, DSAPublicKey knownKey) throws KeyVerifyException { if (isFree() || header == null || data == null) return null; // this is a free block if (!cipherManager.decrypt(this, routingKey)) return null; T block = callback.construct(data, header, routingKey, fullKey, canReadClientCache, canReadSlashdotCache, meta, knownKey); byte[] blockRoutingKey = block.getRoutingKey(); if (!Arrays.equals(blockRoutingKey, routingKey)) { // can't recover, as decrypt() depends on a correct route key return null; } return block; } private long[] getOffset() { if (digestedRoutingKey != null) return getOffsetFromDigestedKey(digestedRoutingKey, storeSize); else return getOffsetFromPlainKey(plainRoutingKey, storeSize); } private boolean isFree() { return (flag & ENTRY_FLAG_OCCUPIED) == 0; } byte[] getDigestedRoutingKey() { if (digestedRoutingKey == null) if (plainRoutingKey == null) return null; else digestedRoutingKey = cipherManager.getDigestedKey(plainRoutingKey); return digestedRoutingKey; } public int getSlotFilterEntry(byte[] digestedRoutingKey, long flags) { int value = (digestedRoutingKey[2] & 0xFF) + ((digestedRoutingKey[1] & 0xFF) << 8) + ((digestedRoutingKey[0] & 0xFF) << 16); value |= SLOT_CHECKED; if((flags & ENTRY_FLAG_OCCUPIED) != 0) value |= SLOT_OCCUPIED; if((flags & ENTRY_NEW_BLOCK) != 0) value |= SLOT_NEW_BLOCK; if((flags & ENTRY_WRONG_STORE) != 0) value |= SLOT_WRONG_STORE; return value; } public int getSlotFilterEntry() { return getSlotFilterEntry(getDigestedRoutingKey(), flag); } } public boolean slotCacheLikelyMatch(int value, byte[] digestedRoutingKey) { if((value & (SLOT_CHECKED)) == 0) return false; if((value & (SLOT_OCCUPIED)) == 0) return false; int wanted = (digestedRoutingKey[2] & 0xFF) + ((digestedRoutingKey[1] & 0xFF) << 8) + ((digestedRoutingKey[0] & 0xFF) << 16); int got = value & 0xFFFFFF; return wanted == got; } private long translateSlotFlagsToEntryFlags(int cache) { long ret = 0; if((cache & SLOT_OCCUPIED) != 0) ret |= Entry.ENTRY_FLAG_OCCUPIED; if((cache & SLOT_NEW_BLOCK) != 0) ret |= Entry.ENTRY_NEW_BLOCK; if((cache & SLOT_WRONG_STORE) != 0) ret |= Entry.ENTRY_WRONG_STORE; return ret; } private boolean slotCacheIsFree(int value) { return (value & SLOT_OCCUPIED) == 0; } private volatile long storeFileOffsetReady = -1; /** * Open all store files * * @param baseDir * @param name * @throws IOException * @return <code>true</code> iff this is a new datastore */ private boolean openStoreFiles(File baseDir, String name) throws IOException { metaFile = new File(baseDir, name + ".metadata"); hdFile = new File(baseDir, name + ".hd"); boolean newStore = !metaFile.exists() || !hdFile.exists(); metaRAF = new RandomAccessFile(metaFile, "rw"); metaFC = metaRAF.getChannel(); metaFC.lock(); hdRAF = new RandomAccessFile(hdFile, "rw"); hdFC = hdRAF.getChannel(); hdFC.lock(); return newStore; } /** * Read entry from disk. Before calling this function, you should acquire all required locks. * * @return <code>null</code> if and only if <code>routingKey</code> is not <code>null</code> and * the key does not match the entry. */ private Entry readEntry(long offset, byte[] digestedRoutingKey, byte[] routingKey, boolean withData) throws IOException { if(offset >= Integer.MAX_VALUE) throw new IllegalArgumentException(); int cache = 0; boolean validCache = false; boolean likelyMatch = false; if(digestedRoutingKey != null && !slotFilterDisabled) { cache = slotFilter.get((int)offset); validCache = (cache & SLOT_CHECKED) != 0; likelyMatch = slotCacheLikelyMatch(cache, digestedRoutingKey); if(USE_SLOT_FILTER && validCache && !likelyMatch) return null; } if(validCache && logMINOR) { if(likelyMatch) Logger.minor(this, "Likely match"); else Logger.minor(this, "Unlikely match"); } ByteBuffer mbf = ByteBuffer.allocate(Entry.METADATA_LENGTH); do { int status = metaFC.read(mbf, Entry.METADATA_LENGTH * offset + mbf.position()); if (status == -1) { Logger.error(this, "Failed to access offset "+offset, new Exception("error")); throw new EOFException(); } } while (mbf.hasRemaining()); mbf.flip(); Entry entry = new Entry(mbf, null); entry.curOffset = offset; byte[] slotDigestedRoutingKey = entry.digestedRoutingKey; int trueCache = entry.getSlotFilterEntry(); if(trueCache != cache && !slotFilterDisabled) { if(validCache) Logger.error(this, "Slot cache has changed for slot "+offset+" from "+cache+" to "+trueCache); slotFilter.put((int)offset, trueCache); } if (routingKey != null) { if (entry.isFree()) { if(validCache && !likelyMatch && !slotCacheIsFree(cache)) { Logger.error(this, "Slot falsely identified as non-free on slot "+offset+" cache was "+cache); bloomFalsePos.incrementAndGet(); } else if(logMINOR && validCache && !likelyMatch && slotCacheIsFree(cache)) Logger.minor(this, "True negative!"); return null; } if (!Arrays.equals(digestedRoutingKey, slotDigestedRoutingKey)) { if(validCache && likelyMatch) { Logger.normal(this, "False positive from slot cache on slot "+offset+" cache was "+cache); bloomFalsePos.incrementAndGet(); } else if(logMINOR && validCache && !likelyMatch) Logger.minor(this, "True negative!"); return null; } if(validCache && !likelyMatch) { Logger.error(this, "False NEGATIVE from slot cache on slot "+offset+" cache was "+cache); bloomFalsePos.incrementAndGet(); } if (withData) { ByteBuffer hdBuf = readHD(offset); entry.setHD(hdBuf); boolean decrypted = cipherManager.decrypt(entry, routingKey); if (!decrypted) { if(logMINOR && validCache && likelyMatch) Logger.minor(this, "True positive but decrypt failed on slot "+offset+" cache was "+cache); return null; } else { if(logMINOR && validCache && likelyMatch) Logger.minor(this, "True positive!"); } } } return entry; } /** * Read header + data from disk * * @param offset * @throws IOException */ private ByteBuffer readHD(long offset) throws IOException { ByteBuffer buf = ByteBuffer.allocate(headerBlockLength + dataBlockLength + hdPadding); long pos = (headerBlockLength + dataBlockLength + hdPadding) * offset; do { int status = hdFC.read(buf, pos + buf.position()); if (status == -1) throw new EOFException(); } while (buf.hasRemaining()); buf.flip(); return buf; } /** Get the flags for a slot. Tries to use the slot filter if possible. However, the * ENTRY_FLAG_PLAINKEY flag is not included in the slot filter, so it won't contain * that one. * @param offset * @param forceReadEntry * @return * @throws IOException */ private long getFlag(long offset, boolean forceReadEntry) throws IOException { if((!forceReadEntry) && (!slotFilterDisabled) && USE_SLOT_FILTER) { int cache = slotFilter.get((int)offset); if((cache & SLOT_CHECKED) != 0) { return translateSlotFlagsToEntryFlags(cache); } } Entry entry = readEntry(offset, null, null, false); return entry.flag; } private boolean isFree(long offset) throws IOException { if((!slotFilterDisabled) && USE_SLOT_FILTER) { int cache = slotFilter.get((int)offset); if((cache & SLOT_CHECKED) != 0) { return slotCacheIsFree(cache); } } Entry entry = readEntry(offset, null, null, false); return entry.isFree(); } private byte[] getDigestedKeyFromOffset(long offset) throws IOException { Entry entry = readEntry(offset, null, null, false); return entry.getDigestedRoutingKey(); } /** * Write entry to disk. * * Before calling this function, you should: * <ul> * <li>acquire all required locks</li> * <li>update the entry with latest store size</li> * </ul> */ private void writeEntry(Entry entry, byte[] digestedRoutingKey, long offset) throws IOException { if(offset >= Integer.MAX_VALUE) throw new IllegalArgumentException(); if(!slotFilterDisabled) slotFilter.put((int)offset, entry.getSlotFilterEntry(digestedRoutingKey, entry.flag)); cipherManager.encrypt(entry, random); ByteBuffer bf = entry.toMetaDataBuffer(); do { int status = metaFC.write(bf, Entry.METADATA_LENGTH * offset + bf.position()); if (status == -1) throw new EOFException(); } while (bf.hasRemaining()); bf = entry.toHDBuffer(); if (bf != null) { long pos = (headerBlockLength + dataBlockLength + hdPadding) * offset; do { int status = hdFC.write(bf, pos + bf.position()); if (status == -1) throw new EOFException(); } while (bf.hasRemaining()); } entry.curOffset = offset; } private void flushAndClose(boolean abort) { Logger.normal(this, "Flush and closing this store: " + name); try { metaFC.force(true); metaFC.close(); } catch (Exception e) { Logger.error(this, "error flusing store", e); } try { hdFC.force(true); hdFC.close(); } catch (Exception e) { Logger.error(this, "error flusing store", e); } if(!slotFilterDisabled) { if(!abort) slotFilter.shutdown(); else slotFilter.abort(); } } /** * Set preallocate storage space * @param preallocate */ public void setPreallocate(boolean preallocate) { this.preallocate = preallocate; } /** * Change on disk store file size * * @param storeMaxEntries */ private void setStoreFileSize(long storeMaxEntries, boolean starting) { try { long oldMetaLen = metaRAF.length(); long currentHdLen = hdRAF.length(); final long newMetaLen = Entry.METADATA_LENGTH * storeMaxEntries; final long newHdLen = (headerBlockLength + dataBlockLength + hdPadding) * storeMaxEntries; if (preallocate && (oldMetaLen < newMetaLen || currentHdLen < newHdLen)) { /* * Fill the store file with random data. This won't be compressed, unlike filling it with zeros. * So the disk space usage of the node will not change (apart from temp files). * * Note that MersenneTwister is *not* cryptographically secure, in fact from 2.4KB of output you * can predict the rest of the stream! This is okay because an attacker knows which blocks are * occupied anyway; it is essential to label them to get good data retention on resizing etc. * * On my test system (phenom 2.2GHz), this does approx 80MB/sec. If I reseed every 2kB from an * AES CTR, which is pointless as I just explained, it does 40MB/sec. */ byte[] b = new byte[4096]; ByteBuffer bf = ByteBuffer.wrap(b); // start from next 4KB boundary => align to x86 page size oldMetaLen = (oldMetaLen + 4096 - 1) & ~(4096 - 1); currentHdLen = (currentHdLen + 4096 - 1) & ~(4096 - 1); storeFileOffsetReady = -1; // this may write excess the size, the setLength() would fix it while (oldMetaLen < newMetaLen) { // never write random byte to meta data! // this would screw up the isFree() function bf.rewind(); metaFC.write(bf, oldMetaLen); oldMetaLen += 4096; } byte[] seed = new byte[64]; random.nextBytes(seed); Random mt = new MersenneTwister(seed); int x = 0; while (currentHdLen < newHdLen) { mt.nextBytes(b); bf.rewind(); hdFC.write(bf, currentHdLen); currentHdLen += 4096; if(currentHdLen % (1024*1024*1024L) == 0) { random.nextBytes(seed); mt = new MersenneTwister(seed); if (starting) { WrapperManager.signalStarting((int) MINUTES.toMillis(5)); if ( x++ % 32 == 0 ) System.err.println("Preallocating space for " + name + ": " + currentHdLen + "/" + newHdLen); } } storeFileOffsetReady = currentHdLen / (headerBlockLength + dataBlockLength + hdPadding); } } storeFileOffsetReady = 1 + storeMaxEntries; metaRAF.setLength(newMetaLen); hdRAF.setLength(newHdLen); } catch (IOException e) { Logger.error(this, "error resizing store file", e); } } // ------------- Configuration /** * Configuration File * * <pre> * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F| * +----+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |0000| Salt | * +----+---------------+---------------+ * |0010| Store Size | prevStoreSize | * +----+---------------+-------+-------+ * |0020| Est Key Count | Gen | Flags | * +----+-------+-------+-------+-------+ * |0030| K | (reserved) | * +----+-------+-------+---------------+ * |0040| writes | hits | * +----+---------------+---------------+ * |0050| misses | bloomFalsePos | * +----+---------------+---------------+ * * Gen = Generation * K = K for bloom filter * </pre> */ private final File configFile; /** * Load config file * @param masterKey * * @return <code>true</code> iff this is a new datastore */ private boolean loadConfigFile(byte[] masterKey) throws IOException { assert cipherManager == null; // never load the configuration twice if (!configFile.exists()) { // create new byte[] newsalt = new byte[0x10]; random.nextBytes(newsalt); byte[] diskSalt = newsalt; if(masterKey != null) { BlockCipher cipher; try { cipher = new Rijndael(256, 128); } catch (UnsupportedCipherException e) { throw new Error("Impossible: no Rijndael(256,128): "+e, e); } cipher.initialize(masterKey); diskSalt = new byte[0x10]; cipher.encipher(newsalt, diskSalt); if(logDEBUG) Logger.debug(this, "Encrypting with "+HexUtil.bytesToHex(newsalt)+" from "+HexUtil.bytesToHex(diskSalt)); } cipherManager = new CipherManager(newsalt, diskSalt); writeConfigFile(); return true; } else { try { // try to load RandomAccessFile raf = new RandomAccessFile(configFile, "r"); try { byte[] salt = new byte[0x10]; raf.readFully(salt); byte[] diskSalt = salt; if(masterKey != null) { BlockCipher cipher; try { cipher = new Rijndael(256, 128); } catch (UnsupportedCipherException e) { throw new Error("Impossible: no Rijndael(256,128): "+e, e); } cipher.initialize(masterKey); salt = new byte[0x10]; cipher.decipher(diskSalt, salt); if(logDEBUG) Logger.debug(this, "Encrypting (new) with "+HexUtil.bytesToHex(salt)+" from "+HexUtil.bytesToHex(diskSalt)); } cipherManager = new CipherManager(salt, diskSalt); storeSize = raf.readLong(); if(storeSize <= 0) throw new IOException("Bogus datastore size"); prevStoreSize = raf.readLong(); keyCount.set(raf.readLong()); generation = raf.readInt(); flags = raf.readInt(); if (((flags & FLAG_DIRTY) != 0) && // FIXME figure out a way to do this consistently! // Not critical as a few blocks wrong is something we can handle. ResizablePersistentIntBuffer.getPersistenceTime() != -1) flags |= FLAG_REBUILD_BLOOM; try { raf.readInt(); // bloomFilterK raf.readInt(); // reserved raf.readLong(); // reserved long w = raf.readLong(); writes.set(w); initialWrites = w; Logger.normal(this, "Set writes to saved value "+w); hits.set(raf.readLong()); initialHits = hits.get(); misses.set(raf.readLong()); initialMisses = misses.get(); bloomFalsePos.set(raf.readLong()); initialBloomFalsePos = bloomFalsePos.get(); } catch (EOFException e) { // Ignore, back compatibility. } return false; } finally { Closer.close(raf); } } catch (IOException e) { // corrupted? delete it and try again Logger.error(this, "config file corrupted, trying to create a new store: " + name, e); System.err.println("config file corrupted, trying to create a new store: " + name); if (configFile.exists() && configFile.delete()) { File metaFile = new File(baseDir, name + ".metadata"); metaFile.delete(); return loadConfigFile(masterKey); } // last restore Logger.error(this, "can't delete config file, please delete the store manually: " + name, e); System.err.println( "can't delete config file, please delete the store manually: " + name); throw e; } } } /** * Write config file */ private void writeConfigFile() { configLock.writeLock().lock(); try { File tempConfig = new File(configFile.getPath() + ".tmp"); RandomAccessFile raf = new RandomAccessFile(tempConfig, "rw"); raf.seek(0); raf.write(cipherManager.getDiskSalt()); raf.writeLong(storeSize); raf.writeLong(prevStoreSize); raf.writeLong(keyCount.get()); raf.writeInt(generation); raf.writeInt(flags); raf.writeInt(0); // bloomFilterK raf.writeInt(0); raf.writeLong(0); raf.writeLong(writes.get()); raf.writeLong(hits.get()); raf.writeLong(misses.get()); raf.writeLong(bloomFalsePos.get()); raf.getFD().sync(); raf.close(); FileUtil.renameTo(tempConfig, configFile); } catch (IOException ioe) { Logger.error(this, "error writing config file for " + name, ioe); } finally { configLock.writeLock().unlock(); } } // ------------- Store resizing private long prevStoreSize = 0; private Lock cleanerLock = new ReentrantLock(); // local to this datastore private Condition cleanerCondition = cleanerLock.newCondition(); private static Lock cleanerGlobalLock = new ReentrantLock(); // global across all datastore private Cleaner cleanerThread; private CleanerStatusUserAlert cleanerStatusUserAlert; private final Entry NOT_MODIFIED = new Entry(); private interface BatchProcessor<T extends StorableBlock> { // initialize void init(); // call this after reading RESIZE_MEMORY_ENTRIES entries // return false to abort boolean batch(long entriesLeft); // call this on abort (e.g. node shutdown) void abort(); void finish(); // return <code>null</code> to free the entry // return NOT_MODIFIED to keep the old entry SaltedHashFreenetStore<T>.Entry process(SaltedHashFreenetStore<T>.Entry entry); /** Does this batch processor want to see free entries? */ boolean wantFreeEntries(); } private class Cleaner extends NativeThread { /** * How often the clean should run */ private static final int CLEANER_PERIOD = 5 * 60 * 1000; // 5 minutes private volatile boolean isRebuilding; private volatile boolean isResizing; public Cleaner() { super("Store-" + name + "-Cleaner", NativeThread.LOW_PRIORITY, false); setPriority(MIN_PRIORITY); setDaemon(true); } @Override public void realRun() { if(!NO_CLEANER_SLEEP) { try { Thread.sleep((int)(CLEANER_PERIOD / 2 + CLEANER_PERIOD * Math.random())); } catch (InterruptedException e){} } if (shutdown) return; while (!shutdown) { cleanerLock.lock(); try { long _prevStoreSize; configLock.readLock().lock(); try { _prevStoreSize = prevStoreSize; } finally { configLock.readLock().unlock(); } if (_prevStoreSize != 0 && cleanerGlobalLock.tryLock()) { try { isResizing = true; resizeStore(_prevStoreSize, true); } finally { isResizing = false; cleanerGlobalLock.unlock(); } } boolean _rebuildBloom; configLock.readLock().lock(); try { _rebuildBloom = ((flags & FLAG_REBUILD_BLOOM) != 0); } finally { configLock.readLock().unlock(); } if (_rebuildBloom && prevStoreSize == 0 && cleanerGlobalLock.tryLock()) { try { isRebuilding = true; rebuildBloom(false); } finally { isRebuilding = false; cleanerGlobalLock.unlock(); } } writeConfigFile(); try { cleanerCondition.await(CLEANER_PERIOD, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Logger.debug(this, "interrupted", e); } } finally { cleanerLock.unlock(); } } } private static final int RESIZE_MEMORY_ENTRIES = 128; // temporary memory store size (in # of entries) /** * Move old entries to new location and resize store */ private void resizeStore(final long _prevStoreSize, final boolean sleep) { Logger.normal(this, "Starting datastore resize"); System.out.println("Resizing datastore "+name); BatchProcessor<T> resizeProcesser = new BatchProcessor<T>() { Deque<Entry> oldEntryList = new LinkedList<Entry>(); @Override public void init() { if (storeSize > _prevStoreSize) setStoreFileSize(storeSize, false); configLock.writeLock().lock(); try { generation++; keyCount.set(0); } finally { configLock.writeLock().unlock(); } WrapperManager.signalStarting((int) (RESIZE_MEMORY_ENTRIES * SECONDS.toMillis(30) + SECONDS.toMillis(1))); } @Override public Entry process(Entry entry) { int oldGeneration = entry.generation; if (oldGeneration != generation) { entry.generation = generation; keyCount.incrementAndGet(); } if (entry.storeSize == storeSize) { // new size, don't have to relocate if (entry.generation != generation) { return entry; } else { return NOT_MODIFIED; } } // remove from store, prepare for relocation if (oldGeneration == generation) { // should be impossible Logger.error(this, // "new generation object with wrong storeSize. DigestedRoutingKey=" // + HexUtil.bytesToHex(entry.getDigestedRoutingKey()) // + ", Offset=" + entry.curOffset); } try { entry.setHD(readHD(entry.curOffset)); oldEntryList.add(entry); if (oldEntryList.size() > RESIZE_MEMORY_ENTRIES) oldEntryList.poll(); } catch (IOException e) { Logger.error(this, "error reading entry (offset=" + entry.curOffset + ")", e); } return null; } int i = 0; @Override public boolean batch(long entriesLeft) { WrapperManager.signalStarting((int) (RESIZE_MEMORY_ENTRIES * SECONDS.toMillis(30) + SECONDS.toMillis(1))); if (i++ % 16 == 0) writeConfigFile(); // shrink data file to current size if (storeSize < _prevStoreSize) setStoreFileSize(Math.max(storeSize, entriesLeft), false); // try to resolve the list Iterator<Entry> it = oldEntryList.iterator(); while (it.hasNext()) if (resolveOldEntry(it.next())) it.remove(); return _prevStoreSize == prevStoreSize; } @Override public void abort() { // Do nothing } @Override public void finish() { configLock.writeLock().lock(); try { if (_prevStoreSize != prevStoreSize) return; prevStoreSize = 0; if(!slotFilterDisabled) { if(slotFilter.size() != (int)storeSize) slotFilter.resize((int)storeSize); else slotFilter.forceWrite(); } flags &= ~FLAG_REBUILD_BLOOM; resizeCompleteCondition.signalAll(); } finally { configLock.writeLock().unlock(); } Logger.normal(this, "Finish resizing (" + name + ")"); } public boolean wantFreeEntries() { return false; } }; batchProcessEntries(resizeProcesser, _prevStoreSize, true, sleep); } /** * Rebuild bloom filter */ private void rebuildBloom(boolean sleep) { if(slotFilterDisabled) return; Logger.normal(this, "Start rebuilding slot filter (" + name + ")"); BatchProcessor<T> rebuildBloomProcessor = new BatchProcessor<T>() { @Override public void init() { configLock.writeLock().lock(); try { keyCount.set(0); } finally { configLock.writeLock().unlock(); } WrapperManager.signalStarting((int) (RESIZE_MEMORY_ENTRIES * SECONDS.toMillis(5) + SECONDS.toMillis(1))); } @Override public Entry process(Entry entry) { if(!slotFilterDisabled) { int cache = entry.getSlotFilterEntry(); try { slotFilter.put((int)entry.curOffset, cache, true); } catch (IOException e) { Logger.error(this, "Unable to update slot filter in bloom rebuild: "+e, e); } } if (!entry.isFree()) { keyCount.incrementAndGet(); if(entry.generation != generation) { entry.generation = generation; return entry; } } return NOT_MODIFIED; } int i = 0; @Override public boolean batch(long entriesLeft) { WrapperManager.signalStarting((int) (RESIZE_MEMORY_ENTRIES * SECONDS.toMillis(5) + SECONDS.toMillis(1))); if (i++ % 16 == 0) writeConfigFile(); if (i++ % 1024 == 0) { if(!slotFilterDisabled) slotFilter.forceWrite(); } return prevStoreSize == 0; } @Override public void abort() { // Do nothing } @Override public void finish() { slotFilter.forceWrite(); configLock.writeLock().lock(); try { flags &= ~FLAG_REBUILD_BLOOM; writeConfigFile(); } finally { configLock.writeLock().unlock(); } System.out.println(name + " cleaner finished successfully."); Logger.normal(this, "Finish rebuilding bloom filter (" + name + ")"); } public boolean wantFreeEntries() { return true; } }; batchProcessEntries(rebuildBloomProcessor, storeSize, false, sleep); } private volatile long entriesLeft; private volatile long entriesTotal; private void batchProcessEntries(BatchProcessor<T> processor, long storeSize, boolean reverse, boolean sleep) { entriesLeft = entriesTotal = storeSize; long startOffset, step; if (!reverse) { startOffset = 0; step = RESIZE_MEMORY_ENTRIES; } else { startOffset = ((storeSize - 1) / RESIZE_MEMORY_ENTRIES) * RESIZE_MEMORY_ENTRIES; step = -RESIZE_MEMORY_ENTRIES; } int i = 0; processor.init(); try { for (long curOffset = startOffset; curOffset >= 0 && curOffset < storeSize; curOffset += step) { if (shutdown) { processor.abort(); return; } if (i++ % 64 == 0) System.err.println(name + " cleaner in progress: " + (entriesTotal - entriesLeft) + "/" + entriesTotal); batchProcessEntries(curOffset, RESIZE_MEMORY_ENTRIES, processor); entriesLeft = reverse ? curOffset : Math.max(storeSize - curOffset - RESIZE_MEMORY_ENTRIES, 0); if (!processor.batch(entriesLeft)) { processor.abort(); return; } try { if (sleep) Thread.sleep(100); } catch (InterruptedException e) { processor.abort(); return; } } processor.finish(); } catch (Exception e) { Logger.error(this, "Caught: "+e+" while shrinking", e); processor.abort(); } } /** * Read a list of items from store. * * @param offset * start offset, must be multiple of {@link FILE_SPLIT} * @param length * number of items to read, must be multiple of {@link FILE_SPLIT}. If this * excess store size, read as much as possible. * @param processor * batch processor * @return <code>true</code> if operation complete successfully; <code>false</code> * otherwise (e.g. can't acquire locks, node shutting down) */ private boolean batchProcessEntries(long offset, int length, BatchProcessor<T> processor) { boolean wantFreeEntries = processor.wantFreeEntries(); Condition[] locked = new Condition[length]; try { // acquire all locks in the region, will unlock in the finally block for (int i = 0; i < length; i++) { locked[i] = lockManager.lockEntry(offset + i); if (locked[i] == null) return false; } long startFileOffset = offset * Entry.METADATA_LENGTH; long entriesToRead = length; long bufLen = Entry.METADATA_LENGTH * entriesToRead; ByteBuffer buf = ByteBuffer.allocate((int) bufLen); boolean dirty = false; try { while (buf.hasRemaining()) { int status = metaFC.read(buf, startFileOffset + buf.position()); if (status == -1) break; } } catch (IOException ioe) { if (shutdown) return false; Logger.error(this, "unexpected IOException", ioe); } buf.flip(); try { for (int j = 0; !shutdown && buf.limit() > j * Entry.METADATA_LENGTH; j++) { buf.position(j * Entry.METADATA_LENGTH); if (buf.remaining() < Entry.METADATA_LENGTH) // EOF break; ByteBuffer enBuf = buf.slice(); enBuf.limit(Entry.METADATA_LENGTH); Entry entry = new Entry(enBuf, null); entry.curOffset = offset + j; if (entry.isFree() && !wantFreeEntries) continue; // not occupied Entry newEntry = processor.process(entry); if (newEntry == null) {// free the offset buf.position(j * Entry.METADATA_LENGTH); buf.put(ByteBuffer.allocate(Entry.METADATA_LENGTH)); keyCount.decrementAndGet(); if(!slotFilterDisabled) try { slotFilter.put((int)(offset + j), SLOT_CHECKED); } catch (IOException e) { Logger.error(this, "Unable to update slot filter: "+e, e); } dirty = true; } else if (newEntry == NOT_MODIFIED) { } else { // write back buf.position(j * Entry.METADATA_LENGTH); buf.put(newEntry.toMetaDataBuffer()); assert newEntry.header == null; // not supported assert newEntry.data == null; // not supported dirty = true; if(!slotFilterDisabled) { int newVal = newEntry.getSlotFilterEntry(); if(slotFilter.get((int)(offset + j)) != newVal) { try { slotFilter.put((int)(offset + j), newVal); } catch (IOException e) { Logger.error(this, "Unable to update slot filter: "+e, e); } } } dirty = true; } } } finally { // write back. if (dirty) { buf.flip(); try { while (buf.hasRemaining()) { metaFC.write(buf, startFileOffset + buf.position()); } } catch (IOException ioe) { Logger.error(this, "unexpected IOException", ioe); } } } return true; } finally { // unlock for (int i = 0; i < length; i++) if (locked[i] != null) lockManager.unlockEntry(offset + i, locked[i]); } } /** * Put back an old entry to store file * * @param entry * @return <code>true</code> if the entry have put back successfully. */ private boolean resolveOldEntry(Entry entry) { Map<Long, Condition> lockMap = lockDigestedKey(entry.getDigestedRoutingKey(), false); if (lockMap == null) return false; try { entry.storeSize = storeSize; long[] offsets = entry.getOffset(); // Check for occupied entry with same key for (long offset : offsets) { try { if (!isFree(offset) && Arrays.equals(getDigestedKeyFromOffset(offset), entry.getDigestedRoutingKey())) { // do nothing return true; } } catch (IOException e) { Logger.debug(this, "IOExcception on resolveOldEntry", e); } } // Check for free entry for (long offset : offsets) { try { if (isFree(offset)) { byte[] digestedKey = entry.getDigestedRoutingKey(); writeEntry(entry, digestedKey, offset); keyCount.incrementAndGet(); return true; } } catch (IOException e) { Logger.debug(this, "IOExcception on resolveOldEntry", e); } } return false; } finally { unlockDigestedKey(entry.getDigestedRoutingKey(), false, lockMap); } } } private final class CleanerStatusUserAlert extends AbstractUserAlert { private Cleaner cleaner; private CleanerStatusUserAlert(Cleaner cleaner) { this.cleaner = cleaner; } @Override public String anchor() { return "store-cleaner-" + name; } @Override public String dismissButtonText() { return NodeL10n.getBase().getString("UserAlert.hide"); } @Override public HTMLNode getHTMLText() { return new HTMLNode("#", getText()); } @Override public short getPriorityClass() { return UserAlert.ERROR; // So everyone sees it. } @Override public String getShortText() { if (cleaner.isResizing) return NodeL10n.getBase().getString("SaltedHashFreenetStore.shortResizeProgress", // new String[] { "name", "processed", "total" },// new String[] { name, String.valueOf(cleaner.entriesTotal - cleaner.entriesLeft) , String.valueOf(cleaner.entriesTotal) }); else return NodeL10n.getBase().getString("SaltedHashFreenetStore.shortRebuildProgress" + (slotFilter.isNew() ? "New" : ""), new String[] { "name", "processed", "total" },// new String[] { name, String.valueOf(cleaner.entriesTotal - cleaner.entriesLeft) , String.valueOf(cleaner.entriesTotal) }); } @Override public String getText() { if (cleaner.isResizing) return NodeL10n.getBase().getString("SaltedHashFreenetStore.longResizeProgress", // new String[] { "name", "processed", "total" },// new String[] { name, String.valueOf(cleaner.entriesTotal - cleaner.entriesLeft) , String.valueOf(cleaner.entriesTotal) }); else return NodeL10n.getBase().getString("SaltedHashFreenetStore.longRebuildProgress" + (slotFilter.isNew() ? "New" : ""), new String[] { "name", "processed", "total" }, new String[] { name, String.valueOf(cleaner.entriesTotal - cleaner.entriesLeft) , String.valueOf(cleaner.entriesTotal) }); } @Override public String getTitle() { return NodeL10n.getBase().getString("SaltedHashFreenetStore.cleanerAlertTitle", // new String[] { "name" }, // new String[] { name }); } @Override public boolean isValid() { return cleaner.isRebuilding || cleaner.isResizing; } @Override public void isValid(boolean validity) { // Ignore } @Override public void onDismiss() { // Ignore } @Override public boolean shouldUnregisterOnDismiss() { return true; } @Override public boolean userCanDismiss() { return false; } @Override public boolean isEventNotification() { return false; } } public void setUserAlertManager(UserAlertManager userAlertManager) { if (cleanerStatusUserAlert != null) userAlertManager.register(cleanerStatusUserAlert); } @Override public void setMaxKeys(long newStoreSize, boolean shrinkNow) throws IOException { Logger.normal(this, "[" + name + "] Resize newStoreSize=" + newStoreSize + ", shinkNow=" + shrinkNow); if(newStoreSize > Integer.MAX_VALUE) // FIXME 64-bit. throw new IllegalArgumentException("Store size over MAXINT not supported due to ResizablePersistentIntBuffer limitations."); configLock.writeLock().lock(); long old; try { if (newStoreSize == this.storeSize) return; if (prevStoreSize != 0) { Logger.normal(this, "[" + name + "] resize already in progress, ignore resize request"); return; } old = storeSize; prevStoreSize = storeSize; storeSize = newStoreSize; if(!slotFilterDisabled) slotFilter.resize((int)Math.max(storeSize, prevStoreSize)); writeConfigFile(); } finally { configLock.writeLock().unlock(); } if (cleanerLock.tryLock()) { cleanerCondition.signal(); cleanerLock.unlock(); } if(shrinkNow) { configLock.writeLock().lock(); try { System.err.println("Waiting for resize to complete..."); while(prevStoreSize == old) { resizeCompleteCondition.awaitUninterruptibly(); } System.err.println("Completed shrink, old size was "+old+" new size was "+newStoreSize+" size is now "+storeSize+" (prev="+prevStoreSize+")"); } finally { configLock.writeLock().unlock(); } } } // ------------- Locking volatile boolean shutdown = false; private LockManager lockManager; private ReadWriteLock configLock = new ReentrantReadWriteLock(); private Condition resizeCompleteCondition = configLock.writeLock().newCondition(); /** * Lock all possible offsets of a key. This method would release the locks if any locking * operation failed. * * @param digestedKey * @return <code>true</code> if all the offsets are locked. */ private Map<Long, Condition> lockDigestedKey(byte[] digestedKey, boolean usePrevStoreSize) { // use a set to prevent duplicated offsets, // a sorted set to prevent deadlocks SortedSet<Long> offsets = new TreeSet<Long>(); long[] offsetArray = getOffsetFromDigestedKey(digestedKey, storeSize); for (long offset : offsetArray) offsets.add(offset); if (usePrevStoreSize && prevStoreSize != 0) { offsetArray = getOffsetFromDigestedKey(digestedKey, prevStoreSize); for (long offset : offsetArray) offsets.add(offset); } Map<Long, Condition> locked = new TreeMap<Long, Condition>(); for (long offset : offsets) { Condition condition = lockManager.lockEntry(offset); if (condition == null) break; locked.put(offset, condition); } if (locked.size() == offsets.size()) { return locked; } else { // failed, remove the locks for (Map.Entry<Long, Condition> e : locked.entrySet()) lockManager.unlockEntry(e.getKey(), e.getValue()); return null; } } private void unlockDigestedKey(byte[] digestedKey, boolean usePrevStoreSize, Map<Long, Condition> lockMap) { // use a set to prevent duplicated offsets SortedSet<Long> offsets = new TreeSet<Long>(); long[] offsetArray = getOffsetFromDigestedKey(digestedKey, storeSize); for (long offset : offsetArray) offsets.add(offset); if (usePrevStoreSize && prevStoreSize != 0) { offsetArray = getOffsetFromDigestedKey(digestedKey, prevStoreSize); for (long offset : offsetArray) offsets.add(offset); } for (long offset : offsets) { lockManager.unlockEntry(offset, lockMap.get(offset)); lockMap.remove(offset); } } public class ShutdownDB implements Runnable { @Override public void run() { close(); } } // ------------- Hashing private CipherManager cipherManager; /** * Get offset in the hash table, given a plain routing key. * * @param plainKey * @param storeSize * @return */ private long[] getOffsetFromPlainKey(byte[] plainKey, long storeSize) { return getOffsetFromDigestedKey(cipherManager.getDigestedKey(plainKey), storeSize); } public void close() { close(false); } public void close(boolean abort) { shutdown = true; lockManager.shutdown(); cleanerLock.lock(); try { cleanerCondition.signalAll(); cleanerThread.interrupt(); } finally { cleanerLock.unlock(); } configLock.writeLock().lock(); try { flushAndClose(abort); flags &= ~FLAG_DIRTY; // clean shutdown writeConfigFile(); } finally { configLock.writeLock().unlock(); } cipherManager.shutdown(); System.out.println("Successfully closed store "+name); } /** * Get offset in the hash table, given a digested routing key. * * @param digestedKey * @param storeSize * @return */ private long[] getOffsetFromDigestedKey(byte[] digestedKey, long storeSize) { long keyValue = Fields.bytesToLong(digestedKey); long[] offsets = new long[OPTION_MAX_PROBE]; for (int i = 0; i < OPTION_MAX_PROBE; i++) { // h + 141 i^2 + 13 i offsets[i] = ((keyValue + 141 * (i * i) + 13 * i) & Long.MAX_VALUE) % storeSize; // Make sure the slots are all unique. // Important for very small stores e.g. in unit tests. while(true) { boolean clear = true; for(int j=0;j<i;j++) { if(offsets[i] == offsets[j]) { offsets[i] = (offsets[i] + 1) % storeSize; clear = false; } } if(clear || OPTION_MAX_PROBE > storeSize) break; } } return offsets; } // ------------- Statistics (a.k.a. lies) private AtomicLong hits = new AtomicLong(); private AtomicLong misses = new AtomicLong(); private AtomicLong writes = new AtomicLong(); private AtomicLong keyCount = new AtomicLong(); private AtomicLong bloomFalsePos = new AtomicLong(); private long initialHits; private long initialMisses; private long initialWrites; private long initialBloomFalsePos; @Override public long hits() { return hits.get(); } @Override public long misses() { return misses.get(); } @Override public long writes() { return writes.get(); } @Override public long keyCount() { return keyCount.get(); } @Override public long getMaxKeys() { configLock.readLock().lock(); long _storeSize = storeSize; configLock.readLock().unlock(); return _storeSize; } @Override public long getBloomFalsePositive() { return bloomFalsePos.get(); } @Override public boolean probablyInStore(byte[] routingKey) { configLock.readLock().lock(); try { if(slotFilterDisabled) return true; byte[] digestedKey = cipherManager.getDigestedKey(routingKey); long[] offsets = getOffsetFromDigestedKey(digestedKey, storeSize); boolean anyNotValid = false; for(long offset : offsets) { if(offset > Integer.MAX_VALUE) return true; // FIXME! int cache = 0; boolean validCache = false; boolean likelyMatch = false; cache = slotFilter.get((int)offset); validCache = (cache & SLOT_CHECKED) != 0; if(!validCache) { anyNotValid = true; continue; } likelyMatch = slotCacheLikelyMatch(cache, digestedKey); if(validCache && likelyMatch) return true; } if (prevStoreSize != 0) offsets = getOffsetFromDigestedKey(digestedKey, prevStoreSize); for(long offset : offsets) { if(offset > Integer.MAX_VALUE) return true; // FIXME! int cache = 0; boolean validCache = false; boolean likelyMatch = false; cache = slotFilter.get((int)offset); validCache = (cache & SLOT_CHECKED) != 0; if(!validCache) { anyNotValid = true; continue; } likelyMatch = slotCacheLikelyMatch(cache, digestedKey); if(validCache && likelyMatch) return true; } if(anyNotValid) return true; return false; } finally { configLock.readLock().unlock(); } } public void destruct() { metaFile.delete(); hdFile.delete(); configFile.delete(); bloomFile.delete(); } @Override public String toString() { return super.toString()+":"+name; } @Override public StoreAccessStats getSessionAccessStats() { return new StoreAccessStats() { @Override public long hits() { return hits.get() - initialHits; } @Override public long misses() { return misses.get() - initialMisses; } @Override public long falsePos() { return bloomFalsePos.get() - initialBloomFalsePos; } @Override public long writes() { return writes.get() - initialWrites; } }; } @Override public StoreAccessStats getTotalAccessStats() { return new StoreAccessStats() { @Override public long hits() { return hits.get(); } @Override public long misses() { return misses.get(); } @Override public long falsePos() { return bloomFalsePos.get(); } @Override public long writes() { return writes.get(); } }; } /** Testing only! Force all entries that say empty/unknown on the slot * filter to empty/certain. */ public void forceValidEmpty() { slotFilter.replaceAllEntries(0, SLOT_CHECKED); } @Override public FreenetStore<T> getUnderlyingStore() { return this; } /** Only for testing (crude!) * @throws InterruptedException */ void testingWaitForCleanerDone(int delay, int count) throws InterruptedException { for(int i=0;i<count;i++) { configLock.readLock().lock(); try { if((flags & FLAG_REBUILD_BLOOM) == 0) return; } finally { configLock.readLock().unlock(); } Thread.sleep(delay); } throw new AssertionError(); } }