package io.bitsquare.p2p.peers.keepalive;
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.p2p.Message;
import io.bitsquare.p2p.network.*;
import io.bitsquare.p2p.peers.PeerManager;
import io.bitsquare.p2p.peers.keepalive.messages.Ping;
import io.bitsquare.p2p.peers.keepalive.messages.Pong;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class KeepAliveManager implements MessageListener, ConnectionListener, PeerManager.Listener {
private static final Logger log = LoggerFactory.getLogger(KeepAliveManager.class);
private static final int INTERVAL_SEC = new Random().nextInt(5) + 30;
private static final long LAST_ACTIVITY_AGE_MS = INTERVAL_SEC / 2;
private final NetworkNode networkNode;
private final PeerManager peerManager;
private final Map<String, KeepAliveHandler> handlerMap = new HashMap<>();
private boolean stopped;
private Timer keepAliveTimer;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public KeepAliveManager(NetworkNode networkNode, PeerManager peerManager) {
this.networkNode = networkNode;
this.peerManager = peerManager;
networkNode.addMessageListener(this);
networkNode.addConnectionListener(this);
peerManager.addListener(this);
}
public void shutDown() {
Log.traceCall();
stopped = true;
networkNode.removeMessageListener(this);
networkNode.removeConnectionListener(this);
peerManager.removeListener(this);
closeAllHandlers();
stopKeepAliveTimer();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void start() {
restart();
}
///////////////////////////////////////////////////////////////////////////////////////////
// MessageListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onMessage(Message message, Connection connection) {
if (message instanceof Ping) {
Log.traceCall(message.toString() + "\n\tconnection=" + connection);
if (!stopped) {
Ping ping = (Ping) message;
// We get from peer last measured rrt
connection.getStatistic().setRoundTripTime(ping.lastRoundTripTime);
Pong pong = new Pong(ping.nonce);
SettableFuture<Connection> future = networkNode.sendMessage(connection, pong);
Futures.addCallback(future, new FutureCallback<Connection>() {
@Override
public void onSuccess(Connection connection) {
log.trace("Pong sent successfully");
}
@Override
public void onFailure(@NotNull Throwable throwable) {
if (!stopped) {
String errorMessage = "Sending pong to " + connection +
" failed. That is expected if the peer is offline. pong=" + pong + "." +
"Exception: " + throwable.getMessage();
log.debug(errorMessage);
peerManager.handleConnectionFault(connection);
} else {
log.warn("We have stopped already. We ignore that networkNode.sendMessage.onFailure call.");
}
}
});
} else {
log.warn("We have stopped already. We ignore that onMessage call.");
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// ConnectionListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onConnection(Connection connection) {
Log.traceCall();
}
@Override
public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) {
Log.traceCall();
closeHandler(connection);
}
@Override
public void onError(Throwable throwable) {
}
///////////////////////////////////////////////////////////////////////////////////////////
// PeerManager.Listener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onAllConnectionsLost() {
Log.traceCall();
closeAllHandlers();
stopKeepAliveTimer();
stopped = true;
restart();
}
@Override
public void onNewConnectionAfterAllConnectionsLost() {
Log.traceCall();
closeAllHandlers();
stopped = false;
restart();
}
@Override
public void onAwakeFromStandby() {
Log.traceCall();
closeAllHandlers();
stopped = false;
if (!networkNode.getAllConnections().isEmpty())
restart();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void restart() {
if (keepAliveTimer == null)
keepAliveTimer = UserThread.runPeriodically(() -> {
stopped = false;
keepAlive();
}, INTERVAL_SEC);
}
private void keepAlive() {
if (!stopped) {
Log.traceCall();
networkNode.getConfirmedConnections().stream()
.filter(connection -> connection instanceof OutboundConnection &&
connection.getStatistic().getLastActivityAge() > LAST_ACTIVITY_AGE_MS)
.forEach(connection -> {
final String uid = connection.getUid();
if (!handlerMap.containsKey(uid)) {
KeepAliveHandler keepAliveHandler = new KeepAliveHandler(networkNode, peerManager, new KeepAliveHandler.Listener() {
@Override
public void onComplete() {
handlerMap.remove(uid);
}
@Override
public void onFault(String errorMessage) {
handlerMap.remove(uid);
}
});
handlerMap.put(uid, keepAliveHandler);
keepAliveHandler.sendPingAfterRandomDelay(connection);
} else {
// TODO check if this situation causes any issues
log.debug("Connection with id {} has not completed and is still in our map. " +
"We will try to ping that peer at the next schedule.", uid);
}
});
int size = handlerMap.size();
log.debug("handlerMap size=" + size);
if (size > peerManager.getMaxConnections())
log.warn("Seems we didn't clean up out map correctly.\n" +
"handlerMap size={}, peerManager.getMaxConnections()={}", size, peerManager.getMaxConnections());
} else {
log.warn("We have stopped already. We ignore that keepAlive call.");
}
}
private void stopKeepAliveTimer() {
stopped = true;
if (keepAliveTimer != null) {
keepAliveTimer.stop();
keepAliveTimer = null;
}
}
private void closeHandler(Connection connection) {
String uid = connection.getUid();
if (handlerMap.containsKey(uid)) {
handlerMap.get(uid).cancel();
handlerMap.remove(uid);
}
}
private void closeAllHandlers() {
handlerMap.values().stream().forEach(KeepAliveHandler::cancel);
handlerMap.clear();
}
}