package se.sics.gvod.ls.video;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import se.sics.asdistances.ASDistances;
import se.sics.gvod.common.RetryComponentDelegator;
import se.sics.gvod.common.Self;
import se.sics.gvod.common.VodDescriptor;
import se.sics.gvod.croupier.events.CroupierSample;
import se.sics.gvod.ls.interas.events.InterAsSample;
import se.sics.gvod.ls.system.LSConfig;
import se.sics.gvod.ls.video.snapshot.VideoStats;
import se.sics.gvod.ls.video.util.VideoComparator;
import se.sics.gvod.net.VodAddress;
import se.sics.gvod.net.VodNetwork;
import se.sics.gvod.video.msgs.VideoConnectionMsg;
import se.sics.kompics.Positive;
/**
* Holds
*
* @author Niklas Wahlén <nwahlen@kth.se>
*/
public class VideoNeighbours {
// Video references
private RetryComponentDelegator delegator;
private Self self;
private Positive<VodNetwork> network;
// Collections
private List<VodDescriptor> croupierSample;
private List<VodDescriptor> interAsSample;
private List<VodAddress> randomNeighbours;
private List<VodAddress> closeNeighbours;
// Tools
private VideoComparator comparator;
private Random random;
// Inter-AS balancing -- if set to true a connection request to a
// random peer is sent the next round
private boolean incRandomIn;
// Connection configuration and statistics
private int maxOutConnectionsClose, maxOutConnectionsRandom;
private int ingoingClose, ingoingRandom;
private boolean source;
// Connection timeouts, only necessary for outgoing
private Map<VodAddress, Long> outgoingCloseTimeouts;
private Map<VodAddress, Long> outgoingRandomTimeouts;
public VideoNeighbours(RetryComponentDelegator delegator, Self self, Positive<VodNetwork> network, boolean source) {
// Video references
this.delegator = delegator;
this.self = self;
this.network = network;
// Collections
croupierSample = new ArrayList<VodDescriptor>();
interAsSample = new ArrayList<VodDescriptor>();
closeNeighbours = new ArrayList<VodAddress>();
randomNeighbours = new ArrayList<VodAddress>();
// Tools
random = new Random(LSConfig.getSeed());
comparator = new VideoComparator(self);
// Inter-AS balancing -- if set to true a connection request
// to a random peer is sent the next round
incRandomIn = false;
// Connection configuration and statistics
maxOutConnectionsClose = LSConfig.VIDEO_MAX_OUT_CLOSE;
maxOutConnectionsRandom = source ? LSConfig.VIDEO_SOURCE_MAX_OUT_RANDOM : LSConfig.VIDEO_MAX_OUT_RANDOM;
ingoingClose = 0;
ingoingRandom = 0;
this.source = source;
// Connection timeouts, only necessary for outgoing
outgoingCloseTimeouts = new HashMap<VodAddress, Long>();
outgoingRandomTimeouts = new HashMap<VodAddress, Long>();
}
public void cycle() {
/*
* A connection request is sent each turn. Depending on the current
* stream quality it is either sent to a close or a random peer.
*/
if (!source) {
if (incRandomIn) {
sendRandomConnectionRequests(1);
incRandomIn = false;
} else {
sendInterAsConnectionRequests(1);
// if the peer can't get enough InterAS connections, a random request should also be sent
// (what this does is, if the peer do not send random requests by itself,
// even though it should, we will do so for it, however InterAS requests have to
// continue being sent, otherwise the peer will of course never get any)
if (getIngoingInterAs() < 5) {
sendRandomConnectionRequests(1);
}
}
}
// Handle outgoing timeouts
Long currentTime = System.currentTimeMillis();
for (int i = 0; i < closeNeighbours.size(); i++) {
VodAddress a = closeNeighbours.get(i);
Long ts = outgoingCloseTimeouts.get(a);
if ((ts != null) && ((currentTime - ts) > LSConfig.VIDEO_CLOSE_NEIGHBOUR_TIMEOUT)) {
closeNeighbours.remove(a);
outgoingCloseTimeouts.remove(a);
sendDisconnect(a, false);
}
}
for (int i = 0; i < randomNeighbours.size(); i++) {
VodAddress a = randomNeighbours.get(i);
Long ts = outgoingRandomTimeouts.get(a);
if ((ts != null) && ((currentTime - ts) > LSConfig.VIDEO_RANDOM_NEIGHBOUR_TIMEOUT)) {
randomNeighbours.remove(a);
outgoingRandomTimeouts.remove(a);
sendDisconnect(a, true);
}
}
VideoStats.instance(self).setIngoingConnectionsClose(ingoingClose);
VideoStats.instance(self).setIngoingConnectionsRandom(ingoingRandom);
VideoStats.instance(self).setOutgoingConnectionsClose(closeNeighbours.size());
VideoStats.instance(self).setOutgoingConnectionsRandom(randomNeighbours.size());
reportAvgAsHops();
}
/*
* Sample operations
*/
public void handleInterAsSample(InterAsSample sample) {
this.interAsSample = sample.getDescriptors();
}
public void handleCroupierSample(CroupierSample sample) {
this.croupierSample = sample.getNodes();
}
/*
* Connection operations
*/
public void sendConnectionRequest(VodAddress destination, boolean randomRequest) {
VideoConnectionMsg.Request request =
new VideoConnectionMsg.Request(self.getAddress(), destination, randomRequest);
delegator.doRetry(request, self.getOverlayId());
if (randomRequest) {
VideoStats.instance(self).incConnectionRequestsSentRandom();
} else {
VideoStats.instance(self).incConnectionRequestsSentClose();
}
}
public void sendInterAsConnectionRequests(int n) {
Collections.shuffle(interAsSample, random);
for (int i = 0; i < n && i < interAsSample.size(); i++) {
VodDescriptor peer = interAsSample.get(i);
if (peer.getVodAddress().getId() != self.getId()) {
if (!contains(peer.getVodAddress())) {
sendConnectionRequest(peer.getVodAddress(), false);
}
}
}
}
public void sendRandomConnectionRequests(int n) {
Collections.shuffle(croupierSample, random);
for (int i = 0; i < n && i < croupierSample.size(); i++) {
VodDescriptor peer = croupierSample.get(i);
if (peer.getVodAddress().getId() != self.getId()) {
if (!contains(peer.getVodAddress())) {
sendConnectionRequest(peer.getVodAddress(), true);
}
}
}
}
public void handleConnectionRequestTimeout(VideoConnectionMsg.RequestTimeout timeout) {
if (timeout.getRequestMsg().isRandomRequest()) {
VideoStats.instance(self).incConnectionRequestTimeoutsRandom();
} else {
VideoStats.instance(self).incConnectionRequestTimeoutsClose();
}
// TODO: implement connection timeout handler
}
public void handleConnectionRequest(VideoConnectionMsg.Request request) {
if (request.isRandomRequest()) {
handleRandomConnectionRequest(request);
} else {
if (request.getVodSource().getId() != self.getId()) {
if (updateClose(request.getVodSource())) {
VideoConnectionMsg.Response response = new VideoConnectionMsg.Response(request, true);
delegator.doTrigger(response, network);
outgoingCloseTimeouts.put(request.getVodSource(), System.currentTimeMillis());
}
}
}
}
public void handleRandomConnectionRequest(VideoConnectionMsg.Request request) {
if (updateRandom(request.getVodSource())) {
VideoConnectionMsg.Response response = new VideoConnectionMsg.Response(
request, true);
delegator.doTrigger(response, network);
outgoingRandomTimeouts.put(request.getVodSource(), System.currentTimeMillis());
}
}
public void handleConnectionResponse(VideoConnectionMsg.Response response) {
if (response.getVodSource().getId() != self.getId() && response.connectionAccepted()) {
if (response.wasRandomRequest()) {
VideoStats.instance(self).incConnectionResponsesReceivedRandom();
ingoingRandom++;
} else {
VideoStats.instance(self).incConnectionResponsesReceivedClose();
ingoingClose++;
}
}
}
public void sendDisconnect(VodAddress destination, boolean randomConnection) {
delegator.doTrigger(new VideoConnectionMsg.Disconnect(self.getAddress(),
destination, randomConnection), network);
if (randomConnection) {
VideoStats.instance(self).incDisconnectsSentRandom();
} else {
VideoStats.instance(self).incDisconnectsSentClose();
}
}
public void handleDisconnetion(VideoConnectionMsg.Disconnect disconnect) {
if (disconnect.wasRandomConnection()) {
VideoStats.instance(self).incDisconnectsReceivedRandom();
ingoingRandom--;
} else {
VideoStats.instance(self).incDisconnectsReceivedClose();
ingoingClose--;
}
}
/*
* Collection operations
*/
public int closeNeighboursSize() {
return closeNeighbours.size();
}
public int randomNeighboursSize() {
return randomNeighbours.size();
}
public int totalSize() {
return closeNeighbours.size() + randomNeighbours.size();
}
public boolean isEmpty() {
return totalSize() == 0;
}
/**
* Evaluates whether to add a peer as a close neighbour or not.
*
* @param possibleNeighbour
* @return Returns true if the peer was added as a neighbour.
*/
public boolean updateClose(VodAddress possibleNeighbour) {
boolean added = false;
if (possibleNeighbour.getId() == self.getId()) {
return added;
}
if (closeNeighboursSize() >= maxOutConnectionsClose) {
Collections.sort(closeNeighbours, comparator);
// If the ratio of random neighbours has been changed several
// neighbours may have to be removed
while (closeNeighboursSize() - 1 >= maxOutConnectionsClose) {
VodAddress removed = closeNeighbours.remove(closeNeighbours.size() - 1);
sendDisconnect(removed, false);
}
int worstIndex = closeNeighbours.size() - 1;
VodAddress worst = closeNeighbours.get(worstIndex);
if (comparator.compare(worst, possibleNeighbour) < 1) {
VodAddress removed = closeNeighbours.remove(worstIndex);
outgoingCloseTimeouts.remove(removed);
sendDisconnect(removed, false);
added = closeNeighbours.add(possibleNeighbour);
}
} else {
added = closeNeighbours.add(possibleNeighbour);
}
return added;
}
public boolean updateClose(Collection<VodDescriptor> possibleNeighbours) {
boolean changed = false;
for (VodDescriptor d : possibleNeighbours) {
if (updateClose(d.getVodAddress())) {
changed = true;
}
}
return changed;
}
/**
* Adds a random peer as a neighbour.
*
* @param newRandomNeighbour
* @return
*/
public boolean updateRandom(VodAddress newRandomNeighbour) {
if (maxOutConnectionsRandom == 0) {
return false;
}
while (randomNeighbours.size() >= (maxOutConnectionsRandom)
&& randomNeighbours.size() > 0) {
VodAddress removed = randomNeighbours.remove(random.nextInt(randomNeighbours.size()));
outgoingRandomTimeouts.remove(removed);
sendDisconnect(removed, true);
}
return randomNeighbours.add(newRandomNeighbour);
}
public boolean contains(VodAddress a) {
return (containsClose(a) || containsRandom(a));
}
public boolean containsClose(VodAddress a) {
for (VodAddress a1 : closeNeighbours) {
if (a1.getId() == a.getId()) {
return true;
}
}
return false;
}
public boolean containsRandom(VodAddress a) {
for (VodAddress a1 : randomNeighbours) {
if (a1.getId() == a.getId()) {
return true;
}
}
return false;
}
public boolean removeClose(VodAddress d) {
return closeNeighbours.remove(d);
}
public boolean removeRandom(VodAddress d) {
return randomNeighbours.remove(d);
}
public Collection<VodAddress> getNRandomNeighbours(int n) {
List<VodAddress> rNeighbours = new ArrayList<VodAddress>();
if (n >= totalSize()) {
rNeighbours.addAll(closeNeighbours);
rNeighbours.addAll(randomNeighbours);
return rNeighbours;
}
rNeighbours.addAll(closeNeighbours);
rNeighbours.addAll(randomNeighbours);
Collections.shuffle(rNeighbours, random);
rNeighbours = rNeighbours.subList(0, n);
return rNeighbours;
}
public Collection<VodAddress> getNRandomCloseNeighbours(int n) {
if (n >= closeNeighbours.size()) {
return closeNeighbours;
}
Collections.shuffle(closeNeighbours, random);
return closeNeighbours.subList(0, n);
}
public Collection<VodAddress> getNRandomRandomNeighbours(int n) {
if (n >= randomNeighbours.size()) {
return randomNeighbours;
}
Collections.shuffle(randomNeighbours, random);
return randomNeighbours.subList(0, n);
}
public void incRandom(boolean inc) {
incRandomIn = inc;
}
private double getRandomRatio() {
return (double) ((double) randomNeighboursSize() / (randomNeighboursSize() + closeNeighboursSize()));
}
public int getIngoingConnections() {
return ingoingClose + ingoingRandom;
}
public int getIngoingInterAs() {
return ingoingClose;
}
public void mildPanic() {
sendInterAsConnectionRequests(1);
sendRandomConnectionRequests(1);
}
/*
* Statistics operations
*/
public void incAvailablePieces(VodAddress a) {
comparator.incAvailablePieces(a);
}
public void updateLastResponse(VodAddress a) {
comparator.updateLastSeen(a);
}
public void incTimeouts(VodAddress a) {
comparator.incTimeouts(a);
}
public VodAddress getWorstNeighbour() {
if (closeNeighbours.isEmpty()) {
return null;
}
Collections.sort(closeNeighbours, comparator);
return closeNeighbours.get(closeNeighbours.size() - 1);
}
public int compare(VodAddress a1, VodAddress a2) {
return comparator.compare(a1, a2);
}
public short getDistanceTo(VodAddress a) {
return comparator.getDistanceTo(a);
}
public void updateTimstamp(VodAddress a) {
// Only updates timestamps for existing, i.e. not adding any new
long l = System.currentTimeMillis();
// put() should replace but doesn't seem to always
if (outgoingCloseTimeouts.containsKey(a)) {
outgoingCloseTimeouts.remove(a);
outgoingCloseTimeouts.put(a, l);
}
// Don't update random forever, we want to disconnect them sometime to lower inter-AS traffic
// Could set a counter for how many times they are allowed to be updated, or just
// set a higher total timeout and to no updates of the timeout
// if (outgoingRandomTimeouts.containsKey(a)) {
// outgoingRandomTimeouts.remove(a);
// outgoingRandomTimeouts.put(a, l);
// }
}
private void reportAvgAsHops() {
Collection<VodAddress> allNeighbours = new ArrayList<VodAddress>();
allNeighbours.addAll(closeNeighbours);
allNeighbours.addAll(randomNeighbours);
if (allNeighbours.isEmpty()) {
VideoStats.instance(self).setAvgAsHopsToNeighbours(0);
} else {
int sumAsHops = 0;
for (VodAddress d : allNeighbours) {
ASDistances distances = ASDistances.getInstance();
sumAsHops += distances.getDistance(self.getAddress().getIp().getHostAddress(), d.getIp().getHostAddress());
}
VideoStats.instance(self).setAvgAsHopsToNeighbours((float) ((float) sumAsHops) / ((float) allNeighbours.size()));
}
}
}