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 }