/* 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.node; import static java.util.concurrent.TimeUnit.SECONDS; 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.io.comm.RetrievalException; import freenet.io.comm.SlowAsyncMessageFilterCallback; import freenet.io.xfer.AbortedException; import freenet.io.xfer.BlockReceiver; import freenet.io.xfer.BlockReceiver.BlockReceiverCompletion; import freenet.io.xfer.BlockReceiver.BlockReceiverTimeoutHandler; import freenet.io.xfer.PartiallyReceivedBlock; import freenet.keys.CHKBlock; import freenet.keys.CHKVerifyException; import freenet.keys.NodeCHK; import freenet.store.KeyCollisionException; import freenet.support.HexUtil; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.ShortBuffer; import freenet.support.Logger.LogLevel; import freenet.support.io.NativeThread; /** * @author amphibian * * Handle an incoming insert request. * This corresponds to RequestHandler. */ public class CHKInsertHandler implements PrioRunnable, ByteCounter { private static volatile boolean logMINOR; static { Logger.registerLogThresholdCallback(new LogThresholdCallback(){ @Override public void shouldUpdate(){ logMINOR = Logger.shouldLog(LogLevel.MINOR, this); } }); } static final long DATA_INSERT_TIMEOUT = SECONDS.toMillis(10); final Node node; final long uid; final PeerNode source; final NodeCHK key; final long startTime; private short htl; private CHKInsertSender sender; private byte[] headers; private BlockReceiver br; private Thread runThread; PartiallyReceivedBlock prb; final InsertTag tag; private boolean canWriteDatastore; private final boolean forkOnCacheable; private final boolean preferInsert; private final boolean ignoreLowBackoff; private final boolean realTimeFlag; CHKInsertHandler(NodeCHK key, short htl, PeerNode source, long id, Node node, long startTime, InsertTag tag, boolean forkOnCacheable, boolean preferInsert, boolean ignoreLowBackoff, boolean realTimeFlag) { this.node = node; this.uid = id; this.source = source; this.startTime = startTime; this.tag = tag; this.key = key; this.htl = htl; canWriteDatastore = node.canWriteDatastoreInsert(htl); this.forkOnCacheable = forkOnCacheable; this.preferInsert = preferInsert; this.ignoreLowBackoff = ignoreLowBackoff; this.realTimeFlag = realTimeFlag; } @Override public String toString() { return super.toString()+" for "+uid; } @Override public void run() { freenet.support.Logger.OSThread.logPID(this); try { realRun(); } catch (Throwable t) { Logger.error(this, "Caught in run() "+t, t); tag.handlerThrew(t); } finally { if(logMINOR) Logger.minor(this, "Exiting CHKInsertHandler.run() for "+uid); tag.unlockHandler(); } } private void realRun() { runThread = Thread.currentThread(); // FIXME implement rate limiting or something! // Send Accepted Message accepted = DMT.createFNPAccepted(uid); try { //Using sendSync here will help the next message filter not timeout... wait here or at the message filter. source.sendSync(accepted, this, realTimeFlag); } catch (NotConnectedException e1) { if(logMINOR) Logger.minor(this, "Lost connection to source"); return; } catch (SyncSendWaitedTooLongException e) { Logger.error(this, "Unable to send "+accepted+" in a reasonable time to "+source); return; } if(tag.shouldSlowDown()) { try { source.sendAsync(DMT.createFNPRejectedOverload(uid, false, false, realTimeFlag), null, this); } catch (NotConnectedException e) { // Ignore. } } // Source will send us a DataInsert MessageFilter mf; mf = makeDataInsertFilter(DATA_INSERT_TIMEOUT); Message msg; try { msg = node.usm.waitFor(mf, this); } catch (DisconnectedException e) { Logger.normal(this, "Disconnected while waiting for DataInsert on "+uid); return; } if(logMINOR) Logger.minor(this, "Received "+msg); if(msg == null) { handleNoDataInsert(); return; } if(msg.getSpec() == DMT.FNPDataInsertRejected) { try { source.sendAsync(DMT.createFNPDataInsertRejected(uid, msg.getShort(DMT.DATA_INSERT_REJECTED_REASON)), null, this); } catch (NotConnectedException e) { // Ignore. } return; } // We have a DataInsert headers = ((ShortBuffer)msg.getObject(DMT.BLOCK_HEADERS)).getData(); // FIXME check the headers // Now create an CHKInsertSender, or use an existing one, or // discover that the data is in the store. // From this point onwards, if we return cleanly we must go through finish(). prb = new PartiallyReceivedBlock(Node.PACKETS_IN_BLOCK, Node.PACKET_SIZE); if(htl > 0) sender = node.makeInsertSender(key, htl, uid, tag, source, headers, prb, false, false, forkOnCacheable, preferInsert, ignoreLowBackoff, realTimeFlag); br = new BlockReceiver(node.usm, source, uid, prb, this, node.getTicker(), false, realTimeFlag, myTimeoutHandler, false); // Receive the data, off thread Runnable dataReceiver = new DataReceiver(); receiveStarted = true; node.executor.execute(dataReceiver, "CHKInsertHandler$DataReceiver for UID "+uid); // Wait... // What do we want to wait for? // If the data receive completes, that's very nice, // but doesn't really matter. What matters is what // happens to the CHKInsertSender. If the data receive // fails, that does matter... // We are waiting for a terminal status on the CHKInsertSender, // including SUCCESS. // If we get transfer failed, we can check whether the receive // failed first. If it did it's not our fault. // If the receive failed, and we haven't started transferring // yet, we probably want to kill the sender. // So we call the wait method on the CHKInsertSender, but we // also have a flag locally to indicate the receive failed. // And if it does, we interrupt. boolean receivedRejectedOverload = false; while(true) { synchronized(sender) { try { if(sender.getStatus() == CHKInsertSender.NOT_FINISHED) sender.wait(5000); } catch (InterruptedException e) { // Cool, probably this is because the receive failed... } } if(receiveFailed()) { // Nothing else we can do finish(CHKInsertSender.RECEIVE_FAILED); return; } if((!receivedRejectedOverload) && sender.receivedRejectedOverload()) { receivedRejectedOverload = true; // Forward it // Does not need to be sent synchronously since is non-terminal. Message m = DMT.createFNPRejectedOverload(uid, false, true, realTimeFlag); try { source.sendAsync(m, null, this); } catch (NotConnectedException e) { if(logMINOR) Logger.minor(this, "Lost connection to source"); return; } } int status = sender.getStatus(); if(status == CHKInsertSender.NOT_FINISHED) { continue; } // Local RejectedOverload's (fatal). // Internal error counts as overload. It'd only create a timeout otherwise, which is the same thing anyway. if((status == CHKInsertSender.TIMED_OUT) || (status == CHKInsertSender.GENERATED_REJECTED_OVERLOAD) || (status == CHKInsertSender.INTERNAL_ERROR)) { msg = DMT.createFNPRejectedOverload(uid, true, true, realTimeFlag); try { source.sendSync(msg, this, realTimeFlag); } catch (NotConnectedException e) { if(logMINOR) Logger.minor(this, "Lost connection to source"); return; } catch (SyncSendWaitedTooLongException e) { Logger.error(this, "Took too long to send "+msg+" to "+source); return; } // Might as well store it anyway. if((status == CHKInsertSender.TIMED_OUT) || (status == CHKInsertSender.GENERATED_REJECTED_OVERLOAD)) canCommit = true; finish(status); return; } if((status == CHKInsertSender.ROUTE_NOT_FOUND) || (status == CHKInsertSender.ROUTE_REALLY_NOT_FOUND)) { msg = DMT.createFNPRouteNotFound(uid, sender.getHTL()); try { source.sendSync(msg, this, realTimeFlag); } catch (NotConnectedException e) { if(logMINOR) Logger.minor(this, "Lost connection to source"); return; } catch (SyncSendWaitedTooLongException e) { Logger.error(this, "Took too long to send "+msg+" to "+source); return; } canCommit = true; finish(status); return; } if(status == CHKInsertSender.RECEIVE_FAILED) { // Probably source's fault. finish(status); return; } if(status == CHKInsertSender.SUCCESS) { msg = DMT.createFNPInsertReply(uid); try { source.sendSync(msg, this, realTimeFlag); } catch (NotConnectedException e) { Logger.minor(this, "Lost connection to source"); return; } catch (SyncSendWaitedTooLongException e) { Logger.error(this, "Took too long to send "+msg+" to "+source); return; } canCommit = true; finish(status); return; } // Otherwise...? Logger.error(this, "Unknown status code: "+sender.getStatusString()); msg = DMT.createFNPRejectedOverload(uid, true, true, realTimeFlag); try { source.sendSync(msg, this, realTimeFlag); } catch (NotConnectedException e) { // Ignore } catch (SyncSendWaitedTooLongException e) { // Ignore } finish(CHKInsertSender.INTERNAL_ERROR); return; } } private MessageFilter makeDataInsertFilter(long timeout) { MessageFilter mfDataInsert = MessageFilter.create().setType(DMT.FNPDataInsert).setField(DMT.UID, uid).setSource(source).setTimeout(timeout); // DataInsertRejected means the transfer failed upstream so a DataInsert will not be sent. MessageFilter mfDataInsertRejected = MessageFilter.create().setType(DMT.FNPDataInsertRejected).setField(DMT.UID, uid).setSource(source).setTimeout(timeout); return mfDataInsert.or(mfDataInsertRejected); } private void handleNoDataInsert() { try { // Nodes wait until they have the DataInsert before forwarding, so there is absolutely no excuse: There is a local problem here! if(source.isConnected() && (startTime > (source.timeLastConnectionCompleted()+Node.HANDSHAKE_TIMEOUT*4))) Logger.warning(this, "Did not receive DataInsert on "+uid+" from "+source+" !"); Message tooSlow = DMT.createFNPRejectedTimeout(uid); source.sendAsync(tooSlow, null, this); Message m = DMT.createFNPInsertTransfersCompleted(uid, true); source.sendAsync(m, null, this); prb = new PartiallyReceivedBlock(Node.PACKETS_IN_BLOCK, Node.PACKET_SIZE); br = new BlockReceiver(node.usm, source, uid, prb, this, node.getTicker(), false, realTimeFlag, null, false); prb.abort(RetrievalException.NO_DATAINSERT, "No DataInsert", true); source.localRejectedOverload("TimedOutAwaitingDataInsert", realTimeFlag); // Two stage timeout. Don't go fatal unless no response in 60 seconds. // Yes it's ugly everywhere but since we have a longish connection timeout it's necessary everywhere. :| // FIXME review two stage timeout everywhere with some low level networking guru. MessageFilter mf = makeDataInsertFilter(SECONDS.toMillis(60)); node.usm.addAsyncFilter(mf, new SlowAsyncMessageFilterCallback() { @Override public void onMatched(Message m) { // Okay, great. // Either we got a DataInsert, in which case the transfer was aborted above, or we got a DataInsertRejected, which means it never started. // FIXME arguably we should wait until we have the message before sending the transfer cancel in case the message gets lost? Or maybe not? // FIXME unlock here rather than in finally block in realRun??? Unlocking in the finally block is safe (won't cause rejects), unlocking here might be more accurate ... } @Override public boolean shouldTimeout() { return false; } @Override public void onTimeout() { Logger.error(this, "No DataInsert for "+CHKInsertHandler.this+" from "+source+" ("+source.getVersionNumber()+")"); // Fatal timeout. Something is seriously busted. // We've waited long enough that we know it's not just a connectivity problem - if it was we'd have disconnected by now. source.fatalTimeout(); } @Override public void onDisconnect(PeerContext ctx) { // Okay. Somewhat expected, it was having problems. } @Override public void onRestarted(PeerContext ctx) { // Okay. } @Override public int getPriority() { return NativeThread.NORM_PRIORITY; } }, this); return; } catch (NotConnectedException e) { if(logMINOR) Logger.minor(this, "Lost connection to source"); return; } catch (DisconnectedException e) { if(logMINOR) Logger.minor(this, "Lost connection to source"); return; } } private boolean canCommit = false; private boolean sentCompletion = false; private Object sentCompletionLock = new Object(); /** * If canCommit, and we have received all the data, and it * verifies, then commit it. */ private void finish(int code) { if(logMINOR) Logger.minor(this, "Waiting for receive"); long transferTimeout = realTimeFlag ? CHKInsertSender.TRANSFER_COMPLETION_ACK_TIMEOUT_REALTIME : CHKInsertSender.TRANSFER_COMPLETION_ACK_TIMEOUT_BULK; synchronized(this) { while(receiveStarted && !receiveCompleted) { try { wait(SECONDS.toMillis(100)); } catch (InterruptedException e) { // Ignore } } } CHKBlock block = verify(); // If we wanted to reduce latency at the cost of security (bug 3338), we'd commit here, or even on the receiver thread. // Wait for completion boolean sentCompletionWasSet; synchronized(sentCompletionLock) { sentCompletionWasSet = sentCompletion; sentCompletion = true; } Message m=null; boolean routingTookTooLong = false; if((sender != null) && (!sentCompletionWasSet)) { if(logMINOR) Logger.minor(this, "Waiting for completion"); long startedTime = System.currentTimeMillis(); //If there are downstream senders, our final success report depends on there being no timeouts in the chain. while(true) { synchronized(sender) { if(sender.completed()) { break; } try { int t = (int)Math.min(Integer.MAX_VALUE, startedTime + transferTimeout - System.currentTimeMillis()); if(t > 0) sender.wait(t); else { routingTookTooLong = true; break; } } catch (InterruptedException e) { // Loop } } } if(routingTookTooLong) { tag.timedOutToHandlerButContinued(); sentCompletionWasSet = true; try { source.sendAsync(DMT.createFNPInsertTransfersCompleted(uid, true), null, this); } catch (NotConnectedException e) { // Ignore. } Logger.error(this, "Insert took too long, telling downstream that it's finished and reassigning to self on "+this); // Still waiting. while(true) { synchronized(sender) { if(sender.completed()) { break; } try { sender.wait(SECONDS.toMillis(10)); } catch (InterruptedException e) { // Loop } } } if(logMINOR) Logger.minor(this, "Completed after telling downstream on "+this); } boolean failed = sender.anyTransfersFailed(); if(!sentCompletionWasSet) m = DMT.createFNPInsertTransfersCompleted(uid, failed); } if((sender == null) && (!sentCompletionWasSet) && (canCommit)) { //There are no downstream senders, but we stored the data locally, report successful transfer. //Note that this is done even if the verify fails. m = DMT.createFNPInsertTransfersCompleted(uid, false /* no timeouts */); } // Don't commit until after we have received all the downstream transfer completion notifications. // We don't want an attacker to see a ULPR notice from the inserter before he sees it from the end of the chain (bug 3338). if(block != null) { commit(block); block = null; } // Be generous with unlocking incoming requests, and cautious with // unlocking outgoing requests, hence avoid problems. If we wait until // the completion has been acknowledged, then there will be a period // during which the originator thinks we have unlocked but we haven't, // which will cause unnecessary rejects and thus mandatory backoff. tag.unlockHandler(); if(m != null) { try { // We do need to sendSync here so we have accurate byte counter totals. source.sendSync(m, this, realTimeFlag); if(logMINOR) Logger.minor(this, "Sent completion: "+m+" for "+this); } catch (NotConnectedException e1) { if(logMINOR) Logger.minor(this, "Not connected: "+source+" for "+this); // May need to commit anyway... } catch (SyncSendWaitedTooLongException e) { Logger.error(this, "Took too long to send "+m+" to "+source); // May need to commit anyway... } } if(code != CHKInsertSender.TIMED_OUT && code != CHKInsertSender.GENERATED_REJECTED_OVERLOAD && code != CHKInsertSender.INTERNAL_ERROR && code != CHKInsertSender.ROUTE_REALLY_NOT_FOUND && code != CHKInsertSender.RECEIVE_FAILED && !receiveFailed()) { int totalSent = getTotalSentBytes(); int totalReceived = getTotalReceivedBytes(); if(sender != null) { totalSent += sender.getTotalSentBytes(); totalReceived += sender.getTotalReceivedBytes(); } if(logMINOR) Logger.minor(this, "Remote CHK insert cost "+totalSent+ '/' +totalReceived+" bytes ("+code+ ") receive failed = "+receiveFailed()); node.nodeStats.remoteChkInsertBytesSentAverage.report(totalSent); node.nodeStats.remoteChkInsertBytesReceivedAverage.report(totalReceived); if(code == CHKInsertSender.SUCCESS) { // Report both sent and received because we have both a Handler and a Sender if(sender != null && sender.startedSendingData()) node.nodeStats.successfulChkInsertBytesSentAverage.report(totalSent); node.nodeStats.successfulChkInsertBytesReceivedAverage.report(totalReceived); } } } /** * Verify data, or send DataInsertRejected. */ private CHKBlock verify() { Message toSend = null; CHKBlock block = null; synchronized(this) { if((prb == null) || prb.isAborted()) return null; try { if(!canCommit) return null; if(!prb.allReceived()) return null; block = new CHKBlock(prb.getBlock(), headers, key); } catch (CHKVerifyException e) { Logger.error(this, "Verify failed in CHKInsertHandler: "+e+" - headers: "+HexUtil.bytesToHex(headers), e); toSend = DMT.createFNPDataInsertRejected(uid, DMT.DATA_INSERT_REJECTED_VERIFY_FAILED); } catch (AbortedException e) { Logger.error(this, "Receive failed: "+e); // Receiver thread (below) will handle sending the failure notice } } if(toSend != null) { try { source.sendAsync(toSend, null, this); } catch (NotConnectedException e) { // :( if(logMINOR) Logger.minor(this, "Lost connection in "+this+" when sending FNPDataInsertRejected"); } } return block; } private void commit(CHKBlock block) { try { node.store(block, node.shouldStoreDeep(key, source, sender == null ? new PeerNode[0] : sender.getRoutedTo()), false, canWriteDatastore, false); } catch (KeyCollisionException e) { // Impossible with CHKs. } if(logMINOR) Logger.minor(this, "Committed"); } /** Has the receive failed? If so, there's not much more that can be done... */ private boolean receiveFailed; private boolean receiveStarted; private boolean receiveCompleted; public class DataReceiver implements PrioRunnable { @Override public void run() { freenet.support.Logger.OSThread.logPID(this); if(logMINOR) Logger.minor(this, "Receiving data for "+CHKInsertHandler.this); // Don't log whether the transfer succeeded or failed as the transfer was initiated by the source therefore could be unreliable evidence. br.receive(new BlockReceiverCompletion() { @Override public void blockReceived(byte[] buf) { if(logMINOR) Logger.minor(this, "Received data for "+CHKInsertHandler.this); synchronized(CHKInsertHandler.this) { receiveCompleted = true; CHKInsertHandler.this.notifyAll(); } node.nodeStats.successfulBlockReceive(realTimeFlag, false); } @Override public void blockReceiveFailed(RetrievalException e) { synchronized(CHKInsertHandler.this) { receiveCompleted = true; receiveFailed = true; CHKInsertHandler.this.notifyAll(); } // Cancel the sender if(sender != null) sender.onReceiveFailed(); // tell it to stop if it hasn't already failed... unless it's sending from store runThread.interrupt(); tag.timedOutToHandlerButContinued(); // sender is finished, or will be very soon; we may however be waiting for the sendAborted downstream. Message msg = DMT.createFNPDataInsertRejected(uid, DMT.DATA_INSERT_REJECTED_RECEIVE_FAILED); try { source.sendSync(msg, CHKInsertHandler.this, realTimeFlag); } catch (NotConnectedException ex) { //If they are not connected, that's probably why the receive failed! if (logMINOR) Logger.minor(this, "Can't send "+msg+" to "+source+": "+ex); } catch (SyncSendWaitedTooLongException ex) { Logger.error(this, "Took too long to send "+msg+" to "+source); } if (e.getReason()==RetrievalException.SENDER_DISCONNECTED) Logger.normal(this, "Failed to retrieve (disconnect): "+e+" for "+CHKInsertHandler.this, e); else // Annoying, but we have stats for this; no need to call attention to it, it's unlikely to be a bug. Logger.normal(this, "Failed to retrieve ("+e.getReason()+"/"+RetrievalException.getErrString(e.getReason())+"): "+e+" for "+CHKInsertHandler.this, e); if(!prb.abortedLocally()) node.nodeStats.failedBlockReceive(false, false, realTimeFlag, false); return; } }); } @Override public String toString() { return super.toString()+" for "+uid; } @Override public int getPriority() { return NativeThread.HIGH_PRIORITY; } } private synchronized boolean receiveFailed() { return receiveFailed; } private final Object totalSync = new Object(); private int totalSentBytes; private int totalReceivedBytes; @Override public void sentBytes(int x) { synchronized(totalSync) { totalSentBytes += x; } node.nodeStats.insertSentBytes(false, x); } @Override public void receivedBytes(int x) { synchronized(totalSync) { totalReceivedBytes += x; } node.nodeStats.insertReceivedBytes(false, x); } public int getTotalSentBytes() { return totalSentBytes; } public int getTotalReceivedBytes() { return totalReceivedBytes; } @Override public void sentPayload(int x) { node.sentPayload(x); node.nodeStats.insertSentBytes(false, -x); } @Override public int getPriority() { return NativeThread.HIGH_PRIORITY; } private BlockReceiverTimeoutHandler myTimeoutHandler = new BlockReceiverTimeoutHandler() { /** We timed out waiting for a block from the request sender. We do not know * whether it is the fault of the request sender or that of some previous node. * The PRB will be cancelled, resulting in all outgoing transfers for this insert * being cancelled quickly. If the problem occurred on a previous node, we will * receive a cancel. So we are consistent with the nodes we routed to, and it is * safe to wait for the node that routed to us to send an explicit cancel. We do * not need to do anything yet. */ @Override public void onFirstTimeout() { // Do nothing. } /** We timed out, and the sender did not send us a timeout message, even after we * told it we were cancelling. Hence, we know that it was at fault. We need to * take action against it. */ @Override public void onFatalTimeout(PeerContext receivingFrom) { Logger.error(this, "Fatal timeout receiving insert "+CHKInsertHandler.this+" from "+receivingFrom); ((PeerNode)receivingFrom).fatalTimeout(); } }; }