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;
}
}