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.Connection; import io.bitsquare.p2p.network.MessageListener; import io.bitsquare.p2p.network.NetworkNode; 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 javax.annotation.Nullable; import java.util.Random; import java.util.concurrent.TimeUnit; class KeepAliveHandler implements MessageListener { private static final Logger log = LoggerFactory.getLogger(KeepAliveHandler.class); private static int DELAY_MS = 10_000; /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onComplete(); void onFault(String errorMessage); } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final PeerManager peerManager; private final Listener listener; private final int nonce = new Random().nextInt(); @Nullable private Connection connection; private boolean stopped; private Timer delayTimer; private long sendTs; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public KeepAliveHandler(NetworkNode networkNode, PeerManager peerManager, Listener listener) { this.networkNode = networkNode; this.peerManager = peerManager; this.listener = listener; } public void cancel() { cleanup(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void sendPingAfterRandomDelay(Connection connection) { delayTimer = UserThread.runAfterRandomDelay(() -> sendPing(connection), 1, DELAY_MS, TimeUnit.MILLISECONDS); } private void sendPing(Connection connection) { Log.traceCall("connection=" + connection + " / this=" + this); if (!stopped) { Ping ping = new Ping(nonce, connection.getStatistic().roundTripTimeProperty().get()); sendTs = System.currentTimeMillis(); SettableFuture<Connection> future = networkNode.sendMessage(connection, ping); Futures.addCallback(future, new FutureCallback<Connection>() { @Override public void onSuccess(Connection connection) { if (!stopped) { log.trace("Send " + ping + " to " + connection + " succeeded."); KeepAliveHandler.this.connection = connection; connection.addMessageListener(KeepAliveHandler.this); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call."); } } @Override public void onFailure(@NotNull Throwable throwable) { if (!stopped) { String errorMessage = "Sending ping to " + connection + " failed. That is expected if the peer is offline.\n\tping=" + ping + ".\n\tException=" + throwable.getMessage(); log.debug(errorMessage); cleanup(); //peerManager.shutDownConnection(connection, CloseConnectionReason.SEND_MSG_FAILURE); peerManager.handleConnectionFault(connection); listener.onFault(errorMessage); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call."); } } }); } else { log.trace("We have stopped already. We ignore that sendPing call."); } } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(Message message, Connection connection) { if (message instanceof Pong) { Log.traceCall(message.toString() + "\n\tconnection=" + connection); if (!stopped) { Pong pong = (Pong) message; if (pong.requestNonce == nonce) { int roundTripTime = (int) (System.currentTimeMillis() - sendTs); log.trace("roundTripTime=" + roundTripTime + "\n\tconnection=" + connection); connection.getStatistic().setRoundTripTime(roundTripTime); cleanup(); listener.onComplete(); } else { log.warn("Nonce not matching. That should never happen.\n\t" + "We drop that message. nonce={} / requestNonce={}", nonce, pong.requestNonce); } } else { log.trace("We have stopped already. We ignore that onMessage call."); } } } private void cleanup() { stopped = true; if (connection != null) connection.removeMessageListener(this); if (delayTimer != null) { delayTimer.stop(); delayTimer = null; } } }