package io.bitsquare.p2p.peers.getdata; 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.getdata.messages.GetDataRequest; import io.bitsquare.p2p.peers.peerexchange.Peer; import io.bitsquare.p2p.storage.P2PDataStorage; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; public class RequestDataManager implements MessageListener, ConnectionListener, PeerManager.Listener { private static final Logger log = LoggerFactory.getLogger(RequestDataManager.class); private static final long RETRY_DELAY_SEC = 10; private static final long CLEANUP_TIMER = 120; private boolean isPreliminaryDataRequest = true; /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onPreliminaryDataReceived(); void onUpdatedDataReceived(); void onDataReceived(); void onNoPeersAvailable(); void onNoSeedNodeAvailable(); } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final P2PDataStorage dataStorage; private final PeerManager peerManager; private final Collection<NodeAddress> seedNodeAddresses; private final Listener listener; private final Map<NodeAddress, RequestDataHandler> handlerMap = new HashMap<>(); private Map<String, GetDataRequestHandler> getDataRequestHandlers = new HashMap<>(); private Optional<NodeAddress> nodeAddressOfPreliminaryDataRequest = Optional.empty(); private Timer retryTimer; private boolean dataUpdateRequested; private boolean stopped; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public RequestDataManager(NetworkNode networkNode, P2PDataStorage dataStorage, PeerManager peerManager, Set<NodeAddress> seedNodeAddresses, Listener listener) { this.networkNode = networkNode; this.dataStorage = dataStorage; 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); this.listener = listener; networkNode.addMessageListener(this); networkNode.addConnectionListener(this); peerManager.addListener(this); } public void shutDown() { Log.traceCall(); stopped = true; stopRetryTimer(); networkNode.removeMessageListener(this); networkNode.removeConnectionListener(this); peerManager.removeListener(this); closeAllHandlers(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void requestPreliminaryData() { Log.traceCall(); ArrayList<NodeAddress> nodeAddresses = new ArrayList<>(seedNodeAddresses); if (!nodeAddresses.isEmpty()) { Collections.shuffle(nodeAddresses); NodeAddress nextCandidate = nodeAddresses.get(0); nodeAddresses.remove(nextCandidate); isPreliminaryDataRequest = true; requestData(nextCandidate, nodeAddresses); } } public void requestUpdateData() { Log.traceCall(); checkArgument(nodeAddressOfPreliminaryDataRequest.isPresent(), "nodeAddressOfPreliminaryDataRequest must be present"); dataUpdateRequested = true; List<NodeAddress> remainingNodeAddresses = new ArrayList<>(seedNodeAddresses); if (!remainingNodeAddresses.isEmpty()) { Collections.shuffle(remainingNodeAddresses); NodeAddress candidate = nodeAddressOfPreliminaryDataRequest.get(); remainingNodeAddresses.remove(candidate); isPreliminaryDataRequest = false; requestData(candidate, remainingNodeAddresses); } } public Optional<NodeAddress> getNodeAddressOfPreliminaryDataRequest() { return nodeAddressOfPreliminaryDataRequest; } /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onConnection(Connection connection) { Log.traceCall(); } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { Log.traceCall(); closeHandler(connection); if (peerManager.isNodeBanned(closeConnectionReason, connection)) { final NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get(); seedNodeAddresses.remove(nodeAddress); handlerMap.remove(nodeAddress); } } @Override public void onError(Throwable throwable) { } /////////////////////////////////////////////////////////////////////////////////////////// // PeerManager.Listener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAllConnectionsLost() { Log.traceCall(); closeAllHandlers(); 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 GetDataRequest) { Log.traceCall(message.toString() + "\n\tconnection=" + connection); if (!stopped) { if (peerManager.isSeedNode(connection)) connection.setPeerType(Connection.PeerType.SEED_NODE); final String uid = connection.getUid(); if (!getDataRequestHandlers.containsKey(uid)) { GetDataRequestHandler getDataRequestHandler = new GetDataRequestHandler(networkNode, dataStorage, new GetDataRequestHandler.Listener() { @Override public void onComplete() { getDataRequestHandlers.remove(uid); log.trace("requestDataHandshake completed.\n\tConnection={}", connection); } @Override public void onFault(String errorMessage, @Nullable Connection connection) { getDataRequestHandlers.remove(uid); if (!stopped) { log.trace("GetDataRequestHandler failed.\n\tConnection={}\n\t" + "ErrorMessage={}", connection, errorMessage); peerManager.handleConnectionFault(connection); } else { log.warn("We have stopped already. We ignore that getDataRequestHandler.handle.onFault call."); } } }); getDataRequestHandlers.put(uid, getDataRequestHandler); getDataRequestHandler.handle((GetDataRequest) message, connection); } else { log.warn("We have already a GetDataRequestHandler for that connection started. " + "We start a cleanup timer if the handler has not closed by itself in between 2 minutes."); UserThread.runAfter(() -> { if (getDataRequestHandlers.containsKey(uid)) { GetDataRequestHandler handler = getDataRequestHandlers.get(uid); handler.stop(); getDataRequestHandlers.remove(uid); } }, CLEANUP_TIMER); } } else { log.warn("We have stopped already. We ignore that onMessage call."); } } } /////////////////////////////////////////////////////////////////////////////////////////// // RequestData /////////////////////////////////////////////////////////////////////////////////////////// private void requestData(NodeAddress nodeAddress, List<NodeAddress> remainingNodeAddresses) { Log.traceCall("nodeAddress=" + nodeAddress + " / remainingNodeAddresses=" + remainingNodeAddresses); if (!stopped) { if (!handlerMap.containsKey(nodeAddress)) { RequestDataHandler requestDataHandler = new RequestDataHandler(networkNode, dataStorage, peerManager, new RequestDataHandler.Listener() { @Override public void onComplete() { log.trace("RequestDataHandshake of outbound connection complete. nodeAddress={}", nodeAddress); stopRetryTimer(); // need to remove before listeners are notified as they cause the update call handlerMap.remove(nodeAddress); // 1. We get a response from requestPreliminaryData if (!nodeAddressOfPreliminaryDataRequest.isPresent()) { nodeAddressOfPreliminaryDataRequest = Optional.of(nodeAddress); listener.onPreliminaryDataReceived(); } // 2. Later we get a response from requestUpdatesData if (dataUpdateRequested) { dataUpdateRequested = false; listener.onUpdatedDataReceived(); } listener.onDataReceived(); } @Override public void onFault(String errorMessage, @Nullable Connection connection) { log.trace("requestDataHandshake with outbound connection failed.\n\tnodeAddress={}\n\t" + "ErrorMessage={}", nodeAddress, errorMessage); peerManager.handleConnectionFault(nodeAddress); handlerMap.remove(nodeAddress); if (!remainingNodeAddresses.isEmpty()) { log.debug("There are remaining nodes available for requesting data. " + "We will try requestDataFromPeers again."); NodeAddress nextCandidate = remainingNodeAddresses.get(0); remainingNodeAddresses.remove(nextCandidate); requestData(nextCandidate, remainingNodeAddresses); } else { log.debug("There is no remaining node available for requesting data. " + "That is expected if no other node is online.\n\t" + "We will try to use reported peers (if no available we use persisted peers) " + "and try again to request data from our seed nodes after a random pause."); // Notify listeners if (!nodeAddressOfPreliminaryDataRequest.isPresent()) { if (peerManager.isSeedNode(nodeAddress)) listener.onNoSeedNodeAvailable(); else listener.onNoPeersAvailable(); } restart(); } } }); handlerMap.put(nodeAddress, requestDataHandler); requestDataHandler.requestData(nodeAddress, isPreliminaryDataRequest); } else { log.warn("We have started already a requestDataHandshake to peer. nodeAddress=" + nodeAddress + "\n" + "We start a cleanup timer if the handler has not closed by itself in between 2 minutes."); UserThread.runAfter(() -> { if (handlerMap.containsKey(nodeAddress)) { RequestDataHandler handler = handlerMap.get(nodeAddress); handler.stop(); handlerMap.remove(nodeAddress); } }, CLEANUP_TIMER); } } else { log.warn("We have stopped already. We ignore that requestData call."); } } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void restart() { Log.traceCall(); if (retryTimer == null) { retryTimer = UserThread.runAfter(() -> { log.trace("retryTimer called"); stopped = false; stopRetryTimer(); // We create a new list of candidates // 1. shuffled seedNodes // 2. reported peers sorted by last activity date // 3. Add as last persisted peers sorted by last activity date List<NodeAddress> list = getFilteredList(new ArrayList<>(seedNodeAddresses), new ArrayList<>()); Collections.shuffle(list); List<NodeAddress> filteredReportedPeers = getFilteredNonSeedNodeList(getSortedNodeAddresses(peerManager.getReportedPeers()), list); list.addAll(filteredReportedPeers); List<NodeAddress> filteredPersistedPeers = getFilteredNonSeedNodeList(getSortedNodeAddresses(peerManager.getPersistedPeers()), list); list.addAll(filteredPersistedPeers); if (!list.isEmpty()) { NodeAddress nextCandidate = list.get(0); list.remove(nextCandidate); requestData(nextCandidate, list); } }, RETRY_DELAY_SEC); } } private List<NodeAddress> getSortedNodeAddresses(Collection<Peer> collection) { return collection.stream() .collect(Collectors.toList()) .stream() .sorted((o1, o2) -> o2.date.compareTo(o1.date)) .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)) .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 stopRetryTimer() { if (retryTimer != null) { retryTimer.stop(); retryTimer = null; } } private void closeHandler(Connection connection) { 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("closeRequestDataHandler: nodeAddress not set in connection " + connection); } } private void closeAllHandlers() { handlerMap.values().stream().forEach(RequestDataHandler::cancel); handlerMap.clear(); } }