/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.io.xfer;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import freenet.io.comm.AsyncMessageCallback;
import freenet.io.comm.AsyncMessageFilterCallback;
import freenet.io.comm.ByteCounter;
import freenet.io.comm.DMT;
import freenet.io.comm.DisconnectedException;
import freenet.io.comm.Message;
import freenet.io.comm.MessageFilter;
import freenet.io.comm.NotConnectedException;
import freenet.io.comm.PeerContext;
import freenet.node.PrioRunnable;
import freenet.support.BitArray;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.io.NativeThread;
/**
* Bulk data transfer (not block). Bulk transfer is designed for files which may be much bigger than a
* key block, and where we have the whole file at the outset. Do not persist across node restarts.
*
* Used by update over mandatory, sending a file to our peers attached to an N2NTM etc.
* @author toad
*/
public class BulkTransmitter {
public interface AllSentCallback {
void allSent(BulkTransmitter bulkTransmitter, boolean anyFailed);
}
/** If no packets sent in this period, and no completion acknowledgement / cancellation, assume failure. */
static final long TIMEOUT = MINUTES.toMillis(5);
/** Time to hang around listening for the last FNPBulkReceivedAll message */
static final long FINAL_ACK_TIMEOUT = SECONDS.toMillis(10);
final AllSentCallback allSentCallback;
/** Available blocks */
final PartiallyReceivedBulk prb;
/** Peer who we are sending the data to */
final PeerContext peer;
/** Transfer UID for messages */
final long uid;
/** Blocks we have but haven't sent yet. 0 = block sent or not present, 1 = block present but not sent */
final BitArray blocksNotSentButPresent;
private boolean cancelled;
/** Not persistent over reboots */
final long peerBootID;
private boolean sentCancel;
private boolean finished;
/** Not expecting a response? */
final boolean noWait;
private long finishTime=-1;
private String cancelReason;
private final ByteCounter ctr;
private final boolean realTime;
private static long transfersCompleted;
private static long transfersSucceeded;
private static volatile boolean logMINOR;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
}
});
}
public BulkTransmitter(PartiallyReceivedBulk prb, PeerContext peer, long uid, boolean noWait, ByteCounter ctr, boolean realTime) throws DisconnectedException {
this(prb, peer, uid, noWait, ctr, realTime, null);
}
/**
* Create a bulk data transmitter.
* @param prb The PartiallyReceivedBulk containing the file we want to send, or the part of it that we have so far.
* @param peer The peer we want to send it to.
* @param uid The unique identifier for this data transfer
* @param masterThrottle The overall output throttle
* @param noWait If true, don't wait for an FNPBulkReceivedAll, return as soon as we've sent everything.
* @throws DisconnectedException If the peer we are trying to send to becomes disconnected.
*/
public BulkTransmitter(PartiallyReceivedBulk prb, PeerContext peer, long uid, boolean noWait, ByteCounter ctr, boolean realTime, AllSentCallback cb) throws DisconnectedException {
this.prb = prb;
this.peer = peer;
this.uid = uid;
this.noWait = noWait;
this.ctr = ctr;
this.realTime = realTime;
this.allSentCallback = cb;
if(ctr == null) throw new NullPointerException();
peerBootID = peer.getBootID();
// Need to sync on prb while doing both operations, to avoid race condition.
// Specifically, we must not get calls to blockReceived() until blocksNotSentButPresent
// has been set, AND it must be accurate, so there must not be an unlocked period
// between cloning and adding.
synchronized(prb) {
// We can just clone it.
blocksNotSentButPresent = prb.cloneBlocksReceived();
prb.add(this);
}
try {
prb.usm.addAsyncFilter(MessageFilter.create().setNoTimeout().setSource(peer).setType(DMT.FNPBulkReceiveAborted).setField(DMT.UID, uid),
new AsyncMessageFilterCallback() {
@Override
public void onMatched(Message m) {
cancel("Other side sent FNPBulkReceiveAborted");
}
@Override
public boolean shouldTimeout() {
synchronized(BulkTransmitter.this) {
if(cancelled || finished) return true;
}
if(BulkTransmitter.this.prb.isAborted()) return true;
return false;
}
@Override
public void onTimeout() {
// Ignore
}
@Override
public void onDisconnect(PeerContext ctx) {
// Ignore
}
@Override
public void onRestarted(PeerContext ctx) {
// Ignore
}
}, ctr);
prb.usm.addAsyncFilter(MessageFilter.create().setNoTimeout().setSource(peer).setType(DMT.FNPBulkReceivedAll).setField(DMT.UID, uid),
new AsyncMessageFilterCallback() {
@Override
public void onMatched(Message m) {
// send() will terminate, so must call setAllQueued().
setAllQueued();
completed();
}
@Override
public boolean shouldTimeout() {
synchronized(BulkTransmitter.this) {
if (cancelled) return true;
if (finished) return (System.currentTimeMillis()-finishTime > FINAL_ACK_TIMEOUT);
}
if(BulkTransmitter.this.prb.isAborted()) return true;
return false;
}
@Override
public void onTimeout() {
// Ignore
}
@Override
public void onDisconnect(PeerContext ctx) {
// Ignore
}
@Override
public void onRestarted(PeerContext ctx) {
// Ignore
}
}, ctr);
} catch (DisconnectedException e) {
cancel("Disconnected");
throw e;
}
}
/**
* Received a block. Set the relevant bit to 1 to indicate that we have the block but haven't sent
* it yet. **Only called by PartiallyReceivedBulk.**
* @param block The block number that has been received.
*/
synchronized void blockReceived(int block) {
blocksNotSentButPresent.setBit(block, true);
notifyAll();
}
/**
* Called when the PRB is aborted.
*/
public void onAborted() {
sendAbortedMessage();
synchronized(this) {
notifyAll();
}
}
private void sendAbortedMessage() {
synchronized(this) {
if(sentCancel) return;
sentCancel = true;
}
try {
peer.sendAsync(DMT.createFNPBulkSendAborted(uid), null, ctr);
} catch (NotConnectedException e) {
// Cool
}
}
public void cancel(String reason) {
if(logMINOR)
Logger.minor(this, "Cancelling "+this);
sendAbortedMessage();
synchronized(this) {
if(cancelled || finished) return;
cancelled = true;
cancelReason = reason;
notifyAll();
}
prb.remove(this);
synchronized(BulkTransmitter.class) {
transfersCompleted++;
}
// Call AllSentCallback if necessary.
// If there are packets still waiting, it will be called after they are sent or failed.
setAllQueued();
}
/** Like cancel(), but without the negative overtones: The client says it's got everything,
* we believe them (even if we haven't sent everything; maybe they had a partial). */
public void completed() {
synchronized(this) {
if(cancelled || finished) return;
finished = true;
finishTime = System.currentTimeMillis();
notifyAll();
}
prb.remove(this);
synchronized(BulkTransmitter.class) {
transfersCompleted++;
transfersSucceeded++;
}
if(logMINOR) Logger.minor(this, "Completed transfer successfully "+this);
}
/**
* Send the file.
* @return True if the file was successfully sent. False otherwise.
* @throws DisconnectedException
*/
public boolean send() throws DisconnectedException {
long lastSentPacket = System.currentTimeMillis();
outer: while(true) {
int max = Math.min(Integer.MAX_VALUE, prb.blocks);
max = Math.min(max, (int)Math.min(Integer.MAX_VALUE, peer.getThrottleWindowSize()));
// FIXME Need to introduce the global limiter of [code]max[/code] for memory management instead of hard-code for each, no?
max = Math.min(max, 100);
if(max < 1) max = 1;
if(prb.isAborted()) {
if(logMINOR)
Logger.minor(this, "Aborted "+this);
return false;
}
int blockNo;
if(peer.getBootID() != peerBootID) {
synchronized(this) {
cancelled = true;
notifyAll();
}
prb.remove(BulkTransmitter.this);
if(logMINOR)
Logger.minor(this, "Failed to send "+uid+": peer restarted: "+peer);
throw new DisconnectedException();
}
synchronized(this) {
if(finished) return true;
if(cancelled) return false;
blockNo = blocksNotSentButPresent.firstOne();
}
if(blockNo < 0) {
setAllQueued();
if(noWait && prb.hasWholeFile()) {
completed();
return true;
}
synchronized(this) {
// Wait for all packets to complete
while(true) {
if(failedPacket) {
cancel("Packet send failed");
return false;
}
if(logMINOR)
Logger.minor(this, "Waiting for packets: remaining: "+inFlightPackets);
if(inFlightPackets == 0) break;
try {
wait();
if(failedPacket) {
cancel("Packet send failed");
return false;
}
if(inFlightPackets == 0) break;
continue outer; // Might be a packet...
} catch (InterruptedException e) {
// Ignore
}
}
// Wait for a packet to come in, BulkReceivedAll or BulkReceiveAborted
try {
wait(SECONDS.toMillis(60));
} catch (InterruptedException e) {
// No problem
continue;
}
}
long end = System.currentTimeMillis();
if(end - lastSentPacket > TIMEOUT) {
Logger.error(this, "Send timed out on "+this);
cancel("Timeout awaiting BulkReceivedAll");
return false;
}
continue;
}
// Send a packet
byte[] buf = prb.getBlockData(blockNo);
if(buf == null) {
if(logMINOR)
Logger.minor(this, "Block "+blockNo+" is null, presumably the send is cancelled: "+this);
// Already cancelled, quit
return false;
}
// Congestion control and bandwidth limiting
try {
if(logMINOR) Logger.minor(this, "Sending packet "+blockNo);
Message msg = DMT.createFNPBulkPacketSend(uid, blockNo, buf, realTime);
UnsentPacketTag tag = new UnsentPacketTag();
peer.sendAsync(msg, tag, ctr);
synchronized(this) {
while(inFlightPackets >= max && !failedPacket)
try {
wait(1000);
} catch (InterruptedException e) {
// Ignore
}
}
synchronized(this) {
blocksNotSentButPresent.setBit(blockNo, false);
}
lastSentPacket = System.currentTimeMillis();
} catch (NotConnectedException e) {
cancel("Disconnected");
if(logMINOR)
Logger.minor(this, "Cancelled: not connected "+this);
throw new DisconnectedException();
}
}
}
private void setAllQueued() {
if(allSentCallback != null) {
boolean callAllSent = false;
boolean anyFailed = false;
synchronized(this) {
allQueued = true;
if(unsentPackets == 0 && !calledAllSent) {
if(logMINOR) Logger.minor(this, "Calling all sent callback on "+this);
callAllSent = true;
calledAllSent = true;
anyFailed = failedPacket;
} else if(!calledAllSent) {
if(logMINOR) Logger.minor(this, "Still waiting for "+unsentPackets);
}
}
if(callAllSent) {
callAllSentCallbackInner(anyFailed);
}
}
}
private void callAllSentCallbackInner(final boolean anyFailed) {
prb.usm.getExecutor().execute(new PrioRunnable() {
@Override
public void run() {
allSentCallback.allSent(BulkTransmitter.this, anyFailed);
}
@Override
public int getPriority() {
return NativeThread.HIGH_PRIORITY;
}
});
}
private int inFlightPackets = 0;
private int unsentPackets = 0;
private boolean failedPacket = false;
private boolean allQueued = false;
private boolean calledAllSent = false;
private class UnsentPacketTag implements AsyncMessageCallback {
private boolean finished;
private boolean sent;
private UnsentPacketTag() {
synchronized(BulkTransmitter.this) {
inFlightPackets++;
unsentPackets++;
}
}
@Override
public void acknowledged() {
complete(false);
}
private void complete(boolean failed) {
synchronized(this) {
if(finished) return;
finished = true;
notifyAll();
}
if(!failed)
ctr.sentPayload(prb.blockSize);
synchronized(BulkTransmitter.this) {
if(failed) {
failedPacket = true;
BulkTransmitter.this.notifyAll();
if(logMINOR) Logger.minor(this, "Packet failed for "+BulkTransmitter.this);
} else {
inFlightPackets--;
BulkTransmitter.this.notifyAll();
if(logMINOR) Logger.minor(this, "Packet sent "+BulkTransmitter.this+" remaining in flight: "+inFlightPackets);
}
}
sent(true);
}
@Override
public void disconnected() {
complete(true);
}
@Override
public void fatalError() {
complete(true);
}
@Override
public void sent() {
sent(false);
}
public void sent(boolean ignoreFinished) {
if(allSentCallback == null) return;
synchronized(this) {
if(finished && !ignoreFinished) return;
if(sent) return;
sent = true;
notifyAll();
}
final boolean anyFailed;
synchronized(BulkTransmitter.this) {
unsentPackets--;
if(unsentPackets > 0) return;
if(!allQueued) return;
if(calledAllSent) return;
calledAllSent = true;
anyFailed = failedPacket;
}
if(logMINOR) Logger.minor(this, "Calling all sent callback on "+this);
callAllSentCallbackInner(anyFailed);
}
}
@Override
public String toString() {
return "BulkTransmitter:"+uid+":"+peer.shortToString();
}
public String getCancelReason() {
return cancelReason;
}
public static synchronized long[] transferSuccess() {
return new long[] { transfersCompleted, transfersSucceeded };
}
}