Package freenet.io.xfer

Source Code of freenet.io.xfer.BlockReceiver$BlockReceiverCompletion

/*
* Dijjer - A Peer to Peer HTTP Cache
* Copyright (C) 2004,2005 Change.Tv, Inc
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
package freenet.io.xfer;

import static java.util.concurrent.TimeUnit.SECONDS;

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.MessageCore;
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.node.PeerNode;
import freenet.node.SyncSendWaitedTooLongException;
import freenet.support.BitArray;
import freenet.support.Buffer;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.Ticker;
import freenet.support.TimeUtil;
import freenet.support.io.NativeThread;
import freenet.support.math.MedianMeanRunningAverage;

/**
* IMPORTANT: The receiver can cancel the incoming transfer. This may or may not,
* depending on the caller, result in the PRB being cancelled, and thus propagate back to
* the originator.
*
* This allows for a weak DoS, in that a node can start a request and then cancel it,
* having wasted a certain amount of upstream bandwidth on transferring data, especially
* if upstream has lots of bandwidth and the attacker has limited bandwidth in the victim
* -> attacker direction. However this behaviour can be detected fairly easily.
*
* If we allow receiver cancels and don't propagate, a more serious DoS is possible. If we
* don't allow receiver cancels, we have to get rid of turtles, and massively tighten up
* transfer timeouts.
*
* However, if we do that, we have to consider that a node might be able to connect, max
* out the bandwidth with transfers, and then disconnect, avoiding the need to spend
* bandwidth on receiving all the data; and then reconnect, after it's confident that the
* transfers to it will have been cancelled. Or not reconnect at all, on opennet - just
* use a different identity. Downstream bandwidth is very cheap for small-scale attackers,
* but if this is a usable force multiplier it could still be a good DoS if we went that
* way.
*
* But if we did get rid of receiver cancels, it *would* mean we could get rid of a lot of
* code - e.g. the ReceiverAbortHandler, which in some cases (e.g RequestHandler) is
* complex and involves complex security tradeoffs. It would also make transfers
* significantly more reliable.
*
* @author ian
*/
public class BlockReceiver implements AsyncMessageFilterCallback {

  private static volatile boolean logMINOR;

  static {
    Logger.registerLogThresholdCallback(new LogThresholdCallback(){
      @Override
      public void shouldUpdate(){
        logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
      }
    });
  }

  public interface BlockReceiverTimeoutHandler {
   
    /** After a block times out, we call this callback. Once it returns, we cancel the
     * PRB and wait for a cancel message or the second timeout. Hence, if the problem
     * is on the node sending the data, we will get the first timeout then the second
     * (fatal) timeout. But if the problem is upstream, we will only get the first
     * timeout.
     *
     * Simple requests will need to implement this and transfer ownership of
     * the request to this node, because the source node will end the request as soon
     * as it sees the transfer cancel resulting from the PRB being cancelled;
     * assigning the UID to ourselves keeps it consistent, and thus avoids severe load
     * management problems (resulting in e.g. constantly sending requests to a node
     * which are then rejected because we think we have capacity when we don't). */
    void onFirstTimeout();
   
    /** After the first timeout, we wait for either a cancel message (sendAborted
     * here), or the second timeout. If we get the second timeout, the problem was
     * caused by the node we are receiving the data from, rather than upstream. In
     * which case, we may need to take severe action against the node responsible,
     * because we do not know whether or not it thinks the transfer is still running.
     * If it is still running and yet we cancel it, we will think that there is
     * capacity for more requests on the node when there isn't, resulting in load
     * management problems as above. */
    void onFatalTimeout(PeerContext source);
  }

