Coverage Summary for Class: SyncPool (org.ethereum.sync)

Class Class, % Method, % Line, %
SyncPool 0% (0/1) 0% (0/20) 0% (0/133)


1 /* 2  * This file is part of RskJ 3  * Copyright (C) 2017 RSK Labs Ltd. 4  * (derived from ethereumJ library, Copyright (c) 2016 <ether.camp>) 5  * 6  * This program is free software: you can redistribute it and/or modify 7  * it under the terms of the GNU Lesser General Public License as published by 8  * the Free Software Foundation, either version 3 of the License, or 9  * (at your option) any later version. 10  * 11  * This program is distributed in the hope that it will be useful, 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14  * GNU Lesser General Public License for more details. 15  * 16  * You should have received a copy of the GNU Lesser General Public License 17  * along with this program. If not, see <http://www.gnu.org/licenses/>. 18  */ 19  20 package org.ethereum.sync; 21  22 import co.rsk.config.InternalService; 23 import co.rsk.config.RskSystemProperties; 24 import co.rsk.core.BlockDifficulty; 25 import co.rsk.net.NodeBlockProcessor; 26 import co.rsk.net.NodeID; 27 import org.ethereum.core.Blockchain; 28 import org.ethereum.listener.EthereumListener; 29 import org.ethereum.net.NodeHandler; 30 import org.ethereum.net.NodeManager; 31 import org.ethereum.net.client.PeerClient; 32 import org.ethereum.net.rlpx.Node; 33 import org.ethereum.net.server.Channel; 34 import org.ethereum.util.ByteUtil; 35 import org.slf4j.Logger; 36 import org.slf4j.LoggerFactory; 37  38 import java.math.BigInteger; 39 import java.time.Duration; 40 import java.time.Instant; 41 import java.util.*; 42 import java.util.concurrent.Executors; 43 import java.util.concurrent.ScheduledExecutorService; 44 import java.util.concurrent.TimeUnit; 45  46 import static java.lang.Math.min; 47 import static org.ethereum.util.BIUtil.isIn20PercentRange; 48  49 /** 50  * <p>Encapsulates logic which manages peers involved in blockchain sync</p> 51  * 52  * Holds connections, bans, disconnects and other peers logic<br> 53  * The pool is completely threadsafe<br> 54  * Implements {@link Iterable} and can be used in "foreach" loop<br> 55  * 56  * @author Mikhail Kalinin 57  * @since 10.08.2015 58  */ 59 public class SyncPool implements InternalService { 60  61  public static final Logger logger = LoggerFactory.getLogger("sync"); 62  63  private static final long WORKER_TIMEOUT = 3; // 3 seconds 64  65  private static final Duration CONNECTION_TIMEOUT = Duration.ofSeconds(30); 66  67  private final Map<NodeID, Channel> peers = new HashMap<>(); 68  private final List<Channel> activePeers = Collections.synchronizedList(new ArrayList<>()); 69  private final Map<String, Instant> pendingConnections = new HashMap<>(); 70  71  private BlockDifficulty lowerUsefulDifficulty = BlockDifficulty.ZERO; 72  73  private final EthereumListener ethereumListener; 74  private final Blockchain blockchain; 75  private final RskSystemProperties config; 76  private final NodeManager nodeManager; 77  private final NodeBlockProcessor nodeBlockProcessor; 78  private final PeerClientFactory peerClientFactory; 79  80  private ScheduledExecutorService syncPoolExecutor; 81  82  public SyncPool( 83  EthereumListener ethereumListener, 84  Blockchain blockchain, 85  RskSystemProperties config, 86  NodeManager nodeManager, 87  NodeBlockProcessor nodeBlockProcessor, 88  PeerClientFactory peerClientFactory) { 89  this.ethereumListener = ethereumListener; 90  this.blockchain = blockchain; 91  this.config = config; 92  this.nodeManager = nodeManager; 93  this.nodeBlockProcessor = nodeBlockProcessor; 94  this.peerClientFactory = peerClientFactory; 95  } 96  97  @Override 98  public void start() { 99  this.syncPoolExecutor = Executors.newSingleThreadScheduledExecutor(target -> new Thread(target, "syncPool")); 100  101  updateLowerUsefulDifficulty(); 102  103  syncPoolExecutor.scheduleWithFixedDelay( 104  () -> { 105  try { 106  if (config.getIsHeartBeatEnabled()) { 107  heartBeat(); 108  } 109  processConnections(); 110  updateLowerUsefulDifficulty(); 111  fillUp(); 112  prepareActive(); 113  } catch (Throwable t) { 114  logger.error("Unhandled exception", t); 115  } 116  }, WORKER_TIMEOUT, WORKER_TIMEOUT, TimeUnit.SECONDS 117  ); 118  119  if (config.waitForSync()) { 120  try { 121  while (nodeBlockProcessor.getBestBlockNumber() == 0 || nodeBlockProcessor.hasBetterBlockToSync()) { 122  Thread.sleep(10000); 123  } 124  } catch (InterruptedException e) { 125  logger.error("The SyncPool service couldn't be started", e); 126  Thread.currentThread().interrupt(); 127  } 128  } 129  } 130  131  @Override 132  public void stop() { 133  syncPoolExecutor.shutdown(); 134  } 135  136  public void add(Channel peer) { 137  138  if (!config.isSyncEnabled()) { 139  return; 140  } 141  142  String peerId = peer.getPeerId(); 143  logger.trace("Peer {}: adding", peerId); 144  145  synchronized (peers) { 146  peers.put(peer.getNodeId(), peer); 147  } 148  149  synchronized (pendingConnections) { 150  pendingConnections.remove(peer.getPeerId()); 151  } 152  153  ethereumListener.onPeerAddedToSyncPool(peer); 154  logger.info("Peer {}: added to pool", peerId); 155  } 156  157  public void remove(Channel peer) { 158  synchronized (peers) { 159  peers.values().remove(peer); 160  } 161  } 162  163  public void onDisconnect(Channel peer) { 164  165  if (peer.getNodeId() == null) { 166  return; 167  } 168  169  boolean existed; 170  171  synchronized (peers) { 172  existed = peers.values().remove(peer); 173  synchronized (activePeers) { 174  activePeers.remove(peer); 175  } 176  } 177  178  // do not count disconnects for nodeId 179  // if exact peer is not an active one 180  if (!existed) { 181  return; 182  } 183  184  logger.info("Peer {}: disconnected", peer.getPeerId()); 185  } 186  187  private void connect(Node node) { 188  if (logger.isTraceEnabled()) { 189  logger.trace( 190  "Peer {}: initiate connection", 191  node.getHexId() 192  ); 193  } 194  195  if (isInUse(node.getHexId())) { 196  if (logger.isTraceEnabled()) { 197  logger.trace( 198  "Peer {}: connection already initiated", 199  node.getHexId() 200  ); 201  } 202  203  return; 204  } 205  206  synchronized (pendingConnections) { 207  String ip = node.getHost(); 208  int port = node.getPort(); 209  String remoteId = ByteUtil.toHexString(node.getId().getID()); 210  logger.info("Connecting to: {}:{}", ip, port); 211  PeerClient peerClient = peerClientFactory.newInstance(); 212  peerClient.connectAsync(ip, port, remoteId); 213  pendingConnections.put(node.getHexId(), Instant.now()); 214  } 215  } 216  217  private Set<String> nodesInUse() { 218  Set<String> ids = new HashSet<>(); 219  220  synchronized (peers) { 221  for (Channel peer : peers.values()) { 222  ids.add(peer.getPeerId()); 223  } 224  } 225  226  synchronized (pendingConnections) { 227  ids.addAll(pendingConnections.keySet()); 228  } 229  230  return ids; 231  } 232  233  private boolean isInUse(String nodeId) { 234  return nodesInUse().contains(nodeId); 235  } 236  237  private void processConnections() { 238  synchronized (pendingConnections) { 239  Instant earliestAcceptableTime = Instant.now().minus(CONNECTION_TIMEOUT); 240  pendingConnections.values().removeIf(e -> e.isBefore(earliestAcceptableTime)); 241  } 242  } 243  244  private void fillUp() { 245  int lackSize = config.maxActivePeers() - peers.size(); 246  if(lackSize <= 0) { 247  return; 248  } 249  250  Set<String> nodesInUse = nodesInUse(); 251  252  List<NodeHandler> newNodes = nodeManager.getNodes(nodesInUse); 253  254  if (logger.isTraceEnabled()) { 255  logDiscoveredNodes(newNodes); 256  } 257  258  for(NodeHandler n : newNodes) { 259  connect(n.getNode()); 260  } 261  } 262  263  private void prepareActive() { 264  synchronized (peers) { 265  266  List<Channel> active = new ArrayList<>(peers.values()); 267  268  if (active.isEmpty()) { 269  return; 270  } 271  272  // filtering by 20% from top difficulty 273  active.sort(Comparator.comparing(Channel::getTotalDifficulty).reversed()); 274  275  BigInteger highestDifficulty = active.get(0).getTotalDifficulty(); 276  int thresholdIdx = min(config.syncPeerCount(), active.size()) - 1; 277  278  for (int i = thresholdIdx; i >= 0; i--) { 279  if (isIn20PercentRange(active.get(i).getTotalDifficulty(), highestDifficulty)) { 280  thresholdIdx = i; 281  break; 282  } 283  } 284  285  List<Channel> filtered = active.subList(0, thresholdIdx + 1); 286  287  // sorting by latency in asc order 288  filtered.sort(Comparator.comparingDouble(c -> c.getPeerStats().getAvgLatency())); 289  290  synchronized (activePeers) { 291  activePeers.clear(); 292  activePeers.addAll(filtered); 293  } 294  } 295  } 296  297  private void logDiscoveredNodes(List<NodeHandler> nodes) { 298  StringBuilder sb = new StringBuilder(); 299  300  for(NodeHandler n : nodes) { 301  sb.append(ByteUtil.toHexString(n.getNode().getId().getID())); 302  sb.append(", "); 303  } 304  305  if(sb.length() > 0) { 306  sb.delete(sb.length() - 2, sb.length()); 307  } 308  309  logger.trace( 310  "Node list obtained from discovery: {}", 311  nodes.isEmpty() ? "empty" : sb.toString() 312  ); 313  } 314  315  private void updateLowerUsefulDifficulty() { 316  BlockDifficulty td = blockchain.getTotalDifficulty(); 317  318  if (td.compareTo(lowerUsefulDifficulty) > 0) { 319  lowerUsefulDifficulty = td; 320  } 321  } 322  323  private void heartBeat() { 324  synchronized (peers) { 325  for (Channel peer : peers.values()) { 326  if (peer.getSyncStats().secondsSinceLastUpdate() > config.peerChannelReadTimeout()) { 327  logger.info("Peer {}: no response after {} seconds", peer.getPeerId(), config.peerChannelReadTimeout()); 328  peer.dropConnection(); 329  } 330  } 331  } 332  } 333  334  public interface PeerClientFactory { 335  PeerClient newInstance(); 336  } 337 }