package net.tomp2p.relay;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.tomp2p.connection.PeerConnection;
import net.tomp2p.connection.PeerException;
import net.tomp2p.connection.PeerException.AbortCause;
import net.tomp2p.futures.BaseFutureAdapter;
import net.tomp2p.futures.FutureBootstrap;
import net.tomp2p.futures.FutureDone;
import net.tomp2p.futures.FutureResponse;
import net.tomp2p.p2p.Peer;
import net.tomp2p.p2p.builder.BootstrapBuilder;
import net.tomp2p.peers.Number160;
import net.tomp2p.peers.PeerAddress;
import net.tomp2p.peers.PeerMapChangeListener;
import net.tomp2p.peers.PeerSocketAddress;
import net.tomp2p.peers.PeerStatistic;
import net.tomp2p.utils.ConcurrentCacheSet;
/**
* The relay manager is responsible for setting up and maintaining connections
* to relay peers and contains all information about the relays.
*
* @author Raphael Voellmy
* @author Thomas Bocek
* @author Nico Rutishauser
*
*/
public class DistributedRelay implements PeerMapChangeListener {
private final static Logger LOG = LoggerFactory.getLogger(DistributedRelay.class);
private final Peer peer;
private final RelayRPC relayRPC;
//private final List<BaseRelayClient> relayClients;
private final Set<PeerAddress> failedRelays;
private final Map<PeerAddress, PeerConnection> activeClients;
private FutureDone<Void> shutdownFuture = new FutureDone<Void>();
private volatile boolean shutdown = false;
private boolean allRelays = false;
final private ExecutorService executorService;
final private BootstrapBuilder bootstrapBuilder;
final private RelayCallback relayCallback;
private volatile List<PeerAddress> relays;
/**
* @param peer
* the unreachable peer
* @param relayRPC
* the relay RPC
* @param maxFail
* @param relayConfig
* @param bootstrapBuilder
* @param maxRelays
* maximum number of relay peers to set up
* @param relayType
* the kind of the relay connection
*/
public DistributedRelay(final Peer peer, RelayRPC relayRPC, ExecutorService executorService, BootstrapBuilder bootstrapBuilder, RelayCallback relayCallback) {
this.peer = peer;
this.relayRPC = relayRPC;
this.executorService = executorService;
this.bootstrapBuilder = bootstrapBuilder;
this.relayCallback = relayCallback;
activeClients = Collections.synchronizedMap(new HashMap<PeerAddress, PeerConnection>());
failedRelays = new ConcurrentCacheSet<PeerAddress>(60);
peer.peerBean().peerMap().addPeerMapChangeListener(this);
}
/**
* Returns connections to current relay peers
*
* @return List of PeerAddresses of the relay peers (copy)
*/
public Map<PeerAddress, PeerConnection> activeClients() {
synchronized (activeClients) {
// make a copy
return Collections.unmodifiableMap(new HashMap<PeerAddress, PeerConnection>(activeClients));
}
}
/*public void addRelayListener(RelayListener relayListener) {
synchronized (relayListeners) {
relayListeners.add(relayListener);
}
}*/
public FutureDone<Void> shutdown() {
shutdown = true;
peer.peerBean().peerMap().removePeerMapChangeListener(this);
synchronized (activeClients) {
for (Map.Entry<PeerAddress, PeerConnection> entry: activeClients.entrySet()) {
entry.getValue().close();
}
}
executorService.shutdown();
synchronized (peer) {
peer.notify();
}
relayCallback.onShutdown();
return shutdownFuture;
}
/**
* Sets up relay connections to other peers. The number of relays to set up
* is determined by {@link PeerAddress#MAX_RELAYS} or passed to the
* constructor of this class. It is important that we call this after we
* bootstrapped and have recent information in our peer map.
*
* @return RelayFuture containing a {@link DistributedRelay} instance
*/
public DistributedRelay setupRelays(List<PeerAddress> relays) {
this.relays = Collections.synchronizedList(relays);
executorService.submit(new Runnable() {
@Override
public void run() {
try {
startConnectionsLoop();
} catch (Exception e) {
relayCallback.onFailure(e);
}
}
});
return this;
}
private List<PeerAddress> relayCandidates() {
final List<PeerAddress> relayCandidates;
if (relays.isEmpty()) {
// Get the neighbors of this peer that could possibly act as relays. Relay
// candidates are neighboring peers that are not relayed themselves and have
// not recently failed as relay or denied acting as relay.
relayCandidates = peer.distributedRouting().peerMap().all();
} else {
synchronized (relays) {
relayCandidates = new ArrayList<PeerAddress>(relays);
}
}
relayCandidates.removeAll(failedRelays);
//filterRelayCandidates
for (Iterator<PeerAddress> iterator = relayCandidates.iterator(); iterator.hasNext();) {
PeerAddress candidate = iterator.next();
// filter peers that are relayed themselves
if (candidate.relaySize() > 0) {
iterator.remove();
continue;
}
// Remove recently failed relays, peers that are relayed themselves and
// peers that are already relays
if (activeClients.containsKey(candidate)) {
iterator.remove();
}
}
LOG.trace("Found {} additional relay candidates: {}, failed are {}", relayCandidates.size(), relayCandidates, failedRelays);
return relayCandidates;
}
/**
* The relay setup is called sequentially until the number of max relays is reached. If a peerconnection goes down, it will search for other relays
* @param relayCallback
* @throws InterruptedException
*/
private void startConnectionsLoop() throws InterruptedException {
if(shutdown && activeClients.isEmpty()) {
shutdownFuture.done();
LOG.debug("shutting down, don't restart relays");
return;
}
if(shutdown) {
return;
}
if(activeClients.size() >= 5) {
LOG.debug("we have enough relays");
allRelays = true;
relayCallback.onFullRelays(activeClients.size());
updatePeerMap();
//wait at most x seconds for a restart of the loop
executorService.submit(new Runnable() {
@Override
public void run() {
try {
synchronized (peer) {
peer.wait(60 * 1000);
}
startConnectionsLoop();
} catch (Exception e) {
relayCallback.onFailure(e);
}
}
});
return;
}
//get candidates
final List<PeerAddress> relayCandidates = relayCandidates();
if(relayCandidates.isEmpty()) {
LOG.debug("no more relays");
relayCallback.onNoMoreRelays(activeClients.size());
updatePeerMap();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
synchronized (peer) {
peer.wait(60 * 1000);
}
startConnectionsLoop();
} catch (Exception e) {
relayCallback.onFailure(e);
}
}
});
return;
}
final PeerAddress candidate = relayCandidates.get(0);
final FutureDone<PeerConnection> futureDone = relayRPC.sendSetupMessage(candidate);
futureDone.addListener(new BaseFutureAdapter<FutureDone<PeerConnection>>() {
@Override
public void operationComplete(final FutureDone<PeerConnection> future)
throws Exception {
if(future.isSuccess()) {
LOG.debug("found relay: {}", candidate);
activeClients.put(candidate, future.object());
updatePeerAddress();
relayCallback.onRelayAdded(candidate, future.object());
future.object().closeFuture().addListener(new BaseFutureAdapter<FutureDone<Void>>() {
@Override
public void operationComplete(final FutureDone<Void> futureClose)
throws Exception {
LOG.debug("lost/offline relay: {}", candidate);
//we need to notify our map, since we know this peer is offline, TODO: make this generic for all PeerConnections
peer.peerBean().peerMap().peerFailed(candidate, new PeerException(AbortCause.SHUTDOWN, "remote open peer connection was closed"));
failedRelays.add(future.object().remotePeer());
activeClients.remove(candidate);
updatePeerAddress();
relayCallback.onRelayRemoved(candidate, future.object());
//notify to loop now - this may not do anything if we are shutting down
synchronized (peer) {
allRelays = false;
peer.notify();
}
if(shutdown && activeClients.isEmpty()) {
shutdownFuture.done();
}
}
});
} else {
LOG.debug("bad relay: {}", candidate);
failedRelays.add(candidate);
activeClients.remove(candidate);
updatePeerAddress();
relayCallback.onRelayRemoved(candidate, future.object());
}
//loop again
startConnectionsLoop();
}
});
}
/**
* Updates the peer's PeerAddress: Adds the relay addresses to the peer
* address, updates the firewalled flags, and bootstraps to announce its new
* relay peers.
*/
private void updatePeerAddress() {
final boolean hasRelays;
final Collection<PeerSocketAddress> socketAddresses;
synchronized (activeClients) {
// add relay addresses to peer address
hasRelays = !activeClients.isEmpty();
socketAddresses = new ArrayList<PeerSocketAddress>(activeClients.size());
//we can have more than the max relay count in our active client list.
int max = 5;
int i = 0;
for (PeerAddress relay : activeClients.keySet()) {
if(relay.ipv4Flag()) {
socketAddresses.add(relay.ipv4Socket());
}
if(relay.ipv6Flag()) {
socketAddresses.add(relay.ipv6Socket());
}
if(i++ >= max) {
break;
}
}
}
// update firewalled and isRelayed flags
PeerAddress newAddress = peer.peerAddress().withRelays(socketAddresses);
peer.peerBean().serverPeerAddress(newAddress);
LOG.debug("Updated peer address {}, isrelay = {}", newAddress, hasRelays);
}
@Override
public void peerInserted(PeerAddress peerAddress, boolean verified) {
LOG.debug("new peer added, go again "+peerAddress+ " / "+verified);
synchronized (peer) {
if(!allRelays) {
peer.notify();
}
}
}
@Override
public void peerRemoved(PeerAddress peerAddress,
PeerStatistic storedPeerAddress) {}
@Override
public void peerUpdated(PeerAddress peerAddress,
PeerStatistic storedPeerAddress) {}
private void updatePeerMap() {
// bootstrap to get updated peer map and then push it to the relay peers
bootstrapBuilder.start().addListener(new BaseFutureAdapter<FutureBootstrap>() {
@Override
public void operationComplete(FutureBootstrap future)
throws Exception {
// send the peer map to the relays
List<Map<Number160, PeerStatistic>> peerMapVerified = relayRPC.peer().peerBean().peerMap().peerMapVerified();
for (final Map.Entry<PeerAddress, PeerConnection> entry : activeClients().entrySet()) {
FutureResponse fr = relayRPC.sendPeerMap(entry.getKey(), entry.getValue(), peerMapVerified);
//if we have buffered messages, send reply
fr.addListener(new BaseFutureAdapter<FutureResponse>() {
@Override
public void operationComplete(FutureResponse future) throws Exception {
if(future.isSuccess()) {
relayRPC.handleBuffer(future.responseMessage());
}
}
});
LOG.debug("send peermap to {}", entry.getKey());
}
}
});
}
/*@Override
public FutureDone<Void> sendBufferRequest(String relayPeerId) {
for (BaseRelayClient relayConnection : relayClients()) {
String peerId = relayConnection.relayAddress().peerId().toString();
if (peerId.equals(relayPeerId) && relayConnection instanceof BufferedRelayClient) {
return ((BufferedRelayClient) relayConnection).sendBufferRequest();
}
}
LOG.warn("No connection to relay {} found. Ignoring the message.", relayPeerId);
return new FutureDone<Void>().failed("No connection to relay " + relayPeerId + " found");
}*/
}