package freenet.store;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import freenet.keys.KeyVerifyException;
import freenet.node.stats.StoreAccessStats;
import freenet.node.useralerts.UserAlertManager;
import freenet.support.ByteArrayWrapper;
import freenet.support.LRUMap;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Ticker;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;
import freenet.support.io.TempBucketFactory;
/** Short-term cache. Used to cache all blocks retrieved in the last 30 minutes (on low
* security levels), or just to cache data fetched through ULPRs (on higher security levels).
* - Strict LRU.
* - Size limit.
* - Strictly enforced time limit.
* - Blocks are encrypted, and kept in temp files.
*
* @author Matthew Toseland <toad@amphibian.dyndns.org> (0xE43DA450)
*/
public class SlashdotStore<T extends StorableBlock> implements FreenetStore<T> {
private static volatile boolean logDEBUG;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
}
});
}
private class DiskBlock {
Bucket data;
long lastAccessed;
}
private final TempBucketFactory bf;
private long maxLifetime;
private final long purgePeriod;
// PURGING OLD DATA:
// Every X period? I don't think it matters if we're a few minutes out, and it's probably easiest that way...
private final Ticker ticker;
private final LRUMap<ByteArrayWrapper, DiskBlock> blocksByRoutingKey;
private final StoreCallback<T> callback;
private int maxKeys;
private long hits;
private long misses;
private long writes;
private final int headerSize;
private final int dataSize;
private final int fullKeySize;
public SlashdotStore(StoreCallback<T> callback, int maxKeys, long maxLifetime, long purgePeriod, Ticker ticker, TempBucketFactory tbf) {
this.callback = callback;
this.blocksByRoutingKey = LRUMap.createSafeMap(ByteArrayWrapper.FAST_COMPARATOR);
this.maxKeys = maxKeys;
this.bf = tbf;
this.ticker = ticker;
this.maxLifetime = maxLifetime;
this.purgePeriod = purgePeriod;
callback.setStore(this);
this.headerSize = callback.headerLength();
this.dataSize = callback.dataLength();
this.fullKeySize = callback.fullKeyLength();
Runnable purgeOldData = new Runnable() {
@Override
public void run() {
try {
purgeOldData();
} finally {
SlashdotStore.this.ticker.queueTimedJob(this, SlashdotStore.this.purgePeriod);
}
}
};
ticker.queueTimedJob(purgeOldData, maxLifetime + purgePeriod);
}
/**
* @param meta IGNORED!
*/
@Override
public T fetch(byte[] routingKey, byte[] fullKey, boolean dontPromote, boolean canReadClientCache, boolean canReadSlashdotCache, boolean ignoreOldBlocks, BlockMetadata meta) throws IOException {
ByteArrayWrapper key = new ByteArrayWrapper(routingKey);
DiskBlock block;
long timeAccessed;
synchronized(this) {
block = blocksByRoutingKey.get(key);
if(block == null) {
misses++;
return null;
}
timeAccessed = block.lastAccessed;
}
InputStream in = block.data.getInputStream();
DataInputStream dis = new DataInputStream(in);
byte[] fk = new byte[fullKeySize];
byte[] header = new byte[headerSize];
byte[] data = new byte[dataSize];
dis.readFully(fk);
dis.readFully(header);
dis.readFully(data);
in.close();
try {
T ret =
callback.construct(data, header, routingKey, fk, canReadClientCache, canReadSlashdotCache, null, null);
synchronized(this) {
hits++;
if(!dontPromote) {
block.lastAccessed = System.currentTimeMillis();
blocksByRoutingKey.push(key, block);
}
}
if(logDEBUG) Logger.debug(this, "Block was last accessed "+(System.currentTimeMillis() - timeAccessed)+"ms ago");
return ret;
} catch (KeyVerifyException e) {
block.data.free();
synchronized(this) {
blocksByRoutingKey.removeKey(key);
misses++;
}
return null;
}
}
@Override
public long getBloomFalsePositive() {
return -1;
}
@Override
public long getMaxKeys() {
return maxKeys;
}
@Override
public long hits() {
return hits;
}
@Override
public long keyCount() {
return blocksByRoutingKey.size();
}
@Override
public long misses() {
return misses;
}
@Override
public boolean probablyInStore(byte[] routingKey) {
ByteArrayWrapper key = new ByteArrayWrapper(routingKey);
return blocksByRoutingKey.containsKey(key);
}
/**
* @param isOldBlock Ignored, we don't distinguish between stuff that should be cached and
* stuff that shouldn't be cached; really it's all in the latter category anyway here!
*/
@Override
public void put(T block, byte[] data, byte[] header, boolean overwrite, boolean isOldBlock) throws IOException, KeyCollisionException {
byte[] routingkey = block.getRoutingKey();
byte[] fullKey = block.getFullKey();
Bucket bucket = bf.makeBucket(fullKeySize + dataSize + headerSize);
OutputStream os = bucket.getOutputStream();
try {
os.write(fullKey);
os.write(header);
os.write(data);
} finally {
os.close();
}
DiskBlock stored = new DiskBlock();
stored.data = bucket;
purgeOldData(new ByteArrayWrapper(routingkey), stored);
}
@Override
public void setMaxKeys(long maxStoreKeys, boolean shrinkNow) throws IOException {
if(maxStoreKeys > Integer.MAX_VALUE) throw new IllegalArgumentException();
this.maxKeys = (int) maxStoreKeys;
if(shrinkNow) {
purgeOldData();
} else {
ticker.queueTimedJob(new Runnable() {
@Override
public void run() {
purgeOldData();
// Don't re-schedule
}
}, 0);
}
}
@Override
public long writes() {
return writes;
}
protected void purgeOldData() {
purgeOldData(null, null);
}
protected void purgeOldData(ByteArrayWrapper key, DiskBlock addFirst) {
if(logDEBUG) Logger.minor(this, "Dumping old data from "+this+(addFirst == null ? "" : " and adding "+addFirst.data));
List<DiskBlock> blocks = null;
DiskBlock oldBlock;
synchronized(this) {
long now = System.currentTimeMillis();
if(addFirst != null) {
addFirst.lastAccessed = now;
oldBlock = blocksByRoutingKey.push(key, addFirst);
if(oldBlock != null) {
Logger.warning(this, "Replacing "+oldBlock+" with "+addFirst+" for "+key);
if(blocks == null) blocks = new ArrayList<DiskBlock>();
if(logDEBUG) Logger.minor(this, "Will dump "+oldBlock);
blocks.add(oldBlock);
}
writes++;
}
while(true) {
if(blocksByRoutingKey.isEmpty()) break;
DiskBlock block = blocksByRoutingKey.peekValue();
if(now - block.lastAccessed < maxLifetime && blocksByRoutingKey.size() < maxKeys) break;
if(blocks == null) blocks = new ArrayList<DiskBlock>();
if(logDEBUG) Logger.minor(this, "Will dump "+block);
blocks.add(block);
blocksByRoutingKey.popValue();
}
}
if(blocks == null) return;
for(DiskBlock block : blocks) {
block.data.free();
}
}
public synchronized Long getLifetime() {
return maxLifetime;
}
public synchronized void setLifetime(Long val) {
maxLifetime = val;
}
@Override
public StoreAccessStats getSessionAccessStats() {
return new StoreAccessStats() {
@Override
public long hits() {
return hits;
}
@Override
public long misses() {
return misses;
}
@Override
public long falsePos() {
return 0;
}
@Override
public long writes() {
return writes;
}
};
}
@Override
public StoreAccessStats getTotalAccessStats() {
return null;
}
@Override
public boolean start(Ticker ticker, boolean longStart) throws IOException {
return false;
}
@Override
public void setUserAlertManager(UserAlertManager userAlertManager) {
// Do nothing
}
@Override
public FreenetStore<T> getUnderlyingStore() {
return this;
}
@Override
public void close() {
// Do nothing
}
}