package org.limewire.rudp;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AlreadyConnectedException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ConnectionPendingException;
import java.nio.channels.SelectionKey;
import org.limewire.listener.EventBroadcaster;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.rudp.messages.AckMessage;
import org.limewire.rudp.messages.DataMessage;
import org.limewire.rudp.messages.FinMessage;
import org.limewire.rudp.messages.KeepAliveMessage;
import org.limewire.rudp.messages.RUDPMessage;
import org.limewire.rudp.messages.SynMessage;
import org.limewire.rudp.messages.SynMessage.Role;
import org.limewire.service.ErrorService;
/**
* Manages a reliable UDP connection to transfer data.
*/
public class UDPConnectionProcessor {
private static final Log LOG = LogFactory.getLog(UDPConnectionProcessor.class);
/** Define the chunk size used for data bytes. */
public static final int DATA_CHUNK_SIZE = 512;
/** Define the maximum chunk size read for data bytes
before we will blow out the connection. */
public static final int MAX_DATA_SIZE = 4096;
/** Define the size of the data window. */
public static final int DATA_WINDOW_SIZE = 20;
/** Define the maximum accepted write ahead packet. */
private static final int DATA_WRITE_AHEAD_MAX = DATA_WINDOW_SIZE + 5;
/** The maximum number of times to try and send a data message. */
private static final int MAX_SEND_TRIES = 8;
/** Define the wait time between SYN messages. */
private static final long SYN_WAIT_TIME = 400;
/** Define the maximum wait time to connect. */
private static final long MAX_CONNECT_WAIT_TIME = 20*1000;
/** Define the maximum time that we'll allow a connection to remain
* open through keep-alives alone. */
private static final long MAX_KEEPALIVE_TIME = 60 * 1000;
/** Define the maximum wait time before sending a message in order to
keep the connection alive (and firewalls open). */
private static final long KEEPALIVE_WAIT_TIME = (3*1000 - 500);
/** Define the default time to check for an ack to a data message. */
private static final long DEFAULT_RTO_WAIT_TIME = 400;
/** Define the maximum time that a connection will stay open without
a message being received */
private static final long MAX_MESSAGE_WAIT_TIME = 20 * 1000;
/** Define the minimum wait time between ack timeout events. */
private static final long MIN_ACK_WAIT_TIME = 5;
/** Define the size of a small send window for increasing wait time .*/
static final long SMALL_SEND_WINDOW = 2;
/** Ensure that writing takes a break every 4 writes so other
synchronized activity can take place. */
private static final long MAX_WRITE_WITHOUT_SLEEP = 4;
/** Delay the write wakeup event a little so that it isn't constantly
firing - This should achieve part of nagles algorithm. */
private static final long WRITE_WAKEUP_DELAY_TIME = 10;
/** Delay the write events by one second if there is nothing to do. */
private static final long NOTHING_TO_DO_DELAY = 1000;
/** Time to wait after a close before everything is totally shutdown. */
private static final long SHUTDOWN_DELAY_TIME = 400;
/**
* Time to wait after the connection is closed to completely and totally
* shut down the connection (and its channel).
*/
private static final long CHANNEL_SHUTDOWN_DELAY = 30000;
// Handle to various singleton objects in our architecture
private UDPScheduler _scheduler;
/** The UDPSocketChannel backing this processor. */
private UDPSocketChannel _channel;
/** The address we're connected to. */
private InetSocketAddress _connectedTo;
/** The limit on space for data to be written out. */
private volatile int _chunkLimit;
/** The receivers windowSpace defining amount of data that receiver can
accept. */
private volatile int _receiverWindowSpace;
/** Whether or not we've received an ack to our syn. */
private boolean _receivedSynAck;
/** The Window for sending and acking data. */
private DataWindow _sendWindow;
/** The WriteRegulator controls the amount of waiting time between writes. */
private WriteRegulator _writeRegulator;
/** The Window for receiving data. */
private DataWindow _receiveWindow;
/** The connection id of this end of the connection. Used for routing. */
private byte _myConnectionID;
/** The connectionID of the other end of the connection. Used for routing. */
private volatile byte _theirConnectionID;
/** The status of the connection */
private ConnectionState _connectionState;
/** Scheduled event for keeping connection alive. */
private UDPTimerEvent _keepaliveEvent;
/** Scheduled event for writing data appropriately over time. */
private UDPTimerEvent _writeDataEvent;
/** Scheduled event for cleaning up at end of connection life. */
private UDPTimerEvent _closedCleanupEvent;
/** Flag that the writeEvent is shutdown waiting for space to write. */
private boolean _waitingForDataSpace;
/** Flag that the writeEvent is shutdown waiting for data to write. */
private volatile boolean _waitingForDataAvailable;
/** Flag saying that a Fin packet has been acked on shutdown. */
private boolean _waitingForFinAck;
/** The time we started connecting. */
private long _startedConnecting;
/** Scheduled event for connecting. */
private UDPTimerEvent _connectEvent;
/** Scheduled event for ensuring that data is acked or resent. */
private UDPTimerEvent _ackTimeoutEvent;
/** Adhoc event for waking up the writing of data. */
private UDPTimerEvent _safeWriteWakeup;
/** The current sequence number of messages originated here. */
private long _sequenceNumber;
/** The sequence number of a pending fin message */
private long _finSeqNo;
/** Transformer for mapping 2 byte sequenceNumbers of incoming ACK
messages to 8 byte longs of essentially infinite size - note Acks
echo our seqNo. */
private SequenceNumberExtender _localExtender;
/** Transformer for mapping 2 byte sequenceNumbers of incoming messages to
8 byte longs of essentially infinite size. */
private SequenceNumberExtender _extender;
/** The last time that a message was sent to other host. */
private long _lastSendTime;
/** The last time that data was sent to other host. */
private long _lastDataSendTime;
/** The last time that a message was received from the other host. */
private long _lastReceivedTime;
/** The last time we received a non-keepalive message. */
private long _lastDataOrAckTime;
/** The number of resends to take into account when scheduling ack wait. */
private int _ackResendCount;
/** Skip a Data Write if this flag is true. */
private boolean _skipADataWrite;
/** Keep track of the reason for shutting down. */
private byte _closeReasonCode;
////////////////////////////////////////////
// Some settings related to skipping acks
///////////////////////////////////////////
/** Whether to skip any acks at all. */
private final boolean _skipAcks;
/** How long each measuring period is. */
private final int _period;
/** How many periods to keep track of. */
private final int _periodHistory;
/**
* By how much does the current period need to deviate from the average
* before we start acking.
*/
private final float _deviation;
/** Do not skip more than this many acks in a row. */
private final int _maxSkipAck;
/** how many data packets we got each second. */
private final int [] _periods;
/** index within that array, points to the last period. */
private int _currentPeriodId;
/** How many data packets we received this period. */
private int _packetsThisPeriod;
/** whether we have enough data. */
private boolean _enoughData;
/** when the current second started. */
private long _lastPeriod;
/** how many acks we skipped in a row vs. total. */
private int _skippedAcks, _skippedAcksTotal;
/** how many packets we got in total.*/
private int _totalDataPackets;
/** The context containing various aspects required for RUDP. */
private final RUDPContext _context;
private final Role role;
private final EventBroadcaster<UDPSocketChannelConnectionEvent> _connectionStateEventBroadcaster;
/** Creates a new unconnected UDPConnectionProcessor.
* @param role defines the role it plays in the communication, either
* requestor or acceptor.
*/
protected UDPConnectionProcessor(UDPSocketChannel channel,
RUDPContext context,
Role role,
EventBroadcaster<UDPSocketChannelConnectionEvent> connectionStateEventBroadcaster) {
// Init default state
_context = context;
this.role = role;
this._connectionStateEventBroadcaster = connectionStateEventBroadcaster;
_theirConnectionID = UDPMultiplexor.UNASSIGNED_SLOT;
_lastSendTime = 0l;
_lastDataSendTime = 0l;
_chunkLimit = DATA_WINDOW_SIZE;
_receiverWindowSpace = DATA_WINDOW_SIZE;
_waitingForDataSpace = false;
_waitingForDataAvailable = false;
_waitingForFinAck = false;
_skipADataWrite = false;
_ackResendCount = 0;
_closeReasonCode = FinMessage.REASON_NORMAL_CLOSE;
_channel = channel;
setConnectionState(ConnectionState.PRECONNECT);
_scheduler = UDPScheduler.instance();
// Precreate the receive window for response reporting
_receiveWindow = new DataWindow(DATA_WINDOW_SIZE, 1);
// All incoming seqNo and windowStarts get extended
// Acks seqNo need to be extended separately
_localExtender = new SequenceNumberExtender();
_extender = new SequenceNumberExtender();
_skipAcks = _context.getRUDPSettings().isSkipAcksEnabled();
_maxSkipAck = _context.getRUDPSettings().getMaxSkipAcks();
_deviation = _context.getRUDPSettings().getMaxSkipDeviation();
_period = _context.getRUDPSettings().getSkipAckPeriodLength();
_periodHistory = _context.getRUDPSettings().getSkipAckHistorySize();
_periods = new int[_periodHistory];
}
private void setConnectionState(ConnectionState newState) {
_connectionState = newState;
_connectionStateEventBroadcaster.broadcast(new UDPSocketChannelConnectionEvent(_channel, newState));
}
/**
* Attempts to connect to the given IP/port.
*
* @throws IOException
*/
protected void connect(InetSocketAddress addr) throws IOException {
// If UDP is not running or not workable, barf
if (!_context.getUDPService().isListening() || !_context.getUDPService().isNATTraversalCapable()) {
throw new IOException("udp isn't working");
}
synchronized(this) {
if(_connectionState != ConnectionState.PRECONNECT) {
if(isConnected())
throw new AlreadyConnectedException();
else if(isClosed())
throw new ClosedChannelException();
else
throw new ConnectionPendingException();
}
setConnectionState(ConnectionState.CONNECTING);
// Record their address in synchronized block, to avoid close() seeing
// seeing a state != preconnecting with a null address
_connectedTo = addr;
}
_startedConnecting = System.currentTimeMillis();
_sequenceNumber = 0;
if (LOG.isDebugEnabled())
LOG.debug("Connecting to: " + addr);
// See if you can establish a pseudo connection
// which means each side can send/receive a SYN and ACK
tryToConnect();
}
public byte getTheirConnectionID() {
return _theirConnectionID;
}
/** Sets the connection id this is using. */
protected void setConnectionId(byte id) {
this._myConnectionID = id;
}
/** Retrieves the InetSocketAddress this is connecting to. */
protected InetSocketAddress getSocketAddress() {
return _connectedTo;
}
/** Retrieves the DataWindow used for reading data. */
protected DataWindow getReadWindow() {
return _receiveWindow;
}
/** Returns the ready ops of this processor. */
protected synchronized int readyOps() {
if(isClosed())
return 0xFF;
else
return (isConnectReady() ? SelectionKey.OP_CONNECT : 0)
| (isReadReady() ? SelectionKey.OP_READ : 0)
| (isWriteReady() ? SelectionKey.OP_WRITE : 0);
}
/** Gets the connect readiness. */
private boolean isConnectReady() {
return _receivedSynAck
&& isConnecting()
&& _theirConnectionID != UDPMultiplexor.UNASSIGNED_SLOT;
}
/** Gets the read-readiness of this processor. */
private boolean isReadReady() {
return _receiveWindow.hasReadableData();
}
/** Gets the write-readiness of this processor. */
private boolean isWriteReady() {
return isConnected() && _channel.getNumberOfPendingChunks() < getChunkLimit();
}
/**
* Closes the connection.
*
* @throws IOException
*/
protected synchronized void close() throws IOException {
// If closed then done
if ( _connectionState == ConnectionState.FIN )
throw new IOException("already closed");
if(_connectEvent != null)
_connectEvent.unregister();
// Shutdown keepalive event callbacks
if ( _keepaliveEvent != null )
_keepaliveEvent.unregister();
// Shutdown write event callbacks
if ( _writeDataEvent != null )
_writeDataEvent.unregister();
// Shutdown ack timeout event callbacks
if ( _ackTimeoutEvent != null )
_ackTimeoutEvent.unregister();
// Unregister the safeWriteWakeup handler
if ( _safeWriteWakeup != null )
_safeWriteWakeup.unregister();
// Store the old state.
ConnectionState oldState = _connectionState;
// Register that the connection is closed
setConnectionState(ConnectionState.FIN);
// Track incoming ACKS for an ack of FinMessage
_waitingForFinAck = true;
// Tell the receiver that we are shutting down
if(oldState != ConnectionState.PRECONNECT) {
safeSendFin();
// Register for a full cleanup after a slight delay
if (_closedCleanupEvent==null) {
_closedCleanupEvent = new ClosedConnectionCleanupTimerEvent(
System.currentTimeMillis() + SHUTDOWN_DELAY_TIME,this);
LOG.debug("registering a closedCleanupEvent");
_scheduler.register(_closedCleanupEvent);
}
}
}
private synchronized void finalClose() {
// Send one final Fin message if not acked.
if (_waitingForFinAck)
safeSendFin();
// Clean up my caller
_closedCleanupEvent.unregister();
_scheduler.register(new ChannelCloseTimerEvent(System.currentTimeMillis() +
CHANNEL_SHUTDOWN_DELAY, this));
}
/**
* Prepare for handling an open connection.
*/
protected synchronized boolean prepareOpenConnection() throws IOException {
if(isClosed())
throw new ClosedChannelException();
if(isConnected())
return true;
if(!_receivedSynAck || _theirConnectionID == UDPMultiplexor.UNASSIGNED_SLOT)
return false;
setConnectionState(ConnectionState.CONNECTED);
_sequenceNumber = 1;
scheduleKeepAlive();
// Create the delayed connection components
_sendWindow = new DataWindow(DATA_WINDOW_SIZE, 1);
_writeRegulator = new WriteRegulator(_sendWindow);
// Precreate the event for rescheduling writing to allow
// thread safety and faster writing
_safeWriteWakeup = new SafeWriteWakeupTimerEvent(Long.MAX_VALUE, this);
_scheduler.register(_safeWriteWakeup);
// Keep chunkLimit in sync with window space
_chunkLimit = _sendWindow.getWindowSpace();
return true;
}
/**
* Make sure any firewall or NAT (Network Address Translation) stays open by scheduling a keepalive
* message before the connection should close.
*
* This just fires and reschedules itself appropriately so that we
* don't need to worry about rescheduling as every new message is sent.
*/
private synchronized void scheduleKeepAlive() {
// Create event with initial time
_keepaliveEvent =
new KeepAliveTimerEvent(_lastSendTime + KEEPALIVE_WAIT_TIME,this);
// Register keepalive event for future event callbacks
_scheduler.register(_keepaliveEvent);
// Schedule the first keepalive event callback
_scheduler.scheduleEvent(_keepaliveEvent);
}
/**
* Setup and schedule the callback event for writing data.
*/
private synchronized void scheduleWriteDataEvent(long time) {
if ( isConnected() ) {
if ( _writeDataEvent == null ) {
_writeDataEvent =
new WriteDataTimerEvent(time,this);
// Register writeData event for future use
_scheduler.register(_writeDataEvent);
} else {
_writeDataEvent.updateTime(time);
}
// Notify the scheduler that there is a new write event/time
_scheduler.scheduleEvent(_writeDataEvent);
if(LOG.isDebugEnabled()) {
LOG.debug("scheduleWriteDataEvent :"+time);
}
}
}
/**
* Activate writing if we were waiting for space
*/
private synchronized void writeSpaceActivation() {
if ( _waitingForDataSpace ) {
_waitingForDataSpace = false;
// Schedule immediately
scheduleWriteDataEvent(0);
}
}
/**
* Activate writing if we were waiting for data to write
*/
private synchronized void writeDataActivation() {
// Schedule at a reasonable time
long rto = _sendWindow.getRTO();
scheduleWriteDataEvent( _lastDataSendTime + (rto/4) );
}
/**
* Hand off the wakeup of data writing to the scheduler
*/
protected void wakeupWriteEvent(boolean force) {
if (force || _waitingForDataAvailable ) {
LOG.debug("wakupWriteEvent");
if (_safeWriteWakeup.getEventTime() == Long.MAX_VALUE) {
_safeWriteWakeup.updateTime(System.currentTimeMillis()+
WRITE_WAKEUP_DELAY_TIME);
_scheduler.scheduleEvent(_safeWriteWakeup);
}
}
}
/**
* Setup and schedule the callback event for ensuring we resend SYNs on Connect.
*/
private synchronized void scheduleConnectEvent(long time) {
if(_connectEvent == null) {
_connectEvent = new ConnectSynEvent(time, this);
_scheduler.register(_connectEvent);
} else {
_connectEvent.updateTime(time);
}
_scheduler.scheduleEvent(_connectEvent);
}
/**
* Setup and schedule the callback event for ensuring data gets acked.
*/
private synchronized void scheduleAckTimeoutEvent(long time) {
if ( isConnected() ) {
if ( _ackTimeoutEvent == null ) {
_ackTimeoutEvent =
new AckTimeoutTimerEvent(time,this);
// Register ackTimout event for future use
_scheduler.register(_ackTimeoutEvent);
} else {
_ackTimeoutEvent.updateTime(time);
}
// Notify the scheduler that there is a new ack timeout event
_scheduler.scheduleEvent(_ackTimeoutEvent);
}
}
/**
* Suppress ack timeout events for now.
*/
private synchronized void unscheduleAckTimeoutEvent() {
// Nothing required if not initialized
if ( _ackTimeoutEvent == null )
return;
// Set an existing event to an infinite wait
// Note: No need to explicitly inform scheduler.
_ackTimeoutEvent.updateTime(Long.MAX_VALUE);
}
/**
* Determine if an ackTimeout should be rescheduled.
*/
private synchronized boolean isAckTimeoutUpdateRequired() {
// If ack timeout not yet created then yes.
if ( _ackTimeoutEvent == null )
return true;
// If ack timeout exists but is infinite then yes an update is required.
return (_ackTimeoutEvent.getEventTime() == Long.MAX_VALUE);
}
/**
* Test whether the connection is in connecting mode.
*/
protected synchronized boolean isConnected() {
return (_connectionState == ConnectionState.CONNECTED &&
_theirConnectionID != UDPMultiplexor.UNASSIGNED_SLOT);
}
/**
* Test whether the connection is closed.
*/
protected synchronized boolean isClosed() {
return (_connectionState == ConnectionState.FIN);
}
/**
* Test whether the connection is not fully setup.
*/
protected synchronized boolean isConnecting() {
// It is important to check for either CONNECTING_STATE
// or UNASSIGNED_SLOT because the state is advanced when
// an ACK to our syn is received, and the connectionId is
// changed when a SYN is received. Either of these events
// can happen in any order, and only when both happen
// do we consider ourselves connected.
return !isClosed() &&
(_connectionState == ConnectionState.CONNECTING ||
(_connectionState != ConnectionState.PRECONNECT &&
_theirConnectionID == UDPMultiplexor.UNASSIGNED_SLOT)
);
}
/**
* Return the room for new local incoming data in chunks. This should
* remain equal to the space available in the sender and receiver
* data window.
*/
protected int getChunkLimit() {
return Math.min(_chunkLimit, _receiverWindowSpace);
}
/**
* Convenience method for sending keepalive message since we might fire
* these off before waiting
*/
protected void sendKeepAlive() {
KeepAliveMessage keepalive = null;
try {
keepalive = _context.getMessageFactory().createKeepAliveMessage(
_theirConnectionID,
_receiveWindow.getWindowStart(),
_receiveWindow.getWindowSpace());
send(keepalive);
} catch(IllegalArgumentException iae) {
// Report an error since this shouldn't ever happen
ErrorService.error(iae);
closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
}
}
/**
* Convenience method for sending data.
*/
private synchronized void sendData(ByteBuffer chunk) {
try {
assert chunk.position() == 0;
DataMessage dm = _context.getMessageFactory().createDataMessage(_theirConnectionID, _sequenceNumber, chunk);
send(dm);
DataRecord drec = _sendWindow.addData(dm);
drec.sentTime = _lastSendTime;
drec.sends++;
if( LOG.isDebugEnabled() &&
(_lastSendTime - _lastDataSendTime) > 2000) {
LOG.debug("SendData lag = "+
(_lastSendTime - _lastDataSendTime));
}
// Record when data was sent for future scheduling
_lastDataSendTime = _lastSendTime;
// Update the chunk limit for fast (nonlocking) access
_chunkLimit = _sendWindow.getWindowSpace();
_sequenceNumber++;
// If Acking check needs to be woken up then do it
if ( isAckTimeoutUpdateRequired())
scheduleAckTimeoutIfNeeded();
// Predecrement the other sides window until I here otherwise.
// This prevents a cascade of sends before an Ack
if ( _receiverWindowSpace > 0 )
_receiverWindowSpace--;
} catch(IllegalArgumentException iae) {
// Report an error since this shouldn't ever happen
ErrorService.error(iae);
closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
}
}
/**
* Build and send an ack with default error handling with
* the messages sequenceNumber, receive window start and
* receive window space.
*/
private synchronized void safeSendAck(RUDPMessage msg) {
// Ack the message
AckMessage ack = null;
try {
ack = _context.getMessageFactory().createAckMessage(_theirConnectionID, msg.getSequenceNumber(), _receiveWindow.getWindowStart(), _receiveWindow.getWindowSpace());
if (LOG.isDebugEnabled()) {
LOG.debug("total data packets "+_totalDataPackets+
" total acks skipped "+_skippedAcksTotal+
" skipped this session "+ _skippedAcks);
}
_skippedAcks=0;
send(ack);
} catch(IllegalArgumentException iae) {
// Report an error since this shouldn't ever happen
ErrorService.error(iae);
closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
}
}
/**
* Build and send a fin message with default error handling.
*/
private synchronized void safeSendFin() {
// Ack the message
FinMessage fin = null;
try {
// Record sequence number for ack monitoring
// Not that it should increment anymore anyways
_finSeqNo = _sequenceNumber;
// Send the FinMessage
fin = _context.getMessageFactory().createFinMessage(_theirConnectionID, _sequenceNumber, _closeReasonCode);
send(fin);
} catch(IllegalArgumentException iae) {
// Report an error since this shouldn't ever happen
ErrorService.error(iae);
LOG.warn("calling recursively closeAndCleanup");
closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
}
}
/**
* Send a message on to the UDPService.
*/
private synchronized void safeSend(RUDPMessage msg) {
try {
send(msg);
} catch(IllegalArgumentException iae) {
// Report an error since this shouldn't ever happen
ErrorService.error(iae);
closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
}
}
/**
* Send a message on to the UDPService.
*/
private synchronized void send(RUDPMessage msg)
throws IllegalArgumentException {
_lastSendTime = System.currentTimeMillis();
if(msg instanceof DataMessage || msg instanceof AckMessage)
_lastDataOrAckTime = _lastSendTime;
if(LOG.isDebugEnabled()) {
LOG.debug("send:" + msg + " to: " + _connectedTo + ", t:" + _lastSendTime);
if ( msg instanceof FinMessage ) {
Exception ex = new Exception();
LOG.debug("", ex);
}
}
_context.getUDPService().send(msg, _connectedTo);
}
/**
* Schedule an ack timeout for the oldest unacked data.
* If no acks are pending, then do nothing.
*/
private synchronized void scheduleAckTimeoutIfNeeded() {
DataRecord drec = _sendWindow.getOldestUnackedBlock();
if ( drec != null ) {
int rto = _sendWindow.getRTO();
if (rto == 0)
rto = (int) DEFAULT_RTO_WAIT_TIME;
long waitTime = drec.sentTime + rto;
// If there was a resend then base the wait off of current time
if ( _ackResendCount > 0 ) {
waitTime = _lastSendTime + rto;
_ackResendCount = 0;
}
// Enforce a minimum waitTime from now
long minTime = System.currentTimeMillis() + MIN_ACK_WAIT_TIME;
waitTime = Math.max(waitTime, minTime);
scheduleAckTimeoutEvent(waitTime);
} else {
unscheduleAckTimeoutEvent();
}
}
/**
* Ensure that data is getting acked. If not within an appropriate time,
* then resend.
*/
private synchronized void validateAckedData() {
long currTime = System.currentTimeMillis();
if (_sendWindow.acksAppearToBeMissing(currTime, 1)) {
// if the older blocks ack have been missing for a while
// resend them.
// Calculate a good maximum time to wait
int rto = _sendWindow.getRTO();
long start = _sendWindow.getWindowStart();
if(LOG.isDebugEnabled())
LOG.debug("Soft resend check:"+ start+ " rto:"+rto+
" uS:"+_sendWindow.getUsedSpots()+" localSeq:"+_sequenceNumber);
DataRecord drec;
// DataRecord drecNext;
int numResent = 0;
// Resend up to 1 packet at a time
resend: {
// Get the oldest unacked block out of storage
drec = _sendWindow.getOldestUnackedBlock();
int expRTO = (rto * (int)Math.pow(2,drec.sends-1));
if (LOG.isDebugEnabled())
LOG.debug(" exponential backoff is now "+expRTO);
// Check if the next drec is acked
if(_sendWindow.countHigherAckBlocks() >0){
expRTO*=0.75;
if (LOG.isDebugEnabled())
LOG.debug(" higher acked blocks, adjusting exponential backoff is now "+
expRTO);
}
// The assumption is that this record has not been acked
// FIXME this condition is never true
if ( drec.acks > 0)
break resend;
// If too many sends then abort connection
if ( drec.sends > MAX_SEND_TRIES+1 ) {
if(LOG.isDebugEnabled())
LOG.debug("Tried too many send on:"+
drec.msg.getSequenceNumber());
closeAndCleanup(FinMessage.REASON_TOO_MANY_RESENDS);
return;
}
int currentWait = (int)(currTime - drec.sentTime);
// If it looks like we waited too long then speculatively resend
// Case 1: We waited 150% of RTO and next packet had been acked
// Case 2: We waited 200% of RTO
if ( currentWait > expRTO) {
if(LOG.isDebugEnabled())
LOG.debug("Soft resending message:"+
drec.msg.getSequenceNumber());
safeSend(drec.msg);
// Scale back on the writing speed if you are hitting limits
_writeRegulator.addMessageFailure();
_writeRegulator.hitResendTimeout();
currTime = _lastSendTime;
drec.sentTime = currTime;
drec.sends++;
numResent++;
} else
LOG.debug(" not resending message ");
}
// Delay subsequent resends of data based on number resent
_ackResendCount = numResent;
if ( numResent > 0 )
_skipADataWrite = true;
}
scheduleAckTimeoutIfNeeded();
}
/**
* Close and cleanup by unregistering this connection and sending a Fin.
*/
private synchronized void closeAndCleanup(byte reasonCode) {
_closeReasonCode = reasonCode;
try {
close();
} catch (IOException ioe) {}
}
// ------------------ Connection Handling Logic -------------------
//
/**
* Send SYN messages to desired host and wait for Acks and their
* SYN message. Schedules an event to periodically resend a Syn.
*/
private synchronized void tryToConnect() {
if (!isConnecting()) {
LOG.debug("Already connected");
if(_connectEvent != null)
_connectEvent.unregister();
return;
}
// Keep track of how long you are waiting on connection
long now = System.currentTimeMillis();
long waitTime = now - _startedConnecting;
if (waitTime > MAX_CONNECT_WAIT_TIME) {
LOG.debug("Timed out, waited for: " + waitTime);
setConnectionState(ConnectionState.FIN);
_channel.eventPending();
} else {
// We cannot send the SYN until we've registered in the Multiplexor.
if(_myConnectionID != 0) {
// Build SYN message with my connectionID in it
SynMessage synMsg;
if (_theirConnectionID != UDPMultiplexor.UNASSIGNED_SLOT)
synMsg = _context.getMessageFactory().createSynMessage(_myConnectionID, _theirConnectionID, role);
else
synMsg = _context.getMessageFactory().createSynMessage(_myConnectionID, role);
LOG.debug("Sending SYN: " + synMsg);
// Send a SYN packet with our connectionID
send(synMsg);
}
scheduleConnectEvent(now + SYN_WAIT_TIME);
}
}
/**
* Handles an incoming SYN message. All initial and/or duplicate SYNs are acked.
* We set theirConnectionID once we see the first SYN. If a subsequent SYN has a different ID,
* that SYN is ignored.
*/
private void handleSynMessage(SynMessage smsg) {
// Extend the msgs sequenceNumber to 8 bytes based on past state
smsg.extendSequenceNumber(
_extender.extendSequenceNumber(
smsg.getSequenceNumber()) );
// First Message from other host - get his connectionID.
byte theirConnID = smsg.getSenderConnectionID();
LOG.debugf("our id: {0}, their id: {1}, their sender id: {2}", _theirConnectionID, theirConnID, smsg.getSenderConnectionID());
if ( _theirConnectionID == UDPMultiplexor.UNASSIGNED_SLOT ) {
// Keep track of their connectionID
_theirConnectionID = theirConnID;
} else if ( _theirConnectionID == theirConnID ) {
// Getting a duplicate SYN so just ack it again.
} else {
// Unmatching SYN so just ignore it
return;
}
// Ack their SYN message
safeSendAck(smsg);
}
/**
* Handles an ACK message.
* <p>
* If we're connecting, the first received ACK will advance the state
* to CONNECTED_STATE. Duplicate ACKs while connecting will be ignored.
* (Even though the state is moved to CONNECTED_STATE, we may not be
* be isConnected() until we also receive a SYN from them, informing
* us of their connection id.)
* <p>
* ACKs received in response to a FIN are tracked so that another FIN may
* be sent if the first was not acked (allowing the remote side to see that
* the connection was shutdown).
* <p>
* ACKs received while connected update the appropriate window & regulator
* structures.
*/
private void handleAckMessage(AckMessage amsg) {
// Extend the msgs sequenceNumber to 8 bytes based on past state
// Note that this sequence number is of local origin
amsg.extendSequenceNumber(
_localExtender.extendSequenceNumber(
amsg.getSequenceNumber()) );
// Extend the windowStart to 8 bytes the same as the
// sequenceNumber
amsg.extendWindowStart(
_localExtender.extendSequenceNumber(amsg.getWindowStart()) );
long seqNo = amsg.getSequenceNumber();
long wStart = amsg.getWindowStart();
int priorR = _receiverWindowSpace;
_receiverWindowSpace = amsg.getWindowSpace();
// Adjust the receivers window space with knowledge of
// how many extra messages we have sent since this ack
if ( _sequenceNumber > wStart )
_receiverWindowSpace =
DATA_WINDOW_SIZE + (int) (wStart - _sequenceNumber);
//_receiverWindowSpace += (wStart - _sequenceNumber);
// Reactivate writing if required
if ( (priorR == 0 || _waitingForDataSpace) &&
_receiverWindowSpace > 0 ) {
if(LOG.isDebugEnabled())
LOG.debug(" -- ACK wakeup");
writeSpaceActivation();
}
if ( seqNo == 0 && isConnecting()) {
_receivedSynAck = true;
} else if ( _waitingForFinAck && seqNo == _finSeqNo ) {
// A fin message has been acked on shutdown
_waitingForFinAck = false;
} else if (_connectionState == ConnectionState.CONNECTED) {
// Record the ack
_sendWindow.ackBlock(seqNo);
_writeRegulator.addMessageSuccess();
// Ensure that all messages up to sent windowStart are acked
_sendWindow.pseudoAckToReceiverWindow(amsg.getWindowStart());
// Clear out the acked blocks at window start
_sendWindow.clearLowAckedBlocks(_channel);
// Update the chunk limit for fast (nonlocking) access
_chunkLimit = _sendWindow.getWindowSpace();
}
}
/**
* Handles a DataMessage.
* <p>
* This will close the connection if the data size is too large.
* <p>
* If the sequence of the msg is below our start window
* (meaning we've already gobbled up this data), then the msg is
* ignored (but may be acked).
* <p>
* If the msg fits in our window space, we'll add to the incoming
* DataWindow.
* <p>
* An ack may be sent out to signify that we successfully received
* the message.
*
*/
private void handleDataMessage(DataMessage dmsg) {
// Extend the msgs sequenceNumber to 8 bytes based on past state
dmsg.extendSequenceNumber(
_extender.extendSequenceNumber(
dmsg.getSequenceNumber()) );
// Pass the data message to the output window
// If message is more than limit beyond window,
// then throw it away
long seqNo = dmsg.getSequenceNumber();
long baseSeqNo = _receiveWindow.getWindowStart();
// If data is too large then blow out the connection
// before any damage is done
if (dmsg.getDataLength() > MAX_DATA_SIZE) {
closeAndCleanup(FinMessage.REASON_LARGE_PACKET);
return;
}
if ( seqNo > (baseSeqNo + DATA_WRITE_AHEAD_MAX) ) {
if(LOG.isDebugEnabled())
LOG.debug("Received block num too far ahead: "+ seqNo);
return;
}
// Make sure the data is not before the window start
DataRecord drec = null;
if ( seqNo >= baseSeqNo ) {
// Record the receipt of the data in the receive window
drec = _receiveWindow.addData(dmsg);
} else {
if(LOG.isDebugEnabled())
LOG.debug("Received duplicate block num: "+
dmsg.getSequenceNumber());
}
//if this is the first data message we get, start the period now
if (_lastPeriod == 0)
_lastPeriod = _lastReceivedTime;
_packetsThisPeriod++;
_totalDataPackets++;
if (shouldSendAck()) {
if (drec != null) {
drec.ackTime = System.currentTimeMillis();
drec.acks++;
}
safeSendAck(dmsg);
}
// if this is the end of a period, record how many data packets we got
if (_lastReceivedTime - _lastPeriod >= _period) {
_lastPeriod = _lastReceivedTime;
_currentPeriodId++;
if (_currentPeriodId >= _periodHistory) {
_currentPeriodId=0;
_enoughData=true;
}
_periods[_currentPeriodId]=_packetsThisPeriod;
_packetsThisPeriod=0;
}
}
private boolean shouldSendAck() {
//if we have enough history, see if we should skip an ack
if (_skipAcks && _enoughData && _skippedAcks < _maxSkipAck) {
float average = 0;
for (int i = 0;i < _periodHistory;i++)
average+=_periods[i];
average /= _periodHistory;
// skip an ack if the rate at which we receive data has not dropped sharply
if (_periods[_currentPeriodId] > average / _deviation) {
_skippedAcks++;
_skippedAcksTotal++;
return false;
}
}
return true;
}
/**
* Handles a KeepAliveMessage.
* <p>
* If we're closed, a Fin message is sent in reply.
* All sent messages up to the keep alive's window-start are acked,
* and the window space for the remote side is updated.
*/
private void handleKeepAliveMessage(KeepAliveMessage kmsg) {
// No need to extend seqNo on KeepAliveMessage since it is zero
// Extend the windowStart to 8 bytes the same
// as the Ack
kmsg.extendWindowStart(
_localExtender.extendSequenceNumber(kmsg.getWindowStart()) );
// long seqNo = kmsg.getSequenceNumber();
long wStart = kmsg.getWindowStart();
int priorR = _receiverWindowSpace;
_receiverWindowSpace = kmsg.getWindowSpace();
//System.out.println("Keep alive: remote.windowStart=" + kmsg.getWindowStart() + ", remote.windowSpace=" + kmsg.getWindowSpace() + " local.sequenceNumber=" + _sequenceNumber + ", local.receiverWindowSpace=" + _receiverWindowSpace + ", local.chunkLimit=" + _chunkLimit + ", sendWindow.windowStart=" + _sendWindow.getWindowStart() + ", sendWindow.windowSize=" + _sendWindow.getWindowSize() + ", sendWindow.windowSpace=" + _sendWindow.getWindowSpace());
// Adjust the receivers window space with knowledge of
// how many extra messages we have sent since this ack
if ( _sequenceNumber > wStart )
_receiverWindowSpace =
DATA_WINDOW_SIZE + (int) (wStart - _sequenceNumber);
//_receiverWindowSpace += (wStart - _sequenceNumber);
// If receiving KeepAlives when closed, send another FinMessage
if ( isClosed() ) {
safeSendFin();
}
// Ensure that all messages up to sent windowStart are acked
// Note, you could get here preinitialization - in which case,
// do nothing.
if ( _sendWindow != null ) {
_sendWindow.pseudoAckToReceiverWindow(wStart);
// Clear out the acked blocks at window start
_sendWindow.clearLowAckedBlocks(_channel);
// Update the chunk limit for fast (nonlocking) access
_chunkLimit = _sendWindow.getWindowSpace();
// Reactivate writing if required
if ( (priorR == 0 || _waitingForDataSpace) &&
_receiverWindowSpace > 0 ) {
if(LOG.isDebugEnabled())
LOG.debug(" -- KA wakeup");
writeSpaceActivation();
}
}
}
/**
* Handles a Fin message.
* <p>
* This will close the connection.
*/
private void handleFinMessage(FinMessage msg) {
// Extend the msgs sequenceNumber to 8 bytes based on past state
msg.extendSequenceNumber(
_extender.extendSequenceNumber(
msg.getSequenceNumber()) );
// Stop sending data
_receiverWindowSpace = 0;
// Ack the Fin message
safeSendAck(msg);
// If a fin message is received then close connection
if ( !isClosed() )
closeAndCleanup(FinMessage.REASON_YOU_CLOSED);
}
/**
* Take action on a received message.
*/
protected synchronized void handleMessage(RUDPMessage msg) {
// Record when the last message was received
_lastReceivedTime = System.currentTimeMillis();
if (LOG.isDebugEnabled())
LOG.debug("handleMessage :" + msg + " t:" + _lastReceivedTime);
if (msg instanceof SynMessage) {
handleSynMessage((SynMessage) msg);
} else if (msg instanceof AckMessage) {
_lastDataOrAckTime = _lastReceivedTime;
handleAckMessage((AckMessage) msg);
} else if (msg instanceof DataMessage) {
_lastDataOrAckTime = _lastReceivedTime;
handleDataMessage((DataMessage) msg);
} else if (msg instanceof KeepAliveMessage) {
handleKeepAliveMessage((KeepAliveMessage) msg);
} else if (msg instanceof FinMessage) {
handleFinMessage((FinMessage) msg);
}
}
/**
* If there is data to be written then write it and schedule next write time.
*/
private synchronized void writeData() {
// Make sure we don't write without a break for too long
int noSleepCount = 0;
while (true) {
// Reset special flags for long wait times
_waitingForDataAvailable = false;
_waitingForDataSpace = false;
// If someone wanted us to wait a bit then don't send data now
if ( _skipADataWrite ) {
_skipADataWrite = false;
} else { // Otherwise, it is safe to send some data
// If there is room to send something then send data
// if available
if ( getChunkLimit() > 0 ) {
// Get data and send it
ByteBuffer chunk = _channel.getNextChunk();
if(chunk != null)
sendData(chunk);
} else {
//System.out.println("Waiting: sequenceNumber=" + _sequenceNumber + ", receiverWindowSpace=" + _receiverWindowSpace + ", chunkLimit=" + _chunkLimit + ", sendWindow.windowStart=" + _sendWindow.getWindowStart() + ", sendWindow.windowSize=" + _sendWindow.getWindowSize() + ", sendWindow.windowSpace=" + _sendWindow.getWindowSpace());
// if no room to send data then wait for the window to Open
// Don't wait more than 1 second for sanity checking
scheduleWriteDataEvent(
System.currentTimeMillis() + NOTHING_TO_DO_DELAY);
_waitingForDataSpace = true;
if(LOG.isDebugEnabled())
LOG.debug("Shutdown SendData cL:"+_chunkLimit+
" rWS:"+ _receiverWindowSpace);
return;
}
}
// Don't wait for next write if there is no chunk available.
// Writes will get rescheduled if a chunk becomes available.
synchronized(_channel.writeLock()) {
if (_channel.getNumberOfPendingChunks() == 0 ) {
// Don't wait more than 1 second for sanity checking
scheduleWriteDataEvent(
System.currentTimeMillis() + NOTHING_TO_DO_DELAY);
_waitingForDataAvailable = true;
if(LOG.isDebugEnabled())
LOG.debug("Shutdown SendData no pending");
return;
}
}
// Compute how long to wait
// TODO: Simplify experimental algorithm and plug it in
//long waitTime = (long)_sendWindow.getRTO() / 6l;
long currTime = System.currentTimeMillis();
long waitTime = _writeRegulator.getSleepTime(currTime,
_receiverWindowSpace);
// If we are getting too close to the end of window, make a note
if ( _receiverWindowSpace <= SMALL_SEND_WINDOW ) {
// Scale back on the writing speed if you are hitting limits
if ( _receiverWindowSpace <= 1 )
_writeRegulator.hitZeroWindow();
}
// Initially ensure waitTime is not too low
if (waitTime == 0 && _sequenceNumber < 10 )
waitTime = DEFAULT_RTO_WAIT_TIME;
// Enforce some minimal sleep time if we have been in tight loop
// This will allow handleMessages to get done if pending
if (noSleepCount >= MAX_WRITE_WITHOUT_SLEEP) {
waitTime += 1;
}
// Only wait if the waitTime is more than zero
if ( waitTime > 0 ) {
long time = System.currentTimeMillis() + waitTime;
//System.out.println("Write: sequenceNumber=" + _sequenceNumber + ", receiverWindowSpace=" + _receiverWindowSpace + ", chunkLimit=" + _chunkLimit + ", sendWindow.windowStart=" + _sendWindow.getWindowStart() + ", sendWindow.windowSize=" + _sendWindow.getWindowSize() + ", sendWindow.windowSpace=" + _sendWindow.getWindowSpace());
scheduleWriteDataEvent(time);
break;
}
// Count how long we are sending without a sleep
noSleepCount++;
}
}
/**
* Define what happens when a keepalive timer fires.
*/
static class KeepAliveTimerEvent extends UDPTimerEvent {
public KeepAliveTimerEvent(long time,UDPConnectionProcessor proc) {
super(time,proc);
}
@Override
protected void doActualEvent(UDPConnectionProcessor udpCon) {
long time = System.currentTimeMillis();
if(LOG.isDebugEnabled())
LOG.debug("keepalive: "+ time);
// If connection closed, then make sure that keepalives have ended
if (udpCon.isClosed() ) {
udpCon._keepaliveEvent.unregister();
return;
}
if (udpCon.isConnected()
&& (udpCon._lastDataOrAckTime + MAX_KEEPALIVE_TIME < time ||
udpCon._lastReceivedTime + MAX_MESSAGE_WAIT_TIME < time)
) {
LOG.debug("Keepalive generated shutdown");
udpCon.closeAndCleanup(FinMessage.REASON_TIMEOUT);
return;
}
// If reevaluation of the time still requires a keepalive then send
if ( time+1 >= (udpCon._lastSendTime + KEEPALIVE_WAIT_TIME) ) {
if ( udpCon.isConnected() ) {
udpCon.sendKeepAlive();
} else {
return;
}
}
// Reschedule keepalive timer
_eventTime = udpCon._lastSendTime + KEEPALIVE_WAIT_TIME;
udpCon._scheduler.scheduleEvent(this);
if(LOG.isDebugEnabled())
LOG.debug("end keepalive: "+ System.currentTimeMillis());
}
}
/**
* Define what happens when a WriteData timer event fires.
*/
static class WriteDataTimerEvent extends UDPTimerEvent {
public WriteDataTimerEvent(long time,UDPConnectionProcessor proc) {
super(time,proc);
}
@Override
protected void doActualEvent(UDPConnectionProcessor udpCon) {
if(LOG.isDebugEnabled())
LOG.debug("data timeout :"+ System.currentTimeMillis());
long time = System.currentTimeMillis();
// Make sure that some messages are received within timeframe
if ( udpCon.isConnected() &&
udpCon._lastReceivedTime + MAX_MESSAGE_WAIT_TIME < time ) {
// If no incoming messages for very long time then
// close connection
udpCon.closeAndCleanup(FinMessage.REASON_TIMEOUT);
return;
}
// If still connected then handle then try to write some data
if ( udpCon.isConnected() ) {
udpCon.writeData();
}
if(LOG.isDebugEnabled())
LOG.debug("end data timeout: "+ System.currentTimeMillis());
}
}
/**
* Define what happens when an ack timeout occurs
*/
static class AckTimeoutTimerEvent extends UDPTimerEvent {
public AckTimeoutTimerEvent(long time,UDPConnectionProcessor proc) {
super(time,proc);
}
@Override
protected void doActualEvent(UDPConnectionProcessor udpCon) {
if(LOG.isDebugEnabled())
LOG.debug("ack timeout: "+ System.currentTimeMillis());
if ( udpCon.isConnected() ) {
udpCon.validateAckedData();
}
if(LOG.isDebugEnabled())
LOG.debug("end ack timeout: "+ System.currentTimeMillis());
}
}
/**
* This is an event that wakes up writing with a given delay.
*/
static class SafeWriteWakeupTimerEvent extends UDPTimerEvent {
public SafeWriteWakeupTimerEvent(long time,UDPConnectionProcessor proc) {
super(time,proc);
}
@Override
protected void doActualEvent(UDPConnectionProcessor udpCon) {
if(LOG.isDebugEnabled())
LOG.debug("write wakeup timeout: "+ System.currentTimeMillis());
if ( udpCon.isConnected() ) {
udpCon.writeDataActivation();
}
_eventTime = Long.MAX_VALUE;
udpCon._scheduler.scheduleEvent(this);
if(LOG.isDebugEnabled())
LOG.debug("write wakeup timeout: "+ System.currentTimeMillis());
}
}
/**
* Event that will resend a Syn if we need to while connecting.
*/
static class ConnectSynEvent extends UDPTimerEvent {
public ConnectSynEvent(long time,UDPConnectionProcessor proc) {
super(time,proc);
}
@Override
protected void doActualEvent(UDPConnectionProcessor udpCon) {
_eventTime = Long.MAX_VALUE;
LOG.debug("Running SYN Event");
udpCon.tryToConnect();
}
}
/**
* Do final cleanup and shutdown after connection is closed.
*/
static class ClosedConnectionCleanupTimerEvent extends UDPTimerEvent {
public ClosedConnectionCleanupTimerEvent(long time, UDPConnectionProcessor proc) {
super(time,proc );
}
@Override
protected void doActualEvent(UDPConnectionProcessor udpCon) {
if(LOG.isDebugEnabled())
LOG.debug("Closed connection timeout: "+
System.currentTimeMillis());
udpCon.finalClose();
if(LOG.isDebugEnabled())
LOG.debug("Closed connection done: "+ System.currentTimeMillis());
unregister();
}
}
/**
* Makes sure the channel is closed after a certain amount of time.
*/
static class ChannelCloseTimerEvent extends UDPTimerEvent {
public ChannelCloseTimerEvent(long time, UDPConnectionProcessor proc) {
super(time,proc );
}
@Override
protected void doActualEvent(UDPConnectionProcessor udpCon) {
try {
udpCon._channel.close();
} catch(IOException ignored) {}
unregister();
}
}
}