package freenet.client.async;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
import freenet.client.FECCodec;
import freenet.client.InsertException;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.client.async.PersistentJobRunner.CheckpointLock;
import freenet.crypt.ChecksumChecker;
import freenet.crypt.ChecksumFailedException;
import freenet.keys.CHKBlock;
import freenet.keys.CHKEncodeException;
import freenet.keys.ClientCHK;
import freenet.keys.ClientCHKBlock;
import freenet.node.KeysFetchingLocally;
import freenet.node.SendableRequestItem;
import freenet.node.SendableRequestItemKey;
import freenet.support.Logger;
import freenet.support.MemoryLimitedChunk;
import freenet.support.MemoryLimitedJob;
import freenet.support.api.LockableRandomAccessBuffer.RAFLock;
import freenet.support.io.CountedOutputStream;
import freenet.support.io.NativeThread;
import freenet.support.io.NullOutputStream;
import freenet.support.io.StorageFormatException;
/** A single segment within a splitfile to be inserted. */
public class SplitFileInserterSegmentStorage {
private static volatile boolean logMINOR;
private static volatile boolean logDEBUG;
static {
Logger.registerClass(SplitFileInserterSegmentStorage.class);
}
final SplitFileInserterStorage parent;
final int segNo;
final int dataBlockCount;
final int crossCheckBlockCount;
final int checkBlockCount;
final int totalBlockCount;
/** Has the segment been encoded? If so, all of the check blocks have been written. */
private boolean encoded;
private boolean encoding;
private final int statusLength;
/** Length of a single key stored on disk. Includes checksum. */
private final int keyLength;
// FIXME These are refilled by SplitFileInserterCrossSegmentStorage on construction...
/** For each cross-segment block, the cross-segment responsible */
private final SplitFileInserterCrossSegmentStorage[] crossSegmentBlockSegments;
/** For each cross-segment block, the block number within that cross-segment */
private final int[] crossSegmentBlockNumbers;
private final boolean[] blocksHaveKeys;
private int blocksWithKeysCounter;
// These are only used in construction.
private transient final boolean[] crossDataBlocksAllocated;
private transient int crossDataBlocksAllocatedCount;
private transient int crossCheckBlocksAllocatedCount;
// These are also in parent but we need them here for easy access, especially as we don't want to
// make the byte[] visible.
/** For modern splitfiles, the crypto key is the same for every block. */
private final byte[] splitfileCryptoKey;
/** Crypto algorithm is the same for every block. */
private final byte splitfileCryptoAlgorithm;
/** LOCKING: Locked with (this) as needs to access encoded in chooseBlock */
private final SplitFileInserterSegmentBlockChooser blockChooser;
private boolean metadataDirty;
/** Set if the insert is cancelled. */
private boolean cancelled;
public SplitFileInserterSegmentStorage(SplitFileInserterStorage parent, int segNo,
boolean persistent, int dataBlocks, int checkBlocks, int crossCheckBlocks, int keyLength,
byte splitfileCryptoAlgorithm, byte[] splitfileCryptoKey, Random random, int maxRetries,
int consecutiveRNFsCountAsSuccess, KeysFetchingLocally keysFetching) {
this.parent = parent;
this.segNo = segNo;
this.dataBlockCount = dataBlocks;
this.checkBlockCount = checkBlocks;
this.crossCheckBlockCount = crossCheckBlocks;
totalBlockCount = dataBlockCount + crossCheckBlockCount + checkBlockCount;
this.keyLength = keyLength;
crossSegmentBlockSegments = new SplitFileInserterCrossSegmentStorage[crossCheckBlocks];
crossSegmentBlockNumbers = new int[crossCheckBlocks];
blocksHaveKeys = new boolean[totalBlockCount];
this.splitfileCryptoAlgorithm = splitfileCryptoAlgorithm;
this.splitfileCryptoKey = splitfileCryptoKey;
crossDataBlocksAllocated = new boolean[dataBlocks + crossCheckBlocks];
blockChooser = new SplitFileInserterSegmentBlockChooser(this, totalBlockCount, random,
maxRetries, keysFetching, consecutiveRNFsCountAsSuccess);
try {
CountedOutputStream cos = new CountedOutputStream(new NullOutputStream());
DataOutputStream dos = new DataOutputStream(cos);
innerStoreStatus(dos);
dos.close();
statusLength = (int) cos.written() + parent.checker.checksumLength();
} catch (IOException e) {
throw new Error(e); // Impossible
}
}
/** Create a segment from the fixed settings stored in the RAF by writeFixedSettings().
* @throws IOException
* @throws StorageFormatException */
public SplitFileInserterSegmentStorage(SplitFileInserterStorage parent, DataInputStream dis,
int segNo, int keyLength, byte splitfileCryptoAlgorithm, byte[] splitfileCryptoKey,
Random random, int maxRetries, int consecutiveRNFsCountAsSuccess,
KeysFetchingLocally keysFetching) throws IOException, StorageFormatException {
this.parent = parent;
this.segNo = segNo;
this.keyLength = keyLength;
dataBlockCount = dis.readInt();
if(dataBlockCount < 0)
throw new StorageFormatException("Bogus data block count");
crossCheckBlockCount = dis.readInt();
if(crossCheckBlockCount < 0)
throw new StorageFormatException("Bogus cross-check block count");
if((crossCheckBlockCount == 0) != (parent.crossSegments == null))
throw new StorageFormatException("Cross-check block count inconsistent with parent");
checkBlockCount = dis.readInt();
if(checkBlockCount < 0)
throw new StorageFormatException("Bogus check block count");
totalBlockCount = dataBlockCount + crossCheckBlockCount + checkBlockCount;
if(totalBlockCount > FECCodec.MAX_TOTAL_BLOCKS_PER_SEGMENT)
throw new StorageFormatException("Bogus total block count");
this.statusLength = dis.readInt();
if(statusLength < 0)
throw new StorageFormatException("Bogus status length");
crossSegmentBlockSegments = new SplitFileInserterCrossSegmentStorage[crossCheckBlockCount];
crossSegmentBlockNumbers = new int[crossCheckBlockCount];
blocksHaveKeys = new boolean[totalBlockCount];
this.splitfileCryptoAlgorithm = splitfileCryptoAlgorithm;
this.splitfileCryptoKey = splitfileCryptoKey;
crossDataBlocksAllocated = new boolean[dataBlockCount + crossCheckBlockCount];
blockChooser = new SplitFileInserterSegmentBlockChooser(this, totalBlockCount, random,
maxRetries, keysFetching, consecutiveRNFsCountAsSuccess);
try {
CountedOutputStream cos = new CountedOutputStream(new NullOutputStream());
DataOutputStream dos = new DataOutputStream(cos);
innerStoreStatus(dos);
dos.close();
int minStatusLength = (int) cos.written() + parent.checker.checksumLength();
if(minStatusLength > statusLength)
throw new StorageFormatException("Bad status length (too short)");
} catch (IOException e) {
throw new Error(e); // Impossible
}
}
// These two are only used in construction...
/** Allocate a cross-segment data block. Note that this algorithm must be reproduced exactly
* for splitfile compatibility; the Random seed is actually determined by the splitfile metadata.
* @param seg The cross-segment to allocate a block for.
* @param random PRNG seeded from the splitfile metadata, which determines which blocks to
* allocate in a deterministic manner.
* @return The data block number allocated.
*/
int allocateCrossDataBlock(SplitFileInserterCrossSegmentStorage seg, Random random) {
int size = dataBlockCount;
if(crossDataBlocksAllocatedCount == size) return -1;
int x = 0;
for(int i=0;i<10;i++) {
x = random.nextInt(size);
if(!crossDataBlocksAllocated[x]) {
crossDataBlocksAllocated[x] = true;
crossDataBlocksAllocatedCount++;
return x;
}
}
for(int i=0;i<size;i++) {
x++;
if(x == size) x = 0;
if(!crossDataBlocksAllocated[x]) {
crossDataBlocksAllocated[x] = true;
crossDataBlocksAllocatedCount++;
return x;
}
}
throw new IllegalStateException("Unable to allocate cross data block even though have not used all slots up???");
}
/** Allocate a cross-segment check block. **Note that this algorithm must be reproduced exactly
* for splitfile compatibility**; the Random seed is actually determined by the splitfile metadata.
* @param seg The cross-segment to allocate a block for.
* @param random PRNG seeded from the splitfile metadata, which determines which blocks to
* allocate in a deterministic manner.
* @param crossSegmentBlockNumber Block number within the cross-segment.
* @return The block number allocated (between dataBlockCount and dataBlockCount+crossSegmentCheckBlocks).
*/
int allocateCrossCheckBlock(SplitFileInserterCrossSegmentStorage seg, Random random, int crossSegmentBlockNumber) {
if(crossCheckBlocksAllocatedCount == crossCheckBlockCount) return -1;
int x = crossCheckBlockCount - (1 + random.nextInt(crossCheckBlockCount));
for(int i=0;i<crossCheckBlockCount;i++) {
x++;
if(x == crossCheckBlockCount) x = 0;
if(crossSegmentBlockSegments[x] == null) {
crossSegmentBlockSegments[x] = seg;
crossSegmentBlockNumbers[x] = crossSegmentBlockNumber;
crossCheckBlocksAllocatedCount++;
return x + dataBlockCount;
}
}
throw new IllegalStateException("Unable to allocate cross check block even though have not used all slots up???");
}
public void storeStatus(boolean force) {
if(!parent.persistent) return;
if(parent.hasFinished()) return;
try {
DataOutputStream dos;
synchronized(this) {
if(!force && !metadataDirty) return;
if(cancelled) return;
try {
dos = new DataOutputStream(parent.writeChecksummedTo(parent.segmentStatusOffset(segNo), statusLength));
innerStoreStatus(dos);
} catch (IOException e) {
Logger.error(this, "Impossible: "+e, e);
return;
}
metadataDirty = false;
}
// Outside the lock is safe since if we fail we will fail the whole splitfile.
dos.close();
} catch (IOException e) {
Logger.error(this, "I/O error writing segment status?: "+e, e);
parent.failOnDiskError(e);
}
}
private void innerStoreStatus(DataOutputStream dos) throws IOException {
dos.writeInt(segNo); // To make checksum different.
dos.writeBoolean(encoded);
blockChooser.write(dos);
}
public void readStatus() throws IOException, ChecksumFailedException, StorageFormatException {
byte[] data = new byte[statusLength-parent.checker.checksumLength()];
parent.preadChecksummed(parent.getOffsetSegmentStatus(segNo), data, 0, data.length);
DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data));
if(dis.readInt() != segNo) throw new StorageFormatException("Bad segment number");
encoded = dis.readBoolean();
blockChooser.read(dis);
}
public long storedStatusLength() {
return statusLength;
}
public void writeFixedSettings(DataOutputStream dos) throws IOException {
dos.writeInt(dataBlockCount);
dos.writeInt(crossCheckBlockCount);
dos.writeInt(checkBlockCount);
dos.writeInt(statusLength);
}
static int getKeyLength(SplitFileInserterStorage parent) {
return encodeKey(1, 1, ClientCHK.TEST_KEY, parent.hasSplitfileKey(), parent.checker, parent).length;
}
private static byte[] encodeKey(int segNo, int blockNumber, ClientCHK key,
boolean hasSplitfileKey, ChecksumChecker checker, SplitFileInserterStorage parent) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
try {
dos.writeInt(segNo);
dos.writeInt(blockNumber);
dos.writeByte(1); // 1 = present, 0 = not present
innerWriteKey(key, dos, hasSplitfileKey);
dos.close();
} catch (IOException e) {
throw new Error(e); // Impossible
}
byte[] fullBuf = baos.toByteArray();
byte[] bufNoKeyNumber = Arrays.copyOfRange(fullBuf, 8, fullBuf.length);
byte[] ret = checker.appendChecksum(bufNoKeyNumber);
return ret;
}
static void innerWriteKey(ClientCHK key, DataOutputStream dos, boolean hasSplitfileKey) throws IOException {
if(hasSplitfileKey) {
dos.write(key.getRoutingKey());
} else {
key.writeRawBinaryKey(dos);
}
}
void clearKeys() throws IOException {
// Just write 0's. Not valid.
// FIXME optimise (write the whole lot at once).
byte[] buf = new byte[keyLength];
for(int i=0;i<totalBlockCount;i++) {
parent.innerWriteSegmentKey(segNo, i, buf);
}
}
void setKey(int blockNumber, ClientCHK key) throws IOException {
if(logMINOR) Logger.minor(this, "Setting key "+key+" for block "+blockNumber+" on "+this, new Exception("debug"));
try {
ClientCHK oldKey = readKey(blockNumber);
if(!oldKey.equals(key))
throw new IOException("Key for block has changed! Data corruption or bugs in SplitFileInserter code");
} catch (MissingKeyException e) {
// Ok.
writeKey(blockNumber, key);
}
// Must be called either way as we don't regenerate blocksHaveKeys on startup.
setHasKey(blockNumber);
}
/** Write a key for a block.
* @param blockNo The block number. Can be a data block, cross segment check block or check
* block, in that numerical order.
* @param key The key to write.
* @throws IOException If we are unable to write the key. */
void writeKey(int blockNumber, ClientCHK key) throws IOException {
byte[] buf = encodeKey(segNo, blockNumber, key, parent.hasSplitfileKey(), parent.checker, parent);
parent.innerWriteSegmentKey(segNo, blockNumber, buf);
}
/** Set a flag indicating that we have a key. Call parent.onHasKeys if we have all of them.
* Note that this structure is not persisted! */
private void setHasKey(int blockNumber) {
synchronized(this) {
if(blocksHaveKeys[blockNumber]) return;
blocksHaveKeys[blockNumber] = true;
blocksWithKeysCounter++;
if(blocksWithKeysCounter != totalBlockCount) return;
}
parent.onHasKeys(this);
}
public synchronized boolean hasKeys() {
return blocksWithKeysCounter == totalBlockCount;
}
/** Called on startup to check which keys we actually have. Does nothing unless the segment
* claims to have been encoded already. FIXME consider calling this later on for robustness,
* but we would then need to re-encode ... */
public void checkKeys() {
synchronized(this) {
if(!encoded) return;
}
try {
for(int i=0;i<totalBlockCount;i++) {
readKey(i);
}
} catch (IOException e) {
parent.failOnDiskError(e);
return;
} catch (MissingKeyException e) {
// Easy to recover so may as well...
Logger.error(this, "Missing key even though segment encoded. Recovering by re-encoding...");
synchronized(this) {
encoded = false;
}
return;
}
}
public int storedKeysLength() {
return keyLength * totalBlockCount;
}
public byte[] readDataBlock(int blockNo) throws IOException {
assert(blockNo >= 0 && blockNo < dataBlockCount);
return parent.readSegmentDataBlock(segNo, blockNo);
}
private void writeCheckBlock(int checkBlockNo, byte[] buf) throws IOException {
parent.writeSegmentCheckBlock(segNo, checkBlockNo, buf);
}
public byte[] readCheckBlock(int checkBlockNo) throws IOException {
assert(checkBlockNo >= 0 && checkBlockNo < checkBlockCount);
return parent.readSegmentCheckBlock(segNo, checkBlockNo);
}
public synchronized void startEncode() {
if(encoded) return;
if(encoding) return;
encoding = true;
int totalBlockCount = dataBlockCount + checkBlockCount + crossCheckBlockCount;
long limit = totalBlockCount * CHKBlock.DATA_LENGTH +
Math.max(parent.codec.maxMemoryOverheadDecode(dataBlockCount, crossCheckBlockCount),
parent.codec.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();
innerEncode(chunk);
} catch (PersistenceDisabledException e) {
// Will be retried on restarting.
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(SplitFileInserterSegmentStorage.this) {
encoding = false;
}
parent.onFinishedEncoding(SplitFileInserterSegmentStorage.this);
}
} finally {
// Callback is part of the persistent job, unlock *after* calling it.
if(lock != null) lock.unlock(false, prio);
}
}
return true;
}
});
}
private void innerEncode(MemoryLimitedChunk chunk) {
RAFLock lock = null;
try {
synchronized(this) {
if(cancelled) return;
}
lock = parent.lockRAF();
if(logMINOR) Logger.minor(this, "Encoding "+this+" for "+parent);
byte[][] dataBlocks = readDataAndCrossCheckBlocks();
generateKeys(dataBlocks, 0);
byte[][] checkBlocks = new byte[checkBlockCount][];
for(int i=0;i<checkBlocks.length;i++)
checkBlocks[i] = new byte[CHKBlock.DATA_LENGTH];
if(dataBlocks == null || checkBlocks == null) return; // Failed with disk error.
parent.codec.encode(dataBlocks, checkBlocks, new boolean[checkBlocks.length], CHKBlock.DATA_LENGTH);
for(int i=0;i<checkBlocks.length;i++)
writeCheckBlock(i, checkBlocks[i]);
generateKeys(checkBlocks, dataBlockCount + crossCheckBlockCount);
synchronized(this) {
encoded = true;
}
if(logMINOR) Logger.minor(this, "Encoded "+this+" for "+parent);
} catch (IOException e) {
parent.failOnDiskError(e);
} catch (Throwable t) {
Logger.error(this, "Failed: "+t, t);
parent.fail(new InsertException(InsertExceptionMode.INTERNAL_ERROR, t, null));
} finally {
if(lock != null) lock.unlock();
}
}
/** Generate keys for each block and record them.
* @throws IOException */
private void generateKeys(byte[][] dataBlocks, int offset) throws IOException {
for(int i=0;i<dataBlocks.length;i++) {
setKey(i + offset, encodeBlock(dataBlocks[i]).getClientKey());
}
}
private byte[][] readDataAndCrossCheckBlocks() throws IOException {
byte[][] data = new byte[dataBlockCount + crossCheckBlockCount][];
RAFLock lock = parent.lockUnderlying();
try {
for(int i=0;i<dataBlockCount;i++)
data[i] = readDataBlock(i);
} finally {
lock.unlock();
}
for(int i=0;i<crossCheckBlockCount;i++)
data[i+dataBlockCount] = readCrossCheckBlock(i);
return data;
}
private byte[] readCrossCheckBlock(int blockNo) throws IOException {
return crossSegmentBlockSegments[blockNo].
readCheckBlock(crossSegmentBlockNumbers[blockNo], segNo, blockNo + dataBlockCount);
}
public synchronized boolean isFinishedEncoding() {
return encoded;
}
/** For unit tests. Generally for concurrency purposes we want something that won't change
* back, hence e.g. isFinishedEncoding(). */
synchronized boolean isEncoding() {
return encoding;
}
public ClientCHKBlock encodeBlock(int blockNo) throws IOException {
if(parent.isFinishing()) {
throw new IOException("Already finishing reading block "+blockNo+" for "+this+" for "+parent);
}
synchronized(this) {
if(this.blockChooser.hasSucceeded(blockNo)) {
Logger.error(this, "Already inserted block "+blockNo+" for "+this+" for "+parent);
throw new IOException("Already inserted block "+blockNo+" for "+this+" for "+parent);
}
}
byte[] buf = readBlock(blockNo);
return encodeBlock(buf);
}
private byte[] readBlock(int blockNo) throws IOException {
assert(blockNo >= 0 && blockNo < totalBlockCount);
if(blockNo < dataBlockCount)
return readDataBlock(blockNo);
else if(blockNo < dataBlockCount + crossCheckBlockCount)
return readCrossCheckBlock(blockNo - dataBlockCount);
else
return readCheckBlock(blockNo - (dataBlockCount + crossCheckBlockCount));
}
ClientCHKBlock encodeBlock(byte[] buf) {
assert (buf.length == CHKBlock.DATA_LENGTH);
ClientCHKBlock block;
try {
block = ClientCHKBlock.encodeSplitfileBlock(buf, splitfileCryptoKey,
splitfileCryptoAlgorithm);
} catch (CHKEncodeException e) {
throw new Error(e); // Impossible!
}
return block;
}
private ClientCHK innerReadKey(DataInputStream dis) throws IOException {
if(splitfileCryptoKey != null) {
byte[] routingKey = new byte[32];
dis.readFully(routingKey);
return new ClientCHK(routingKey, splitfileCryptoKey, false, splitfileCryptoAlgorithm,
(short)-1);
} else {
return new ClientCHK(dis);
}
}
ClientCHK readKey(int blockNumber) throws IOException, MissingKeyException {
byte[] buf = parent.innerReadSegmentKey(segNo, blockNumber);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeInt(segNo);
dos.writeInt(blockNumber);
dos.close();
byte[] prefix = baos.toByteArray();
byte[] checkBuf = new byte[prefix.length + buf.length];
System.arraycopy(prefix, 0, checkBuf, 0, prefix.length);
int checksumLength = parent.checker.checksumLength();
System.arraycopy(buf, 0, checkBuf, prefix.length, buf.length - checksumLength);
byte[] checksum = Arrays.copyOfRange(buf, buf.length - checksumLength, buf.length);
if(parent.checker.checkChecksum(checkBuf, 0, checkBuf.length, checksum))
throw new MissingKeyException();
DataInputStream dis = new DataInputStream(new ByteArrayInputStream(buf));
byte b = dis.readByte();
if(b != 1) throw new MissingKeyException();
ClientCHK key = innerReadKey(dis);
setHasKey(blockNumber);
if(logDEBUG) Logger.debug(this, "Returning "+key);
return key;
}
public class MissingKeyException extends Exception {
}
/** Has the segment completed all inserts?
* Should not change once we reach this state, but might be possible in case of disk errors
* causing losing keys etc. */
public synchronized boolean hasSucceeded() {
if(cancelled) return false;
return blockChooser.hasSucceededAll();
}
/** Has the segment encoded all check blocks and cross-check blocks? */
public synchronized boolean hasEncoded() {
return encoded;
}
/** Called when a block insert succeeds */
public void onInsertedBlock(int blockNo, ClientCHK key) {
try {
if(parent.hasFinished()) return;
this.setKey(blockNo, key);
if(blockChooser.onSuccess(blockNo))
parent.callback.onInsertedBlock();
lazyWriteMetadata();
} catch (IOException e) {
if(parent.hasFinished()) return; // Race condition possible as this is a callback
parent.failOnDiskError(e);
}
}
/** Called by BlockChooser when all blocks have been inserted. */
void onInsertedAllBlocks() {
if(logMINOR) Logger.minor(this, "Inserted all blocks in segment "+this);
synchronized(this) {
if(!encoded) return;
}
parent.segmentSucceeded(this);
}
public void onFailure(int blockNo, InsertException e) {
if(logMINOR) Logger.minor(this, "Failed block "+blockNo+" with "+e+" for "+this+" for "+parent);
if(parent.hasFinished()) return; // Race condition possible as this is a callback
parent.addFailure(e);
if(e.isFatal()) {
parent.failFatalErrorInBlock();
} else {
if(e.mode == InsertExceptionMode.ROUTE_NOT_FOUND &&
blockChooser.consecutiveRNFsCountAsSuccess > 0) {
try {
readKey(blockNo);
blockChooser.onRNF(blockNo);
parent.clearCooldown();
return;
} catch (MissingKeyException e1) {
Logger.error(this, "RNF but no key on block "+blockNo+" on "+this);
} catch (IOException e1) {
if(parent.hasFinished()) return; // Race condition possible as this is a callback
parent.failOnDiskError(e1);
return;
}
} else if(blockChooser.consecutiveRNFsCountAsSuccess > 0) {
if(blockChooser.pushRNFs(blockNo)) {
parent.failTooManyRetriesInBlock();
return;
}
}
if(blockChooser.onNonFatalFailure(blockNo)) {
parent.failTooManyRetriesInBlock();
} else {
if(blockChooser.maxRetries >= 0) lazyWriteMetadata();
parent.clearCooldown();
}
}
}
private void lazyWriteMetadata() {
synchronized(this) {
metadataDirty = true;
}
parent.lazyWriteMetadata();
}
public synchronized boolean hasCompletedOrFailed() {
if(encoded) return true; // No more encoding jobs will run.
if(encoding) return false; // Waiting for job to finish.
if(cancelled) return true;
if(blockChooser.hasSucceededAll()) return true;
return false;
}
/** Caller must check hasCompletedOrFailed() explicitly after calling cancel() on all
* segments.
* @return True if the segment has completed cancellation. False if it is waiting for an
* encode, in which case a callback to parent will be made when the encode finishes. */
public synchronized boolean cancel() {
if(cancelled) return false;
cancelled = true;
if(hasCompletedOrFailed()) return true;
return false;
}
public synchronized BlockInsert chooseBlock() {
int chosenBlock = innerChooseBlock();
if(chosenBlock == -1) return null;
return new BlockInsert(this, chosenBlock);
}
synchronized int innerChooseBlock() {
if(cancelled) return -1;
return blockChooser.chooseKey();
}
static final class BlockInsert implements SendableRequestItemKey, SendableRequestItem {
final SplitFileInserterSegmentStorage segment;
final int blockNumber;
final int hashCode;
BlockInsert(SplitFileInserterSegmentStorage segment, int blockNumber) {
this.segment = segment;
this.blockNumber = blockNumber;
hashCode = computeHashCode();
}
private int computeHashCode() {
final int prime = 31;
int result = 1;
result = prime * result + blockNumber;
result = prime * result + ((segment == null) ? 0 : segment.hashCode());
return result;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof BlockInsert))
return false;
BlockInsert other = (BlockInsert) obj;
if (blockNumber != other.blockNumber)
return false;
return segment == other.segment;
}
@Override
public void dump() {
// Do nothing. We don't encode in advance.
}
@Override
public SendableRequestItemKey getKey() {
return this;
}
public String toString() {
return "BlockInsert:"+segment+":"+blockNumber+"@memory:"+super.hashCode();
}
}
/** Set the cross-segment associated with a cross-check block, which tells us how to read that
* block from disk.
* @param crossSegment The cross-segment.
* @param segmentBlockNumber The block number within this segment
* @param crossSegmentBlockNumber The cross-check block number (usually between 0 and 2 inclusive).
*/
void setCrossCheckBlock(SplitFileInserterCrossSegmentStorage crossSegment,
int segmentBlockNumber, int crossSegmentBlockNumber) {
crossSegmentBlockSegments[segmentBlockNumber-dataBlockCount] = crossSegment;
crossSegmentBlockNumbers[segmentBlockNumber-dataBlockCount] = crossSegmentBlockNumber;
}
public int countSendableKeys() {
return blockChooser.countFetchable();
}
public String toString() {
return super.toString()+":"+parent;
}
}