/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.sync;
import org.ethereum.config.SystemProperties;
import org.ethereum.core.Blockchain;
import org.ethereum.listener.EthereumListener;
import org.ethereum.net.rlpx.Node;
import org.ethereum.net.rlpx.discover.NodeHandler;
import org.ethereum.net.rlpx.discover.NodeManager;
import org.ethereum.net.server.Channel;
import org.ethereum.net.server.ChannelManager;
import org.ethereum.util.Functional;
import org.ethereum.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static java.lang.Math.min;
import static org.ethereum.util.BIUtil.isIn20PercentRange;
/**
* <p>Encapsulates logic which manages peers involved in blockchain sync</p>
*
* Holds connections, bans, disconnects and other peers logic<br>
* The pool is completely threadsafe<br>
* Implements {@link Iterable} and can be used in "foreach" loop<br>
* Used by {@link SyncManager}
*
* @author Mikhail Kalinin
* @since 10.08.2015
*/
@Component
public class SyncPool {
public static final Logger logger = LoggerFactory.getLogger("sync");
private static final long WORKER_TIMEOUT = 3; // 3 seconds
private final List<Channel> activePeers = Collections.synchronizedList(new ArrayList<Channel>());
private BigInteger lowerUsefulDifficulty = BigInteger.ZERO;
@Autowired
private EthereumListener ethereumListener;
@Autowired
private NodeManager nodeManager;
private ChannelManager channelManager;
private Blockchain blockchain;
private SystemProperties config;
private ScheduledExecutorService poolLoopExecutor = Executors.newSingleThreadScheduledExecutor();
private Functional.Predicate<NodeHandler> nodesSelector;
private ScheduledExecutorService logExecutor = Executors.newSingleThreadScheduledExecutor();
@Autowired
public SyncPool(final SystemProperties config, final Blockchain blockchain) {
this.config = config;
this.blockchain = blockchain;
}
public void init(final ChannelManager channelManager) {
if (this.channelManager != null) return; // inited already
this.channelManager = channelManager;
updateLowerUsefulDifficulty();
poolLoopExecutor.scheduleWithFixedDelay(
new Runnable() {
@Override
public void run() {
try {
heartBeat();
updateLowerUsefulDifficulty();
fillUp();
prepareActive();
cleanupActive();
} catch (Throwable t) {
logger.error("Unhandled exception", t);
}
}
}, WORKER_TIMEOUT, WORKER_TIMEOUT, TimeUnit.SECONDS
);
logExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
logActivePeers();
logger.info("\n");
} catch (Throwable t) {
t.printStackTrace();
logger.error("Exception in log worker", t);
}
}
}, 30, 30, TimeUnit.SECONDS);
}
public void setNodesSelector(Functional.Predicate<NodeHandler> nodesSelector) {
this.nodesSelector = nodesSelector;
}
public void close() {
try {
poolLoopExecutor.shutdownNow();
logExecutor.shutdownNow();
} catch (Exception e) {
logger.warn("Problems shutting down executor", e);
}
}
@Nullable
public synchronized Channel getAnyIdle() {
ArrayList<Channel> channels = new ArrayList<>(activePeers);
Collections.shuffle(channels);
for (Channel peer : channels) {
if (peer.isIdle())
return peer;
}
return null;
}
@Nullable
public synchronized Channel getBestIdle() {
for (Channel peer : activePeers) {
if (peer.isIdle())
return peer;
}
return null;
}
@Nullable
public synchronized Channel getNotLastIdle() {
ArrayList<Channel> channels = new ArrayList<>(activePeers);
Collections.shuffle(channels);
Channel candidate = null;
for (Channel peer : channels) {
if (peer.isIdle()) {
if (candidate == null) {
candidate = peer;
} else {
return candidate;
}
}
}
return null;
}
public synchronized List<Channel> getAllIdle() {
List<Channel> ret = new ArrayList<>();
for (Channel peer : activePeers) {
if (peer.isIdle())
ret.add(peer);
}
return ret;
}
public synchronized List<Channel> getActivePeers() {
return new ArrayList<>(activePeers);
}
public synchronized int getActivePeersCount() {
return activePeers.size();
}
@Nullable
public synchronized Channel getByNodeId(byte[] nodeId) {
return channelManager.getActivePeer(nodeId);
}
public synchronized void onDisconnect(Channel peer) {
if (activePeers.remove(peer)) {
logger.info("Peer {}: disconnected", peer.getPeerIdShort());
}
}
public synchronized Set<String> nodesInUse() {
Set<String> ids = new HashSet<>();
for (Channel peer : channelManager.getActivePeers()) {
ids.add(peer.getPeerId());
}
return ids;
}
synchronized void logActivePeers() {
if (logger.isInfoEnabled()) {
StringBuilder sb = new StringBuilder("Peer stats:\n");
sb.append("Active peers\n");
sb.append("============\n");
Set<Node> activeSet = new HashSet<>();
for (Channel peer : new ArrayList<>(activePeers)) {
sb.append(peer.logSyncStats()).append('\n');
activeSet.add(peer.getNode());
}
sb.append("Other connected peers\n");
sb.append("============\n");
for (Channel peer : new ArrayList<>(channelManager.getActivePeers())) {
if (!activeSet.contains(peer.getNode())) {
sb.append(peer.logSyncStats()).append('\n');
}
}
logger.info(sb.toString());
}
}
class NodeSelector implements Functional.Predicate<NodeHandler> {
BigInteger lowerDifficulty;
Set<String> nodesInUse;
public NodeSelector(BigInteger lowerDifficulty) {
this.lowerDifficulty = lowerDifficulty;
}
public NodeSelector(BigInteger lowerDifficulty, Set<String> nodesInUse) {
this.lowerDifficulty = lowerDifficulty;
this.nodesInUse = nodesInUse;
}
@Override
public boolean test(NodeHandler handler) {
if (nodesInUse != null && nodesInUse.contains(handler.getNode().getHexId())) {
return false;
}
if (handler.getNodeStatistics().isPredefined()) return true;
if (nodesSelector != null && !nodesSelector.test(handler)) return false;
if (lowerDifficulty.compareTo(BigInteger.ZERO) > 0 &&
handler.getNodeStatistics().getEthTotalDifficulty() == null) {
return false;
}
if (handler.getNodeStatistics().getReputation() < 100) return false;
return handler.getNodeStatistics().getEthTotalDifficulty().compareTo(lowerDifficulty) >= 0;
}
}
private void fillUp() {
int lackSize = config.maxActivePeers() - channelManager.getActivePeers().size();
if(lackSize <= 0) return;
final Set<String> nodesInUse = nodesInUse();
nodesInUse.add(Hex.toHexString(config.nodeId())); // exclude home node
List<NodeHandler> newNodes;
newNodes = nodeManager.getNodes(new NodeSelector(lowerUsefulDifficulty, nodesInUse), lackSize);
if (lackSize > 0 && newNodes.isEmpty()) {
newNodes = nodeManager.getNodes(new NodeSelector(BigInteger.ZERO, nodesInUse), lackSize);
}
if (logger.isTraceEnabled()) {
logDiscoveredNodes(newNodes);
}
for(NodeHandler n : newNodes) {
channelManager.connect(n.getNode());
}
}
private synchronized void prepareActive() {
List<Channel> managerActive = new ArrayList<>(channelManager.getActivePeers());
// Filtering out with nodeSelector because server-connected nodes were not tested
NodeSelector nodeSelector = new NodeSelector(BigInteger.ZERO);
List<Channel> active = new ArrayList<>();
for (Channel channel : managerActive) {
if (nodeSelector.test(nodeManager.getNodeHandler(channel.getNode()))) {
active.add(channel);
}
}
if (active.isEmpty()) return;
// filtering by 20% from top difficulty
Collections.sort(active, new Comparator<Channel>() {
@Override
public int compare(Channel c1, Channel c2) {
return c2.getTotalDifficulty().compareTo(c1.getTotalDifficulty());
}
});
BigInteger highestDifficulty = active.get(0).getTotalDifficulty();
int thresholdIdx = min(config.syncPeerCount(), active.size()) - 1;
for (int i = thresholdIdx; i >= 0; i--) {
if (isIn20PercentRange(active.get(i).getTotalDifficulty(), highestDifficulty)) {
thresholdIdx = i;
break;
}
}
List<Channel> filtered = active.subList(0, thresholdIdx + 1);
// sorting by latency in asc order
Collections.sort(filtered, new Comparator<Channel>() {
@Override
public int compare(Channel c1, Channel c2) {
return Double.valueOf(c1.getPeerStats().getAvgLatency()).compareTo(c2.getPeerStats().getAvgLatency());
}
});
for (Channel channel : filtered) {
if (!activePeers.contains(channel)) {
ethereumListener.onPeerAddedToSyncPool(channel);
}
}
activePeers.clear();
activePeers.addAll(filtered);
}
private synchronized void cleanupActive() {
Iterator<Channel> iterator = activePeers.iterator();
while (iterator.hasNext()) {
Channel next = iterator.next();
if (next.isDisconnected()) {
logger.info("Removing peer " + next + " from active due to disconnect.");
iterator.remove();
}
}
}
private void logDiscoveredNodes(List<NodeHandler> nodes) {
StringBuilder sb = new StringBuilder();
for(NodeHandler n : nodes) {
sb.append(Utils.getNodeIdShort(Hex.toHexString(n.getNode().getId())));
sb.append(", ");
}
if(sb.length() > 0) {
sb.delete(sb.length() - 2, sb.length());
}
logger.trace(
"Node list obtained from discovery: {}",
nodes.size() > 0 ? sb.toString() : "empty"
);
}
private void updateLowerUsefulDifficulty() {
BigInteger td = blockchain.getTotalDifficulty();
if (td.compareTo(lowerUsefulDifficulty) > 0) {
lowerUsefulDifficulty = td;
}
}
public ChannelManager getChannelManager() {
return channelManager;
}
private void heartBeat() {
// for (Channel peer : channelManager.getActivePeers()) {
// if (!peer.isIdle() && peer.getSyncStats().secondsSinceLastUpdate() > config.peerChannelReadTimeout()) {
// logger.info("Peer {}: no response after {} seconds", peer.getPeerIdShort(), config.peerChannelReadTimeout());
// peer.dropConnection();
// }
// }
}
}