package io.bitsquare.p2p.peers.peerexchange; import io.bitsquare.app.Log; import io.bitsquare.common.Timer; import io.bitsquare.common.UserThread; import io.bitsquare.p2p.Message; import io.bitsquare.p2p.NodeAddress; import io.bitsquare.p2p.network.*; import io.bitsquare.p2p.peers.PeerManager; import io.bitsquare.p2p.peers.peerexchange.messages.GetPeersRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; public class PeerExchangeManager implements MessageListener, ConnectionListener, PeerManager.Listener { private static final Logger log = LoggerFactory.getLogger(PeerExchangeManager.class); private static final long RETRY_DELAY_SEC = 10; private static final long RETRY_DELAY_AFTER_ALL_CON_LOST_SEC = 3; private static final long REQUEST_PERIODICALLY_INTERVAL_SEC = 10 * 60; private final NetworkNode networkNode; private final PeerManager peerManager; private final Set<NodeAddress> seedNodeAddresses; private final Map<NodeAddress, PeerExchangeHandler> handlerMap = new HashMap<>(); private Timer retryTimer, periodicTimer; private boolean stopped; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public PeerExchangeManager(NetworkNode networkNode, PeerManager peerManager, Set<NodeAddress> seedNodeAddresses) { this.networkNode = networkNode; this.peerManager = peerManager; // seedNodeAddresses can be empty (in case there is only 1 seed node, the seed node starting up has no other seed nodes) this.seedNodeAddresses = new HashSet<>(seedNodeAddresses); 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); stopPeriodicTimer(); stopRetryTimer(); closeAllHandlers(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void requestReportedPeersFromSeedNodes(NodeAddress nodeAddress) { checkNotNull(networkNode.getNodeAddress(), "My node address must not be null at requestReportedPeers"); ArrayList<NodeAddress> remainingNodeAddresses = new ArrayList<>(seedNodeAddresses); remainingNodeAddresses.remove(nodeAddress); Collections.shuffle(remainingNodeAddresses); requestReportedPeers(nodeAddress, remainingNodeAddresses); startPeriodicTimer(); } /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onConnection(Connection connection) { Log.traceCall(); } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { Log.traceCall(); closeHandler(connection); if (retryTimer == null) { retryTimer = UserThread.runAfter(() -> { log.trace("ConnectToMorePeersTimer called from onDisconnect code path"); stopRetryTimer(); requestWithAvailablePeers(); }, RETRY_DELAY_SEC); } if (peerManager.isNodeBanned(closeConnectionReason, connection)) seedNodeAddresses.remove(connection.getPeersNodeAddressOptional().get()); } @Override public void onError(Throwable throwable) { } /////////////////////////////////////////////////////////////////////////////////////////// // PeerManager.Listener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAllConnectionsLost() { Log.traceCall(); closeAllHandlers(); stopPeriodicTimer(); stopRetryTimer(); 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(); } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(Message message, Connection connection) { if (message instanceof GetPeersRequest) { Log.traceCall(message.toString() + "\n\tconnection=" + connection); if (!stopped) { if (peerManager.isSeedNode(connection)) connection.setPeerType(Connection.PeerType.SEED_NODE); GetPeersRequestHandler getPeersRequestHandler = new GetPeersRequestHandler(networkNode, peerManager, new GetPeersRequestHandler.Listener() { @Override public void onComplete() { log.trace("PeerExchangeHandshake completed.\n\tConnection={}", connection); } @Override public void onFault(String errorMessage, Connection connection) { log.trace("PeerExchangeHandshake failed.\n\terrorMessage={}\n\t" + "connection={}", errorMessage, connection); peerManager.handleConnectionFault(connection); } }); getPeersRequestHandler.handle((GetPeersRequest) message, connection); } else { log.warn("We have stopped already. We ignore that onMessage call."); } } } /////////////////////////////////////////////////////////////////////////////////////////// // Request /////////////////////////////////////////////////////////////////////////////////////////// private void requestReportedPeers(NodeAddress nodeAddress, List<NodeAddress> remainingNodeAddresses) { Log.traceCall("nodeAddress=" + nodeAddress); if (!stopped) { if (!handlerMap.containsKey(nodeAddress)) { PeerExchangeHandler peerExchangeHandler = new PeerExchangeHandler(networkNode, peerManager, new PeerExchangeHandler.Listener() { @Override public void onComplete() { log.trace("PeerExchangeHandshake of outbound connection complete. nodeAddress={}", nodeAddress); handlerMap.remove(nodeAddress); requestWithAvailablePeers(); } @Override public void onFault(String errorMessage, @Nullable Connection connection) { log.trace("PeerExchangeHandshake of outbound connection failed.\n\terrorMessage={}\n\t" + "nodeAddress={}", errorMessage, nodeAddress); peerManager.handleConnectionFault(nodeAddress); handlerMap.remove(nodeAddress); if (!remainingNodeAddresses.isEmpty()) { if (!peerManager.hasSufficientConnections()) { log.debug("There are remaining nodes available for requesting peers. " + "We will try getReportedPeers again."); NodeAddress nextCandidate = remainingNodeAddresses.get(new Random().nextInt(remainingNodeAddresses.size())); remainingNodeAddresses.remove(nextCandidate); requestReportedPeers(nextCandidate, remainingNodeAddresses); } else { // That path will rarely be reached log.debug("We have already sufficient connections."); } } else { log.debug("There is no remaining node available for requesting peers. " + "That is expected if no other node is online.\n\t" + "We will try again after a pause."); if (retryTimer == null) retryTimer = UserThread.runAfter(() -> { if (!stopped) { log.trace("retryTimer called from requestReportedPeers code path"); stopRetryTimer(); requestWithAvailablePeers(); } else { stopRetryTimer(); log.warn("We have stopped already. We ignore that retryTimer.run call."); } }, RETRY_DELAY_SEC); } } }); handlerMap.put(nodeAddress, peerExchangeHandler); peerExchangeHandler.sendGetPeersRequestAfterRandomDelay(nodeAddress); } else { log.trace("We have started already a peerExchangeHandler. " + "We ignore that call. nodeAddress=" + nodeAddress); } } else { log.trace("We have stopped already. We ignore that requestReportedPeers call."); } } private void requestWithAvailablePeers() { Log.traceCall(); if (!stopped) { if (!peerManager.hasSufficientConnections()) { // We create a new list of not connected candidates // 1. shuffled reported peers // 2. shuffled persisted peers // 3. Add as last shuffled seedNodes (least priority) List<NodeAddress> list = getFilteredNonSeedNodeList(getNodeAddresses(peerManager.getReportedPeers()), new ArrayList<>()); Collections.shuffle(list); List<NodeAddress> filteredPersistedPeers = getFilteredNonSeedNodeList(getNodeAddresses(peerManager.getPersistedPeers()), list); Collections.shuffle(filteredPersistedPeers); list.addAll(filteredPersistedPeers); List<NodeAddress> filteredSeedNodeAddresses = getFilteredList(new ArrayList<>(seedNodeAddresses), list); Collections.shuffle(filteredSeedNodeAddresses); list.addAll(filteredSeedNodeAddresses); log.debug("Number of peers in list for connectToMorePeers: {}", list.size()); log.trace("Filtered connectToMorePeers list: list=" + list); if (!list.isEmpty()) { // Dont shuffle as we want the seed nodes at the last entries NodeAddress nextCandidate = list.get(0); list.remove(nextCandidate); requestReportedPeers(nextCandidate, list); } else { log.debug("No more peers are available for requestReportedPeers. We will try again after a pause."); if (retryTimer == null) retryTimer = UserThread.runAfter(() -> { if (!stopped) { log.trace("retryTimer called from requestWithAvailablePeers code path"); stopRetryTimer(); requestWithAvailablePeers(); } else { stopRetryTimer(); log.warn("We have stopped already. We ignore that retryTimer.run call."); } }, RETRY_DELAY_SEC); } } else { log.debug("We have already sufficient connections."); } } else { log.trace("We have stopped already. We ignore that requestWithAvailablePeers call."); } } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void startPeriodicTimer() { stopped = false; if (periodicTimer == null) periodicTimer = UserThread.runPeriodically(this::requestWithAvailablePeers, REQUEST_PERIODICALLY_INTERVAL_SEC, TimeUnit.SECONDS); } private void restart() { startPeriodicTimer(); if (retryTimer == null) { retryTimer = UserThread.runAfter(() -> { stopped = false; log.trace("retryTimer called from restart"); stopRetryTimer(); requestWithAvailablePeers(); }, RETRY_DELAY_AFTER_ALL_CON_LOST_SEC); } else { log.debug("retryTimer already started"); } } private List<NodeAddress> getNodeAddresses(Collection<Peer> collection) { return collection.stream() .map(e -> e.nodeAddress) .collect(Collectors.toList()); } private List<NodeAddress> getFilteredList(Collection<NodeAddress> collection, List<NodeAddress> list) { return collection.stream() .filter(e -> !list.contains(e) && !peerManager.isSelf(e) && !peerManager.isConfirmed(e)) .collect(Collectors.toList()); } private List<NodeAddress> getFilteredNonSeedNodeList(Collection<NodeAddress> collection, List<NodeAddress> list) { return getFilteredList(collection, list).stream() .filter(e -> !peerManager.isSeedNode(e)) .collect(Collectors.toList()); } private void stopPeriodicTimer() { stopped = true; if (periodicTimer != null) { periodicTimer.stop(); periodicTimer = null; } } private void stopRetryTimer() { if (retryTimer != null) { retryTimer.stop(); retryTimer = null; } } private void closeHandler(Connection connection) { Log.traceCall(); Optional<NodeAddress> peersNodeAddressOptional = connection.getPeersNodeAddressOptional(); if (peersNodeAddressOptional.isPresent()) { NodeAddress nodeAddress = peersNodeAddressOptional.get(); if (handlerMap.containsKey(nodeAddress)) { handlerMap.get(nodeAddress).cancel(); handlerMap.remove(nodeAddress); } } else { log.trace("closeHandler: nodeAddress not set in connection " + connection); } } private void closeAllHandlers() { Log.traceCall(); handlerMap.values().stream().forEach(PeerExchangeHandler::cancel); handlerMap.clear(); } }