package freenet.store.saltedhash; 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.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import freenet.support.Fields; import freenet.support.Logger; import freenet.support.Ticker; /** A large resizable block of int's, which is persisted to disk with a specific policy, * which is either to write it on shutdown, immediately, or every X millis. * * It would be better to do this with ByteBuffer's and an IntBuffer view, unfortunately * it is not possible to subclass ByteBuffer's! Also, ideally we'd memory map, but there * is no way to unmap, and it is likely there will never be, so resizing would be very * messy and expensive. * @author toad */ public class ResizablePersistentIntBuffer { private final File filename; private final RandomAccessFile raf; private final FileChannel channel; private final boolean isNew; private int size; /** The buffer. When we resize we write-lock and replace this. */ private int[] buffer; private final ReadWriteLock lock; // 5 minutes by default. Disk I/O kills disks, and annoys users, so it's a fair tradeoff. // Anything other than -1 risks data loss if the node is shut down uncleanly. // But it does not damage the store: We recover from it transparently. // Note also that any value other than -1 will trigger a bloom filter rebuild after an unclean shutdown, which arguably is the opposite of what we want... :| // FIXME make that configurable. public static final int DEFAULT_PERSISTENCE_TIME = 300000; // FIXME is static the best way to do this? It seems simplest at least... /** -1 = write immediately, 0 = write only on shutdown, +ve = write period in millis */ private static int globalPersistenceTime = DEFAULT_PERSISTENCE_TIME; private Ticker ticker; /** Is the buffer dirty? Protected by (this). */ private boolean dirty; /** Is the writer job scheduled? Protected by (this). */ private boolean scheduled; /** Is the writer job running? So we can wait for it to complete on shutdown e.g. * Protected by (this). */ private boolean writing; private boolean closed; public static synchronized void setPersistenceTime(int val) { globalPersistenceTime = val; } public static synchronized int getPersistenceTime() { return globalPersistenceTime; } /** Create the buffer. Open the file, creating if necessary, read in the data, and set * its size. * @param f The filename. * @param size The expected size in ints (i.e. multiply by four to get bytes). * @throws IOException */ public ResizablePersistentIntBuffer(File f, int size) throws IOException { this.filename = f; isNew = !f.exists(); this.raf = new RandomAccessFile(f, "rw"); this.lock = new ReentrantReadWriteLock(); this.size = size; buffer = new int[size]; long expectedLength = ((long)size)*4; long realLength = raf.length(); if(realLength > expectedLength) raf.setLength(expectedLength); readBuffer((int)Math.min(size, realLength/4)); if(realLength < expectedLength) raf.setLength(expectedLength); channel = raf.getChannel(); } /** Should be called during startup to fill in an appropriate default value e.g. if the store * is completely new. */ public void fill(int value) { for(int i=0;i<buffer.length;i++) buffer[i] = value; } private void readBuffer(int size) throws IOException { raf.seek(0); byte[] buf = new byte[32768]; int read = 0; while(read < size) { int toRead = Math.min(buf.length, (size - read) * 4); raf.readFully(buf, 0, toRead); int[] data = Fields.bytesToInts(buf, 0, toRead); System.arraycopy(data, 0, buffer, read, data.length); read += data.length; } } public void start(Ticker ticker) { synchronized(this) { this.ticker = ticker; if(dirty) { int persistenceTime = getPersistenceTime(); Logger.normal(this, "Scheduling write of slot cache "+this+" in "+persistenceTime); ticker.queueTimedJob(writer, persistenceTime); scheduled = true; } } } public int get(int offset) { lock.readLock().lock(); if(closed) throw new IllegalStateException("Already shut down"); try { return buffer[offset]; } finally { lock.readLock().unlock(); } } public void put(int offset, int value) throws IOException { put(offset, value, false); } public void put(int offset, int value, boolean noWrite) throws IOException { lock.readLock().lock(); // Only resize needs write lock because it creates a new buffer. if(closed) throw new IllegalStateException("Already shut down"); try { int persistenceTime = getPersistenceTime(); buffer[offset] = value; if(persistenceTime == -1 && !noWrite) { channel.write(ByteBuffer.wrap(Fields.intToBytes(value)), ((long)offset)*4); } else if(persistenceTime > 0) { synchronized(this) { dirty = true; if(ticker != null) { if(!scheduled) { Logger.normal(this, "Scheduling write of slot cache "+this+" in "+persistenceTime); ticker.queueTimedJob(writer, persistenceTime); scheduled = true; } } else { Logger.normal(this, "Will scheduling write of slot cache after startup: "+this+" in "+persistenceTime); } } } else { synchronized(this) { dirty = true; } } } finally { lock.readLock().unlock(); } } private Runnable writer = new Runnable() { public void run() { Logger.normal(this, "Writing slot cache "+ResizablePersistentIntBuffer.this); lock.readLock().lock(); // Protect buffer. try { synchronized(ResizablePersistentIntBuffer.this) { if(writing || !dirty || closed) { scheduled = false; return; } scheduled = false; dirty = false; writing = true; } try { writeBuffer(); } catch (IOException e) { Logger.error(this, "Write failed during shutdown: "+e+" on "+filename, e); } } finally { synchronized(ResizablePersistentIntBuffer.this) { writing = false; ResizablePersistentIntBuffer.this.notifyAll(); } lock.readLock().unlock(); } Logger.normal(this, "Written slot cache "+ResizablePersistentIntBuffer.this); } }; public void shutdown() { lock.writeLock().lock(); try { synchronized(this) { if(closed) return; closed = true; if(writing) { // Wait for write to finish. while(writing) { try { wait(); } catch (InterruptedException e) { // Ignore. } } if(!dirty) return; } writing = true; } try { Logger.normal(this, "Writing slot cache on shutdown: "+this); writeBuffer(); } catch (IOException e) { Logger.error(this, "Write failed during shutdown: "+e+" on "+filename, e); } synchronized(this) { writing = false; } try { raf.close(); } catch (IOException e) { Logger.error(this, "Close failed during shutdown: "+e+" on "+filename, e); } } finally { lock.writeLock().unlock(); } } public void abort() { lock.writeLock().lock(); try { synchronized(this) { if(closed) return; closed = true; } try { raf.close(); } catch (IOException e) { Logger.error(this, "Close failed during shutdown: "+e+" on "+filename, e); } } finally { lock.writeLock().unlock(); } } private void writeBuffer() throws IOException { // FIXME do we need to do partial writes? raf.seek(0); int written = 0; while(written < size) { int toWrite = Math.min(32768, size - written); byte[] buf = Fields.intsToBytes(buffer, written, toWrite); raf.write(buf); written += toWrite; } } public void resize(int size) { lock.writeLock().lock(); try { if(this.size == size) return; Logger.normal(this, "Resizing cache from "+this.size+" slots to "+size); this.size = size; buffer = Arrays.copyOf(buffer, size); try { raf.setLength(size * 4); writeBuffer(); } catch (IOException e) { Logger.error(this, "Failed to change size or write during resize on "+filename+" : "+e, e); } } finally { lock.writeLock().unlock(); } } public void forceWrite() { Logger.normal(this, "Force write slot cache: "+this); lock.readLock().lock(); try { synchronized(this) { if(closed) return; dirty = false; if(writing) { // Wait for write to finish. while(writing) { try { wait(); } catch (InterruptedException e) { // Ignore. } } if(!dirty) return; } writing = true; } try { writeBuffer(); } catch (IOException e) { Logger.error(this, "Write failed during shutdown: "+e+" on "+filename, e); } } finally { synchronized(this) { writing = false; } lock.readLock().unlock(); } } public boolean isNew() { return isNew; } public String toString() { return filename.getPath(); } // Testing only! Hence no lock. public void replaceAllEntries(int key, int value) { for(int i=0;i<buffer.length;i++) if(buffer[i] == key) buffer[i] = value; } public int size() { return size; } }