  /*
   * RECEIPT_TIMEOUT must be less than 60 seconds because BlockTransmitter times out after not
   * hearing from us in 60 seconds. Without contact from the transmitter, we will try sending
   * at most MAX_CONSECUTIVE_MISSING_PACKET_REPORTS every RECEIPT_TIMEOUT to recover.
   */
  public final long RECEIPT_TIMEOUT;
  public static final long RECEIPT_TIMEOUT_REALTIME = SECONDS.toMillis(10);
  public static final long RECEIPT_TIMEOUT_BULK = SECONDS.toMillis(30);
  // TODO: This should be proportional to the calculated round-trip-time, not a constant
  public final long MAX_ROUND_TRIP_TIME;
  public static final int MAX_CONSECUTIVE_MISSING_PACKET_REPORTS = 4;
  public static final int MAX_SEND_INTERVAL = 500;
  public static final long CLEANUP_TIMEOUT = SECONDS.toMillis(5);
  // After 15 seconds, the receive is overdue and will cause backoff.
  public static final long TOO_LONG_TIMEOUT = SECONDS.toMillis(15);
  /** sendAborted is not sent at the realtime/bulk priority. Most of the two
   * stage timeout stuff uses 60 seconds, it's a good number. */
  public static final long ACK_TRANSFER_FAILED_TIMEOUT = SECONDS.toMillis(60);
  PartiallyReceivedBlock _prb;
  PeerContext _sender;
  long _uid;
  MessageCore _usm;
  ByteCounter _ctr;
  Ticker _ticker;
  boolean sentAborted;
  private MessageFilter discardFilter;
  private long discardEndTime;
  private boolean senderAborted;
  private final boolean _realTime;
//  private final boolean _doTooLong;
  private final BlockReceiverTimeoutHandler _timeoutHandler;
  private final boolean completeAfterAckedAllReceived;

  /**
   * @param usm
   * @param sender
   * @param uid
   * @param prb
   * @param ctr
   * @param ticker
   * @param doTooLong
   * @param realTime
   * @param timeoutHandler
   * @param completeAfterAckedAllReceived If true, we need to call completion
   * only after we have received an ack to the allReceived message. Generally,
   * handlers want to complete early (=false), so the slot is freed up and can
   * be reused by the other side; senders want to complete late, so they don't
   * end up reusing the slot before the handler has completed (=true).
   */
  public BlockReceiver(MessageCore usm, PeerContext sender, long uid, PartiallyReceivedBlock prb, ByteCounter ctr, Ticker ticker, boolean doTooLong, boolean realTime, BlockReceiverTimeoutHandler timeoutHandler, boolean completeAfterAckedAllReceived) {
    BlockReceiverTimeoutHandler nullTimeoutHandler = new BlockReceiverTimeoutHandler() {

      @Override
      public void onFirstTimeout() {
        // Do nothing
      }

      @Override
      public void onFatalTimeout(PeerContext source) {
        // Do nothing
      }

    };
    _timeoutHandler = timeoutHandler == null ? nullTimeoutHandler : timeoutHandler;
    _sender = sender;
    _prb = prb;
    _uid = uid;
    _usm = usm;
    _ctr = ctr;
    _ticker = ticker;
    _realTime = realTime;
    this.completeAfterAckedAllReceived = completeAfterAckedAllReceived;
    RECEIPT_TIMEOUT = _realTime ? RECEIPT_TIMEOUT_REALTIME : RECEIPT_TIMEOUT_BULK;
    MAX_ROUND_TRIP_TIME = RECEIPT_TIMEOUT;
//    _doTooLong = doTooLong;
  }

  private void sendAborted(int reason, String desc) throws NotConnectedException {
    synchronized(this) {
      if(sentAborted) return;
      sentAborted = true;
    }
    _usm.send(_sender, DMT.createSendAborted(_uid, reason, desc), _ctr);
  }
 
  public interface BlockReceiverCompletion {

    public void blockReceived(byte[] buf);

    public void blockReceiveFailed(RetrievalException e);

  }
 
  private BlockReceiverCompletion callback;
 
  private long startTime;
 
  // If false, don't check for duplicate messages from the sender.
  // Turn off if e.g. we know that the PRB is already partially received when we start the transfer.
  // This prevents malicious or broken nodes from trickling transfers forever by sending the same packets over and over.
  static final boolean CHECK_DUPES = true;
 
  private boolean gotAllSent;
 
