package edu.washington.cs.oneswarm.f2f.servicesharing;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.gudy.azureus2.core3.util.DirectByteBuffer;
import org.gudy.azureus2.core3.util.ReferenceCountedDirectByteBuffer;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FChannelDataMsg;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FHashSearch;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FHashSearchResp;
import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessage;
import edu.washington.cs.oneswarm.f2f.network.DelayedExecutorService;
import edu.washington.cs.oneswarm.f2f.network.DelayedExecutorService.DelayedExecutor;
import edu.washington.cs.oneswarm.f2f.network.FriendConnection;
import edu.washington.cs.oneswarm.f2f.network.OverlayEndpoint;
import edu.washington.cs.oneswarm.f2f.network.OverlayTransport;
/**
* This class represents one Friend connection channel used for multiplexed
* service channels.
* Functionality extends from {@code OverlayEndpoint}, the additional
* functionality from
* this class is that received data is forwarded to the aggregate service
* connection, and
* outstanding sent data is tracked for congestion control across channels.
*
* @author willscott
*
*/
public class ServiceChannelEndpoint extends OverlayEndpoint {
public final static Logger logger = Logger.getLogger(ServiceChannelEndpoint.class.getName());
private static final byte ss = 0;
// Moving average sampling weight for latency estimation.
private static final double EWMA = 0.25;
// How long (in # RTT) before packet retransmission.
private static final double RETRANSMISSION_MIN = 2;
private static final double RETRANSMISSION_MAX = 3;
public static final int MAX_SERVICE_MESSAGE_SIZE = 1024;
private final DelayedExecutor delayedExecutor;
protected final Hashtable<SequenceNumber, sentMessage> sentMessages;
protected final Hashtable<Short, ServiceChannelEndpointDelegate> delegates = new Hashtable<Short, ServiceChannelEndpointDelegate>();
protected final ArrayList<Short> delegateOrder = new ArrayList<Short>();
private int outstandingBytes;
private long latency = 1000;
private long minLatency = Long.MAX_VALUE;
private final long serviceKey;
public ServiceChannelEndpoint(FriendConnection connection, OSF2FHashSearch search,
OSF2FHashSearchResp response,
boolean outgoing) {
super(connection, response.getPathID(), 0, search, response, outgoing);
logger.info("Service Channel Endpoint Created.");
this.sentMessages = new Hashtable<SequenceNumber, sentMessage>();
this.outstandingBytes = 0;
this.delayedExecutor = DelayedExecutorService.getInstance().getVariableDelayExecutor();
this.serviceKey = search.getInfohashhash();
this.started = true;
friendConnection.isReadyForWrite(new OverlayTransport.WriteQueueWaiter() {
@Override
public void readyForWrite() {
logger.info("friend connection marked ready for write.");
for (ServiceChannelEndpointDelegate d : ServiceChannelEndpoint.this.delegates
.values()) {
d.channelIsReady(ServiceChannelEndpoint.this);
}
}
});
}
public void addDelegate(ServiceChannelEndpointDelegate d, short flow) {
if (d.writesMessages()) {
this.delegateOrder.add(flow);
}
this.delegates.put(flow, d);
if (friendConnection.isReadyForWrite(null)) {
d.channelIsReady(this);
}
}
public void removeDelegate(ServiceChannelEndpointDelegate d) {
for (Short flow : this.delegates.keySet()) {
if (this.delegates.get(flow).equals(d)) {
this.delegates.remove(flow);
this.delegateOrder.remove(flow);
break;
}
}
}
public int getPotentialWriteCapacity() {
int channelCapacity = friendConnection.getSendQueuePotentialCapacity(this.channelId);
return channelCapacity / this.delegateOrder.size();
}
public int getWriteCapacity(ServiceChannelEndpointDelegate d) {
int networkCapacity = friendConnection.getSendQueueCurrentCapacity(this.channelId);
int fullPackets = networkCapacity / (this.delegates.size() * MAX_SERVICE_MESSAGE_SIZE);
int delegatePriority = this.delegates.size();
for (Short flow : this.delegates.keySet()) {
if (this.delegates.get(flow).equals(d)) {
delegatePriority = this.delegateOrder.indexOf(flow);
}
}
networkCapacity -= fullPackets * this.delegates.size() * MAX_SERVICE_MESSAGE_SIZE
+ delegatePriority * MAX_SERVICE_MESSAGE_SIZE;
if (networkCapacity >= MAX_SERVICE_MESSAGE_SIZE) {
fullPackets += 1;
}
return fullPackets * MAX_SERVICE_MESSAGE_SIZE;
}
@Override
public void start() {
// TODO(willscott): allow server to open channels.
}
@Override
public boolean isStarted() {
if (!this.outgoing) {
return this.getBytesIn() > 0;
}
return friendConnection.isHandshakeReceived();
}
@Override
protected void destroyBufferedMessages() {
// No buffered messages to destroy.
for (sentMessage b : this.sentMessages.values()) {
b.msg.returnToPool();
}
this.sentMessages.clear();
this.outstandingBytes = 0;
}
@Override
public void cleanup() {
for (ServiceChannelEndpointDelegate d : this.delegates.values()) {
d.channelDidClose(this);
}
};
@Override
protected void handleDelayedOverlayMessage(OSF2FChannelDataMsg msg) {
if (logger.isLoggable(Level.FINEST)) {
logger.finest("incoming message: " + msg.getDescription());
}
if (closed) {
return;
}
if (!this.isStarted()) {
start();
}
if (!(msg instanceof OSF2FServiceDataMsg)) {
logger.warning("Msg wasn't SDM: " + msg.getDescription());
return;
}
OSF2FServiceDataMsg newMessage = (OSF2FServiceDataMsg) msg;
// logger.fine("Received msg with sequence number " +
if (!newMessage.isAck()) {
logger.finest("ack enqueued for " + newMessage.getDescription());
super.writeMessage(OSF2FServiceDataMsg.acknowledge(OSF2FMessage.CURRENT_VERSION,
channelId, newMessage.getSubchannel(),
new int[] { newMessage.getSequenceNumber() }, newMessage.isDatagram()));
}
for (ServiceChannelEndpointDelegate d : this.delegates.values()) {
if (d.channelGotMessage(this, newMessage)) {
break;
}
}
}
public long getServiceKey() {
return this.serviceKey;
}
public void writeMessage(final SequenceNumber num, DirectByteBuffer buffer, boolean datagram) {
// Move the requester to the bottom of the priority list.
try {
this.delegateOrder.remove(num.getFlow());
} catch (IndexOutOfBoundsException e) {
return;
}
this.delegateOrder.add(num.getFlow());
boolean rst = buffer == null;
if (buffer == null) {
buffer = new DirectByteBuffer(ByteBuffer.allocate(0));
}
int length = buffer.remaining(ss);
ReferenceCountedDirectByteBuffer cpy = buffer.getReferenceCountedBuffer();
sentMessage msg = new sentMessage(num, cpy, length, 0, datagram, rst);
writeMessage(msg);
}
private void writeMessage(sentMessage msg) {
SequenceNumber num = msg.num;
synchronized (sentMessages) {
this.sentMessages.put(num, msg);
}
this.outstandingBytes += msg.length;
double retransmit = RETRANSMISSION_MIN + (RETRANSMISSION_MAX - RETRANSMISSION_MIN)
* Math.random();
// Remember the message may need to be retransmitted.
delayedExecutor.queue((long) (retransmit * this.latency * (1 << msg.attempt)), msg);
if (msg.attempt > 0 && msg.creation + latency > System.currentTimeMillis()) {
logger.warning("Skipping over-aggresive retransmission.");
return;
}
msg.creation = System.currentTimeMillis();
// Outgoing msg will be freed by super.writeMessage.
msg.msg.incrementReferenceCount();
OSF2FServiceDataMsg outgoing = new OSF2FServiceDataMsg(OSF2FMessage.CURRENT_VERSION,
channelId, num.getNum(), num.getFlow(), new int[0], msg.msg);
if (num.getNum() == 0 && !msg.rst) {
// Mark SYN messages.
outgoing.setControlFlag(4);
}
if (msg.rst) {
outgoing.setControlFlag(2);
}
if (msg.datagram) {
// Set datagram flag to allow the packet to be sent over UDP.
outgoing.setDatagram(true);
}
long totalWritten = msg.length;
if (logger.isLoggable(Level.FINEST)) {
logger.finest(String.format("Wrote %s to network. bytes: %d", num, msg.length));
}
super.writeMessage(outgoing);
bytesOut += totalWritten;
}
public int getOutstanding() {
return this.outstandingBytes;
}
/**
* Get the recent latency experienced on the channel. Latency is recorded as
* an exponentially weighted moving average. Each acknowledgment is weighted
* as some fraction of the total latency, and previous samples are decayed
* accordingly.
*
* @return Channel latency estimate.
*/
public long getLatency() {
return this.latency;
}
public DirectByteBuffer getMessage(SequenceNumber num) {
synchronized (sentMessages) {
sentMessage m = this.sentMessages.get(num);
if (m != null) {
return m.msg;
}
return null;
}
}
/**
* Attempt to forget a sent message.
*
* @param num
* The message to forget
* @return True if the message was successfully stopped from retransmitting.
*/
public boolean forgetMessage(SequenceNumber num) {
synchronized (sentMessages) {
sentMessage msg = this.sentMessages.remove(num);
if (msg == null) {
return false;
}
msg.cancel();
this.outstandingBytes -= msg.length;
long now = System.currentTimeMillis();
long sample = now - msg.creation;
// If not the first attempt, we don't know which attempt was acked.
if (msg.attempt == 0) {
this.latency = (long) (this.latency * (1 - EWMA) + sample * EWMA);
if (sample < minLatency) {
minLatency = sample;
}
// Pending messages sent before this one were potentially lost
sentMessage[] messages = this.sentMessages.values().toArray(new sentMessage[0]);
for (sentMessage m : messages) {
if (m.creation < msg.creation) {
m.run();
}
}
}
}
return true;
}
@Override
protected boolean isService() {
return true;
}
private class sentMessage extends TimerTask {
public ReferenceCountedDirectByteBuffer msg;
public int length;
private final int position;
public long creation;
private final SequenceNumber num;
private int attempt;
private final boolean datagram;
public final boolean rst;
public sentMessage(SequenceNumber num, ReferenceCountedDirectByteBuffer msg, int length,
int attempt, boolean datagram, boolean rst) {
this.creation = System.currentTimeMillis();
this.msg = msg;
this.position = msg.position(ss);
msg.incrementReferenceCount();
this.length = length;
this.num = num;
this.attempt = attempt;
this.datagram = datagram;
this.rst = rst;
}
@Override
public void run() {
synchronized (sentMessages) {
sentMessage self = sentMessages.remove(num);
if (self == null || closed) {
return;
}
if (self.attempt != attempt) {
logger.warning("Message queue concurency issues");
sentMessages.put(num, self);
return;
}
this.attempt += 1;
logger.fine("retransmitting " + num + ", try " + attempt);
outstandingBytes -= length;
msg.position(ss, position);
writeMessage(this);
}
}
@Override
public boolean cancel() {
msg.returnToPool();
return super.cancel();
}
}
}