package freenet.client.async;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import freenet.client.FECCodec;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.async.PersistentJobRunner.CheckpointLock;
import freenet.keys.CHKBlock;
import freenet.keys.CHKEncodeException;
import freenet.keys.ClientCHK;
import freenet.keys.ClientCHKBlock;
import freenet.support.Logger;
import freenet.support.MemoryLimitedChunk;
import freenet.support.MemoryLimitedJob;
import freenet.support.io.NativeThread;
import freenet.support.io.StorageFormatException;
/** Cross-segments are "in parallel" with the main segments, an interlaced Reed-Solomon scheme
* similar to that used on CD's, allowing us to fill in blocks from other segments. There are 3
* "cross-check blocks" in each segment, and therefore 3 in each cross-segment; the rest are data
* blocks.
*/
public class SplitFileFetcherCrossSegmentStorage {
private static volatile boolean logMINOR;
static {
Logger.registerClass(SplitFileFetcherCrossSegmentStorage.class);
}
public final int crossSegmentNumber;
public final SplitFileFetcherStorage parent;
/** Segment for each block */
private final SplitFileFetcherSegmentStorage[] segments;
/** Block number within the segment for each block */
private final int[] blockNumbers;
/** Whether each block in the cross-segment has been found. Kept up to date when blocks are
* found in the other segments. However, as in a normal segment, these may not be 100%
* accurate! */
private final boolean[] blocksFound;
/** Number of data blocks chosen from the various segments. */
final int dataBlockCount;
/** Number of check blocks chosen from the various segments. Typically 3. */
final int crossCheckBlockCount;
final int totalBlocks;
private int totalFound;
/** If true, we are currrently decoding */
private boolean tryDecode;
/** True if the request has been terminated for some reason. */
private boolean cancelled;
/** If true, the segment has completed. Once a segment decode starts, finished must not be set
* until it exits. */
private boolean succeeded;
private final FECCodec codec;
/** Used in assigning blocks */
private int counter;
SplitFileFetcherCrossSegmentStorage(int segNo, int blocksPerSegment, int crossCheckBlocks,
SplitFileFetcherStorage parent, FECCodec codec) {
this.crossSegmentNumber = segNo;
this.parent = parent;
this.dataBlockCount = blocksPerSegment;
this.crossCheckBlockCount = crossCheckBlocks;
totalBlocks = dataBlockCount + crossCheckBlocks;
int totalBlocks = dataBlockCount + crossCheckBlocks;
this.codec = codec;
segments = new SplitFileFetcherSegmentStorage[totalBlocks];
blockNumbers = new int[totalBlocks];
blocksFound = new boolean[totalBlocks];
}
/** Called when a segment fetches a block that it believes to be relevant to us */
public void onFetchedRelevantBlock(SplitFileFetcherSegmentStorage segment, int blockNo) {
synchronized(this) {
boolean found = false;
for(int i=0;i<segments.length;i++) {
if(segments[i] == segment && blockNumbers[i] == blockNo) {
found = true;
if(blocksFound[i]) {
// Already handled, don't loop.
return;
}
blocksFound[i] = true;
totalFound++;
}
}
if(tryDecode || succeeded || cancelled) return;
if(!found) {
Logger.warning(this, "Block "+blockNo+" on "+segment+" not wanted by "+this);
return;
}
if(totalFound < dataBlockCount) {
if(logMINOR) Logger.minor(this, "Not decoding "+this+" : found "+totalFound+" blocks of "+dataBlockCount+" (total "+segments.length+")");
return;
}
tryDecodeOrEncode();
}
}
private synchronized void tryDecodeOrEncode() {
if(succeeded) return;
if(tryDecode) return;
if(cancelled) return;
long limit = totalBlocks * CHKBlock.DATA_LENGTH +
Math.max(parent.fecCodec.maxMemoryOverheadDecode(dataBlockCount, crossCheckBlockCount),
parent.fecCodec.maxMemoryOverheadEncode(dataBlockCount, crossCheckBlockCount));
final int prio = NativeThread.LOW_PRIORITY;
parent.memoryLimitedJobRunner.queueJob(new MemoryLimitedJob(limit) {
@Override
public int getPriority() {
return prio;
}
@Override
public boolean start(MemoryLimitedChunk chunk) {
boolean shutdown = false;
CheckpointLock lock = null;
try {
lock = parent.jobRunner.lock();
innerDecode(chunk);
} catch (IOException e) {
Logger.error(this, "Failed to decode "+this+" because of disk error: "+e, e);
parent.failOnDiskError(e);
} catch (PersistenceDisabledException e) {
shutdown = true;
} finally {
chunk.release();
try {
if(!shutdown) {
// We do want to call the callback even if we threw something, because we
// may be waiting to cancel. However we DON'T call it if we are shutting down.
synchronized(SplitFileFetcherCrossSegmentStorage.this) {
tryDecode = false;
}
parent.finishedEncoding(SplitFileFetcherCrossSegmentStorage.this);
}
} finally {
// Callback is part of the persistent job, unlock *after* calling it.
if(lock != null) lock.unlock(false, prio);
}
}
return true;
}
});
tryDecode = true;
}
/** Attempt FEC decoding. Check blocks before decoding in case there is disk corruption. Check
* the new decoded blocks afterwards to ensure reproducible behaviour. */
private void innerDecode(MemoryLimitedChunk chunk) throws IOException {
if(logMINOR) Logger.minor(this, "Trying to decode "+this+" for "+parent);
boolean killed = false;
synchronized(this) {
if(succeeded) return;
if(cancelled) {
killed = true;
}
}
if(killed) {
return;
}
// readAllBlocks does most of the housekeeping for us, see below...
byte[][] dataBlocks = readBlocks(false);
byte[][] checkBlocks = readBlocks(true);
if(dataBlocks == null || checkBlocks == null) return; // Failed with disk error.
// Original status.
boolean[] dataBlocksFound = wasNonNullFill(dataBlocks);
boolean[] checkBlocksFound = wasNonNullFill(checkBlocks);
int realTotalDataBlocks = count(dataBlocksFound);
int realTotalCrossCheckBlocks = count(checkBlocksFound);
int realTotalFound = realTotalDataBlocks + realTotalCrossCheckBlocks;
if(realTotalFound < dataBlockCount) {
// Not finished yet.
return;
}
boolean decoded = false;
boolean encoded = false;
if(realTotalDataBlocks < dataBlockCount) {
// Decode.
codec.decode(dataBlocks, checkBlocks, dataBlocksFound, checkBlocksFound,
CHKBlock.DATA_LENGTH);
for(int i=0;i<dataBlockCount;i++) {
if(!dataBlocksFound[i]) {
checkDecodedBlock(i, dataBlocks[i]);
dataBlocksFound[i] = true;
}
}
}
if(realTotalCrossCheckBlocks < crossCheckBlockCount) {
// Decode.
codec.encode(dataBlocks, checkBlocks, checkBlocksFound, CHKBlock.DATA_LENGTH);
for(int i=0;i<crossCheckBlockCount;i++) {
if(!checkBlocksFound[i]) {
checkDecodedBlock(i+dataBlockCount, checkBlocks[i]);
}
}
}
synchronized(this) {
succeeded = true;
}
if(logMINOR) Logger.minor(this, "Completed a cross-segment: decoded="+decoded+" encoded="+encoded);
}
private void checkDecodedBlock(int i, byte[] data) {
ClientCHK key = getKey(i);
if(key == null) {
Logger.error(this, "Key not found");
failOffThread(new FetchException(FetchExceptionMode.INTERNAL_ERROR, "Key not found"));
return;
}
ClientCHKBlock block = encodeBlock(key, data);
String decoded = i >= dataBlockCount ? "Encoded" : "Decoded";
if(block == null || !key.getNodeCHK().equals(block.getKey())) {
Logger.error(this, decoded+" cross-segment block "+i+" failed!");
failOffThread(new FetchException(FetchExceptionMode.SPLITFILE_DECODE_ERROR, decoded+" cross-segment block does not match expected key"));
return;
} else {
reportBlockToSegmentOffThread(i, key, block, data);
}
}
private ClientCHKBlock encodeBlock(ClientCHK key, byte[] data) {
try {
return ClientCHKBlock.encodeSplitfileBlock(data, key.getCryptoKey(), key.getCryptoAlgorithm());
} catch (CHKEncodeException e) {
return null;
}
}
private void reportBlockToSegmentOffThread(final int blockNo, final ClientCHK key,
final ClientCHKBlock block, final byte[] data) {
parent.jobRunner.queueNormalOrDrop(new PersistentJob() {
@Override
public boolean run(ClientContext context) {
try {
// FIXME CPU USAGE Add another API to the segment to avoid re-decoding.
SplitFileSegmentKeys keys = segments[blockNo].getSegmentKeys();
if(keys == null) return false;
boolean success = segments[blockNo].innerOnGotKey(key.getNodeCHK(), block, keys,
blockNumbers[blockNo], data);
if(success) {
if(logMINOR)
Logger.minor(this, "Successfully decoded cross-segment block");
} else {
// Not really a big deal, but potentially interesting...
Logger.warning(this, "Decoded cross-segment block but not wanted by segment");
}
} catch (IOException e) {
parent.failOnDiskError(e);
return true;
}
return false;
}
});
}
private void failOffThread(final FetchException e) {
parent.jobRunner.queueNormalOrDrop(new PersistentJob() {
@Override
public boolean run(ClientContext context) {
parent.fail(e);
return true;
}
});
}
private void failDiskOffThread(final IOException e) {
parent.jobRunner.queueNormalOrDrop(new PersistentJob() {
@Override
public boolean run(ClientContext context) {
parent.failOnDiskError(e);
return true;
}
});
}
private ClientCHK getKey(int i) {
return segments[i].getKey(blockNumbers[i]);
}
private static boolean[] wasNonNullFill(byte[][] blocks) {
boolean[] nonNulls = new boolean[blocks.length];
for(int i=0;i<blocks.length;i++) {
if(blocks[i] == null) {
blocks[i] = new byte[CHKBlock.DATA_LENGTH];
} else {
nonNulls[i] = true;
}
}
return nonNulls;
}
/** Read all blocks from all segments, checking the contents of the block against each key.
* @param If false, read data blocks, if true, read check blocks. (The FEC code takes separate
* arrays).
* @return An array of blocks, in the correct order. Each element is either a valid block or
* null if the block is invalid or hasn't been fetched yet. Will tell the ordinary segment if
* the block is bogus. Will also update our blocksFound. */
private byte[][] readBlocks(boolean checkBlocks) {
int start = checkBlocks ? dataBlockCount : 0;
int end = checkBlocks ? totalBlocks : dataBlockCount;
byte[][] blocks = new byte[end-start][];
for(int i=start;i<end;i++) {
try {
byte[] block = segments[i].checkAndGetBlockData(blockNumbers[i]);
blocks[i-start] = block;
synchronized(this) {
if(block != null) {
if(!blocksFound[i]) totalFound++;
blocksFound[i] = true;
} else {
if(blocksFound[i]) totalFound--;
blocksFound[i] = false;
}
}
} catch (IOException e) {
failDiskOffThread(e);
return null;
}
}
return blocks;
}
private static int count(boolean[] array) {
int total = 0;
for(boolean b : array)
if(b) total++;
return total;
}
public void addDataBlock(SplitFileFetcherSegmentStorage seg, int blockNum) {
segments[counter] = seg;
blockNumbers[counter] = blockNum;
counter++;
}
public synchronized boolean isDecoding() {
return tryDecode;
}
public void writeFixedMetadata(DataOutputStream dos) throws IOException {
dos.writeInt(dataBlockCount);
dos.writeInt(crossCheckBlockCount);
for(int i=0;i<totalBlocks;i++) {
dos.writeInt(segments[i].segNo);
dos.writeInt(blockNumbers[i]);
}
}
public SplitFileFetcherCrossSegmentStorage(SplitFileFetcherStorage parent, int segNo,
DataInputStream dis) throws IOException, StorageFormatException {
this.parent = parent;
this.crossSegmentNumber = segNo;
this.codec = parent.fecCodec;
this.dataBlockCount = dis.readInt();
this.crossCheckBlockCount = dis.readInt();
this.totalBlocks = dataBlockCount + crossCheckBlockCount;
blocksFound = new boolean[totalBlocks];
segments = new SplitFileFetcherSegmentStorage[totalBlocks];
blockNumbers = new int[totalBlocks];
for(int i=0;i<totalBlocks;i++) {
int readSeg = dis.readInt();
if(readSeg < 0 || readSeg >= parent.segments.length)
throw new StorageFormatException("Invalid segment number "+readSeg);
SplitFileFetcherSegmentStorage segment = parent.segments[readSeg];
this.segments[i] = segment;
int blockNo = dis.readInt();
if(blockNo < 0 || blockNo >= segment.totalBlocks())
throw new StorageFormatException("Invalid block number "+blockNo+" for segment "+segment.segNo);
this.blockNumbers[i] = blockNo;
segment.resumeCallback(blockNo, this);
}
}
/** Should be called before scheduling, unlike restart(). Doesn't lock, i.e. part of
* construction. But we must have read metadata on the regular segments first, which won't be
* true in the constructor. */
public void checkBlocks() {
for(int i=0;i<totalBlocks;i++) {
if(segments[i].hasBlock(blockNumbers[i])) {
blocksFound[i] = true;
totalFound++;
}
}
}
/** Check for blocks and try to decode. */
public void restart() {
synchronized(this) {
if(succeeded) return;
}
synchronized(this) {
if(totalBlocks < dataBlockCount) return;
tryDecodeOrEncode();
}
}
public void cancel() {
synchronized(this) {
cancelled = true;
if(tryDecode) return;
succeeded = true;
}
parent.finishedEncoding(this);
}
int[] getSegmentNumbers() {
int[] ret = new int[totalBlocks];
for(int i=0;i<totalBlocks;i++)
ret[i] = segments[i].segNo;
return ret;
}
int[] getBlockNumbers() {
return blockNumbers.clone();
}
}