  private AsyncMessageFilterCallback notificationWaiter = new SlowAsyncMessageFilterCallback() {

    @Override
    public void onMatched(Message m1) {
            if(logMINOR)
              Logger.minor(this, "Received "+m1);
            if ((m1 != null) && m1.getSpec().equals(DMT.sendAborted)) {
        String desc=m1.getString(DMT.DESCRIPTION);
        if (desc.indexOf("Upstream")<0)
          desc="Upstream transmit error: "+desc;
        _prb.abort(m1.getInt(DMT.REASON), desc, false);
        synchronized(BlockReceiver.this) {
          senderAborted = true;
        }
        complete(m1.getInt(DMT.REASON), desc);
        return;
      }
            boolean truncateTimeout = false;
      if ((m1 != null) && (m1.getSpec().equals(DMT.packetTransmit))) {
        // packetTransmit received
        int packetNo = m1.getInt(DMT.PACKET_NO);
        BitArray sent = (BitArray) m1.getObject(DMT.SENT);
        Buffer data = (Buffer) m1.getObject(DMT.DATA);
        int missing = 0;
        try {
          synchronized(BlockReceiver.this) {
            if(completed) return;
          }
          if(CHECK_DUPES && _prb.isReceived(packetNo)) {
            // Transmitter sent the same packet twice?!?!?
            Logger.error(this, "Already received the packet - DoS??? on "+this+" uid "+_uid+" from "+_sender);
            // Does not extend timeouts.
            truncateTimeout = true;
          } else {
            _prb.addPacket(packetNo, data);
            if(logMINOR) {
              synchronized(BlockReceiver.this) {
                long interval = System.currentTimeMillis() - timeStartedWaiting;
                Logger.minor(this, "Packet interval: "+interval+" = "+TimeUtil.formatTime(interval, 2, true)+" from "+_sender);
              }
            }
            // Check that we have what the sender thinks we have
            for (int x = 0; x < sent.getSize(); x++) {
              if (sent.bitAt(x) && !_prb.isReceived(x)) {
                missing++;
              }
            }
            if(logMINOR && missing != 0)
              Logger.minor(this, "Packets which the sender says it has sent but we have not received: "+missing);
          }
        } catch (AbortedException e) {
          // We didn't cause it?!
          Logger.error(this, "Caught in receive - probably a bug as receive sets it: "+e, e);
          complete(RetrievalException.UNKNOWN, "Aborted?");
          return;
        }
      } else if (m1 != null && m1.getSpec().equals(DMT.allSent)) {
        synchronized(BlockReceiver.this) {
          if(completed) return;
          if(gotAllSent)
            // Multiple allSent's don't extend the timeouts.
            truncateTimeout = true;
          gotAllSent = true;
        }
      }
      try {
        if(_prb.allReceived()) {
          try {
            Message m = DMT.createAllReceived(_uid);
            if(completeAfterAckedAllReceived) {
              try {
                // FIXME layer violation
                // FIXME make asynchronous
                ((PeerNode)_sender).sendSync(m, _ctr, _realTime);
              } catch (SyncSendWaitedTooLongException e) {
                // Complete anyway.
              }
            } else {
              _usm.send(_sender, m, _ctr);
            }
            discardEndTime=System.currentTimeMillis()+CLEANUP_TIMEOUT;
            discardFilter=relevantMessages(CLEANUP_TIMEOUT);
            maybeResetDiscardFilter();
          } catch (NotConnectedException e1) {
            // Ignore, we've got it.
            if(logMINOR) Logger.minor(this, "Got data but can't send allReceived to "+_sender+" as is disconnected");
          }
          long endTime = System.currentTimeMillis();
          long transferTime = (endTime - startTime);
          if(logMINOR) {
            synchronized(avgTimeTaken) {
              avgTimeTaken.report(transferTime);
              Logger.minor(this, "Block transfer took "+transferTime+"ms - average is "+avgTimeTaken);
            }
          }
          complete(_prb.getBlock());
          return;
        }
      } catch (AbortedException e1) {
        // We didn't cause it?!
        Logger.error(this, "Caught in receive - probably a bug as receive sets it: "+e1, e1);
        complete(RetrievalException.UNKNOWN, "Aborted?");
        return;
      }
      try {
        // Even if timeout <= 0, we still add the filter, because we want to receive any messages that are already buffered before we timeout.
        waitNotification(truncateTimeout);
      } catch (DisconnectedException e) {
        onDisconnect(null);
        return;
      }
    }
   
    @Override
    public boolean shouldTimeout() {
      return completed;
    }
   
    @Override
    public void onTimeout() {
      synchronized(this) {
        if(completed) return;
      }
      try {
        if(_prb.allReceived()) return;
        _prb.abort(RetrievalException.SENDER_DIED, "Sender unresponsive to resend requests", false);
        complete(RetrievalException.SENDER_DIED,
            "Sender unresponsive to resend requests");
       
        _timeoutHandler.onFirstTimeout();
        // If upstream caused the problem, then sender will itself timeout
        // and will tell us. So wait for a timeout.
        // It is important for load management that the two sides agree on the number of transfers happening.
        // Therefore we need to not complete until the other side has acknowledged that the transfer has been cancelled.
        MessageFilter mfSendAborted = MessageFilter.create().setTimeout(ACK_TRANSFER_FAILED_TIMEOUT).setType(DMT.sendAborted).setField(DMT.UID, _uid).setSource(_sender);
        try {
          _usm.addAsyncFilter(mfSendAborted, new SlowAsyncMessageFilterCallback() {

            @Override
            public void onMatched(Message m) {
              // Ok.
              if(logMINOR) Logger.minor(this, "Transfer cancel acknowledged");
            }

            @Override
            public boolean shouldTimeout() {
              return false;
            }

            @Override
            public void onTimeout() {
              Logger.error(this, "Other side did not acknowlege transfer failure on "+BlockReceiver.this);
              _timeoutHandler.onFatalTimeout(_sender);
            }

            @Override
            public void onDisconnect(PeerContext ctx) {
              // Ok.
            }

            @Override
            public void onRestarted(PeerContext ctx) {
              // Ok.
            }

            @Override
            public int getPriority() {
              return NativeThread.NORM_PRIORITY;
            }
           
          }, _ctr);
        } catch (DisconnectedException e) {
          // Ignore
        }
       
        return;
      } catch (AbortedException e) {
        // We didn't cause it?!
        Logger.error(this, "Caught in receive - probably a bug as receive sets it: "+e, e);
        complete(RetrievalException.UNKNOWN, "Aborted?");
        return;
      }
    }
   
    @Override
    public void onDisconnect(PeerContext ctx) {
      complete(RetrievalException.SENDER_DISCONNECTED, RetrievalException.getErrString(RetrievalException.SENDER_DISCONNECTED));
    }
   
    @Override
    public void onRestarted(PeerContext ctx) {
      complete(RetrievalException.SENDER_DISCONNECTED, RetrievalException.getErrString(RetrievalException.SENDER_DISCONNECTED));
    }

    @Override
    public int getPriority() {
      return NativeThread.NORM_PRIORITY;
    }
   
  };
 
