package io.bitsquare.p2p.peers;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import io.bitsquare.app.Log;
import io.bitsquare.common.Timer;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.util.Utilities;
import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.p2p.network.Connection;
import io.bitsquare.p2p.network.NetworkNode;
import io.bitsquare.p2p.storage.messages.BroadcastMessage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class BroadcastHandler implements PeerManager.Listener {
///////////////////////////////////////////////////////////////////////////////////////////
// Static
///////////////////////////////////////////////////////////////////////////////////////////
private static final Logger log = LoggerFactory.getLogger(BroadcastHandler.class);
private static final long TIMEOUT_PER_PEER_SEC = 30;
interface ResultHandler {
void onCompleted(BroadcastHandler broadcastHandler);
void onFault(BroadcastHandler broadcastHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener {
void onBroadcasted(BroadcastMessage message, int numOfCompletedBroadcasts);
void onBroadcastedToFirstPeer(BroadcastMessage message);
void onBroadcastCompleted(BroadcastMessage message, int numOfCompletedBroadcasts, int numOfFailedBroadcasts);
void onBroadcastFailed(String errorMessage);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Instance fields
///////////////////////////////////////////////////////////////////////////////////////////
private final NetworkNode networkNode;
public final String uid;
private PeerManager peerManager;
private boolean stopped = false;
private int numOfCompletedBroadcasts = 0;
private int numOfFailedBroadcasts = 0;
private BroadcastMessage message;
private ResultHandler resultHandler;
@Nullable
private Listener listener;
private int numOfPeers;
private Timer timeoutTimer;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public BroadcastHandler(NetworkNode networkNode, PeerManager peerManager) {
this.networkNode = networkNode;
this.peerManager = peerManager;
peerManager.addListener(this);
uid = UUID.randomUUID().toString();
}
public void cancel() {
stopped = true;
onFault("Broadcast canceled.", false);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, ResultHandler resultHandler,
@Nullable Listener listener, boolean isDataOwner) {
this.message = message;
this.resultHandler = resultHandler;
this.listener = listener;
Log.traceCall("Sender=" + sender + "\n\t" +
"Message=" + Utilities.toTruncatedString(message));
Set<Connection> connectedPeersSet = networkNode.getConfirmedConnections()
.stream()
.filter(connection -> !connection.getPeersNodeAddressOptional().get().equals(sender))
.collect(Collectors.toSet());
if (!connectedPeersSet.isEmpty()) {
numOfCompletedBroadcasts = 0;
List<Connection> connectedPeersList = new ArrayList<>(connectedPeersSet);
Collections.shuffle(connectedPeersList);
numOfPeers = connectedPeersList.size();
int delay = 50;
if (!isDataOwner) {
// for not data owner (relay nodes) we send to max. 7 nodes and use a longer delay
numOfPeers = Math.min(7, connectedPeersList.size());
delay = 100;
}
long timeoutDelay = TIMEOUT_PER_PEER_SEC * numOfPeers;
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
String errorMessage = "Timeout: Broadcast did not complete after " + timeoutDelay + " sec.";
log.debug(errorMessage + "\n\t" +
"numOfPeers=" + numOfPeers + "\n\t" +
"numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n\t" +
"numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n\t" +
"numOfFailedBroadcasts=" + numOfFailedBroadcasts);
onFault(errorMessage, false);
}, timeoutDelay);
log.debug("Broadcast message to {} peers out of {} total connected peers.", numOfPeers, connectedPeersSet.size());
for (int i = 0; i < numOfPeers; i++) {
if (stopped)
break; // do not continue sending after a timeout or a cancellation
final long minDelay = (i + 1) * delay;
final long maxDelay = (i + 2) * delay;
final Connection connection = connectedPeersList.get(i);
UserThread.runAfterRandomDelay(() -> sendToPeer(connection, message), minDelay, maxDelay, TimeUnit.MILLISECONDS);
}
} else {
onFault("Message not broadcasted because we have no available peers yet.\n\t" +
"message = " + Utilities.toTruncatedString(message), false);
}
}
private void sendToPeer(Connection connection, BroadcastMessage message) {
String errorMessage = "Message not broadcasted because we have stopped the handler already.\n\t" +
"message = " + Utilities.toTruncatedString(message);
if (!stopped) {
if (!connection.isStopped()) {
if (!connection.isCapabilityRequired(message) || connection.isCapabilitySupported(message)) {
NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get();
log.trace("Broadcast message to " + nodeAddress + ".");
SettableFuture<Connection> future = networkNode.sendMessage(connection, message);
Futures.addCallback(future, new FutureCallback<Connection>() {
@Override
public void onSuccess(Connection connection) {
numOfCompletedBroadcasts++;
if (!stopped) {
log.trace("Broadcast to " + nodeAddress + " succeeded.");
if (listener != null)
listener.onBroadcasted(message, numOfCompletedBroadcasts);
if (listener != null && numOfCompletedBroadcasts == 1)
listener.onBroadcastedToFirstPeer(message);
if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numOfPeers) {
if (listener != null)
listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts);
cleanup();
resultHandler.onCompleted(BroadcastHandler.this);
}
} else {
// TODO investigate why that is called very often at seed nodes
onFault("stopped at onSuccess: " + errorMessage, false);
}
}
@Override
public void onFailure(@NotNull Throwable throwable) {
numOfFailedBroadcasts++;
if (!stopped) {
log.debug("Broadcast to " + nodeAddress + " failed.\n\t" +
"ErrorMessage=" + throwable.getMessage());
if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numOfPeers)
onFault("stopped at onFailure: " + errorMessage);
} else {
onFault("stopped at onFailure: " + errorMessage);
}
}
});
}
} else {
onFault("Connection stopped already", false);
}
} else {
onFault("stopped at sendToPeer: " + errorMessage, false);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// PeerManager.Listener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onAllConnectionsLost() {
onFault("All connections lost", false);
}
@Override
public void onNewConnectionAfterAllConnectionsLost() {
}
@Override
public void onAwakeFromStandby() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void cleanup() {
stopped = true;
peerManager.removeListener(this);
if (timeoutTimer != null) {
timeoutTimer.stop();
timeoutTimer = null;
}
}
private void onFault(String errorMessage) {
onFault(errorMessage, true);
}
private void onFault(String errorMessage, boolean logWarning) {
cleanup();
if (logWarning)
log.warn(errorMessage);
else
log.debug(errorMessage);
if (listener != null)
listener.onBroadcastFailed(errorMessage);
if (listener != null && (numOfCompletedBroadcasts + numOfFailedBroadcasts == numOfPeers || stopped))
listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts);
resultHandler.onFault(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BroadcastHandler)) return false;
BroadcastHandler that = (BroadcastHandler) o;
return !(uid != null ? !uid.equals(that.uid) : that.uid != null);
}
@Override
public int hashCode() {
return uid != null ? uid.hashCode() : 0;
}
}