Package freenet.store.saltedhash

Source Code of freenet.store.saltedhash.SaltedHashFreenetStore

/* 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();
  }
}
TOP

Related Classes of freenet.store.saltedhash.SaltedHashFreenetStore

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.