  private boolean completed;
 
  private void complete(int reason, String description) {
    synchronized(this) {
      if(completed) {
        if(logMINOR) Logger.minor(this, "Already completed");
        return;
      }
      completed = true;
    }
    if(logMINOR)
      Logger.minor(this, "Transfer failed: ("+(_realTime?"realtime":"bulk")+") "+reason+" : "+description+" on "+_uid+" from "+_sender);
    _prb.removeListener(myListener);
    byte[] block = _prb.abort(reason, description, false);
    if(block == null) {
      // Expected behaviour.
      // Send the abort whether we have received one or not.
      // If we are cancelling due to failing to turtle, we need to tell the sender
      // this otherwise he will keep sending, wasting a lot of bandwidth on packets
      // that we will ignore. If we are cancelling because the sender has told us
      // to, we need to acknowledge that.
      try {
        sendAborted(_prb._abortReason, _prb._abortDescription);
      } catch (NotConnectedException e) {
        // Ignore at this point.
      }
      callback.blockReceiveFailed(new RetrievalException(reason, description));
    } else {
      Logger.error(this, "Succeeded in complete("+reason+","+description+") on "+this, new Exception("error"));
      callback.blockReceived(block);
    }
    decRunningBlockReceives();
  }

  private void complete(byte[] ret) {
    synchronized(this) {
      if(completed) {
        if(logMINOR) Logger.minor(this, "Already completed");
        return;
      }
      completed = true;
    }
    _prb.removeListener(myListener);
    callback.blockReceived(ret);
    decRunningBlockReceives();
  }

  private long timeStartedWaiting = -1;
 
  private void waitNotification(boolean truncateTimeout) throws DisconnectedException {
    long timeout;
    long now = System.currentTimeMillis();
    synchronized(this) {
      if(truncateTimeout) {
        timeout = (int)Math.min(timeStartedWaiting + RECEIPT_TIMEOUT - now, RECEIPT_TIMEOUT);
      } else {
        timeStartedWaiting = now;
        timeout = RECEIPT_TIMEOUT;
      }
    }
    _usm.addAsyncFilter(relevantMessages(timeout), notificationWaiter, _ctr);
  }

  private MessageFilter relevantMessages(long timeout) {
    MessageFilter mfPacketTransmit = MessageFilter.create().setTimeout(timeout).setType(DMT.packetTransmit).setField(DMT.UID, _uid).setSource(_sender);
    MessageFilter mfAllSent = MessageFilter.create().setTimeout(timeout).setType(DMT.allSent).setField(DMT.UID, _uid).setSource(_sender);
    MessageFilter mfSendAborted = MessageFilter.create().setTimeout(timeout).setType(DMT.sendAborted).setField(DMT.UID, _uid).setSource(_sender);
    return mfPacketTransmit.or(mfAllSent.or(mfSendAborted));
  }

  PartiallyReceivedBlock.PacketReceivedListener myListener;
 
  public void receive(BlockReceiverCompletion callback) {
    startTime = System.currentTimeMillis();
    this.callback = callback;
    synchronized(_prb) {
      try {
        _prb.addListener(myListener = new PartiallyReceivedBlock.PacketReceivedListener() {;

          @Override
          public void packetReceived(int packetNo) {
            // Ignore
          }

          @Override
          public void receiveAborted(int reason, String description) {
            complete(reason, description);
          }
        });
      } catch (AbortedException e) {
        try {
          callback.blockReceived(_prb.getBlock());
          return;
        } catch (AbortedException e1) {
          e = e1;
        }
        callback.blockReceiveFailed(new RetrievalException(_prb._abortReason, _prb._abortDescription));
        return;
      }
    }
    incRunningBlockReceives();
    try {
      waitNotification(false);
    } catch (DisconnectedException e) {
      RetrievalException retrievalException = new RetrievalException(RetrievalException.SENDER_DISCONNECTED);
      _prb.abort(retrievalException.getReason(), retrievalException.toString(), true /* kind of, it shouldn't count towards the stats anyway */);
      callback.blockReceiveFailed(retrievalException);
      decRunningBlockReceives();
    } catch(RuntimeException e) {
      decRunningBlockReceives();
      throw e;
    } catch (Error e) {
      decRunningBlockReceives();
      throw e;
    }
  }
 
  private static MedianMeanRunningAverage avgTimeTaken = new MedianMeanRunningAverage();
 
  private void maybeResetDiscardFilter() {
    long timeleft=discardEndTime-System.currentTimeMillis();
    if (timeleft>0) {
      try {
        discardFilter.setTimeout((int)timeleft);
        _usm.addAsyncFilter(discardFilter, this, _ctr);
      } catch (DisconnectedException e) {
        //ignore
      }
    }
  }
 
  /**
   * Used to discard leftover messages, usually just packetTransmit and allSent.
   * allSent, is quite common, as the receive() routine usually quits immeadiately on receiving all packets.
   * packetTransmit is less common, when receive() requested what it thought was a missing packet, only reordered.
   */
  @Override
  public void onMatched(Message m) {
    if (logMINOR)
      Logger.minor(this, "discarding message post-receive: "+m);
    maybeResetDiscardFilter();                          
  }
 
  @Override
  public boolean shouldTimeout() {
    return false;
  }
 
  @Override
  public void onTimeout() {
    //ignore
  }

  @Override
  public void onDisconnect(PeerContext ctx) {
    // Ignore
  }

  @Override
  public void onRestarted(PeerContext ctx) {
    // Ignore
  }

  public synchronized boolean senderAborted() {
    return senderAborted;
  }
 
  static int runningBlockReceives = 0;
 
  private void incRunningBlockReceives() {
    if(logMINOR) Logger.minor(this, "Starting block receive "+_uid);
    synchronized(BlockReceiver.class) {
      runningBlockReceives++;
      if(logMINOR) Logger.minor(BlockTransmitter.class, "Started a block receive, running: "+runningBlockReceives);
    }
  }
 
  private void decRunningBlockReceives() {
    if(logMINOR) Logger.minor(this, "Stopping block receive "+_uid);
    synchronized(BlockReceiver.class) {
      runningBlockReceives--;
      if(logMINOR) Logger.minor(BlockTransmitter.class, "Finished a block receive, running: "+runningBlockReceives);
    }
  }

  public synchronized static int getRunningReceives() {
    return runningBlockReceives;
  }
 
  @Override
  public String toString() {
    return super.toString()+":"+_uid+":"+_sender.shortToString();
  }
 
}
TOP

Related Classes of freenet.io.xfer.BlockReceiver$BlockReceiverCompletion

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.