/*
* 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.net.rlpx.discover;
import org.apache.commons.lang3.tuple.Pair;
import org.ethereum.config.SystemProperties;
import org.ethereum.crypto.ECKey;
import org.ethereum.db.PeerSource;
import org.ethereum.listener.EthereumListener;
import org.ethereum.net.rlpx.*;
import org.ethereum.net.rlpx.discover.table.NodeTable;
import org.ethereum.util.CollectionUtils;
import org.ethereum.util.Functional;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import static java.lang.Math.min;
/**
* The central class for Peer Discovery machinery.
*
* The NodeManager manages info on all the Nodes discovered by the peer discovery
* protocol, routes protocol messages to the corresponding NodeHandlers and
* supplies the info about discovered Nodes and their usage statistics
*
* Created by Anton Nashatyrev on 16.07.2015.
*/
@Component
public class NodeManager implements Functional.Consumer<DiscoveryEvent>{
static final org.slf4j.Logger logger = LoggerFactory.getLogger("discover");
private final boolean PERSIST;
private static final long LISTENER_REFRESH_RATE = 1000;
private static final long DB_COMMIT_RATE = 1 * 60 * 1000;
static final int MAX_NODES = 2000;
static final int NODES_TRIM_THRESHOLD = 3000;
PeerConnectionTester peerConnectionManager;
PeerSource peerSource;
EthereumListener ethereumListener;
SystemProperties config = SystemProperties.getDefault();
Functional.Consumer<DiscoveryEvent> messageSender;
NodeTable table;
private Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
final ECKey key;
final Node homeNode;
private List<Node> bootNodes;
// option to handle inbounds only from known peers (i.e. which were discovered by ourselves)
boolean inboundOnlyFromKnownNodes = false;
private boolean discoveryEnabled;
private Map<DiscoverListener, ListenerHandler> listeners = new IdentityHashMap<>();
private boolean inited = false;
private Timer logStatsTimer = new Timer();
private Timer nodeManagerTasksTimer = new Timer("NodeManagerTasks");;
private ScheduledExecutorService pongTimer;
@Autowired
public NodeManager(SystemProperties config, EthereumListener ethereumListener,
ApplicationContext ctx, PeerConnectionTester peerConnectionManager) {
this.config = config;
this.ethereumListener = ethereumListener;
this.peerConnectionManager = peerConnectionManager;
PERSIST = config.peerDiscoveryPersist();
if (PERSIST) peerSource = ctx.getBean(PeerSource.class);
discoveryEnabled = config.peerDiscovery();
key = config.getMyKey();
homeNode = new Node(config.nodeId(), config.externalIp(), config.listenPort());
table = new NodeTable(homeNode, config.isPublicHomeNode());
logStatsTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
logger.trace("Statistics:\n {}", dumpAllStatistics());
}
}, 1 * 1000, 60 * 1000);
this.pongTimer = Executors.newSingleThreadScheduledExecutor();
for (Node node : config.peerActive()) {
getNodeHandler(node).getNodeStatistics().setPredefined(true);
}
}
public ScheduledExecutorService getPongTimer() {
return pongTimer;
}
void setBootNodes(List<Node> bootNodes) {
this.bootNodes = bootNodes;
}
void channelActivated() {
// channel activated now can send messages
if (!inited) {
// no another init on a new channel activation
inited = true;
// this task is done asynchronously with some fixed rate
// to avoid any overhead in the NodeStatistics classes keeping them lightweight
// (which might be critical since they might be invoked from time critical sections)
nodeManagerTasksTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
processListeners();
}
}, LISTENER_REFRESH_RATE, LISTENER_REFRESH_RATE);
if (PERSIST) {
dbRead();
nodeManagerTasksTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
dbWrite();
}
}, DB_COMMIT_RATE, DB_COMMIT_RATE);
}
for (Node node : bootNodes) {
getNodeHandler(node);
}
}
}
private void dbRead() {
logger.info("Reading Node statistics from DB: " + peerSource.getNodes().size() + " nodes.");
for (Pair<Node, Integer> nodeElement : peerSource.getNodes()) {
getNodeHandler(nodeElement.getLeft()).getNodeStatistics().setPersistedReputation(nodeElement.getRight());
}
}
private void dbWrite() {
List<Pair<Node, Integer>> batch = new ArrayList<>();
synchronized (this) {
for (NodeHandler handler : nodeHandlerMap.values()) {
batch.add(Pair.of(handler.getNode(), handler.getNodeStatistics().getPersistedReputation()));
}
}
peerSource.clear();
for (Pair<Node, Integer> nodeElement : batch) {
peerSource.getNodes().add(nodeElement);
}
peerSource.getNodes().flush();
logger.info("Write Node statistics to DB: " + peerSource.getNodes().size() + " nodes.");
}
public void setMessageSender(Functional.Consumer<DiscoveryEvent> messageSender) {
this.messageSender = messageSender;
}
private String getKey(Node n) {
return getKey(new InetSocketAddress(n.getHost(), n.getPort()));
}
private String getKey(InetSocketAddress address) {
InetAddress addr = address.getAddress();
// addr == null if the hostname can't be resolved
return (addr == null ? address.getHostString() : addr.getHostAddress()) + ":" + address.getPort();
}
public synchronized NodeHandler getNodeHandler(Node n) {
String key = getKey(n);
NodeHandler ret = nodeHandlerMap.get(key);
if (ret == null) {
trimTable();
ret = new NodeHandler(n ,this);
nodeHandlerMap.put(key, ret);
logger.debug(" +++ New node: " + ret + " " + n);
if (!n.isDiscoveryNode() && !n.getHexId().equals(homeNode.getHexId())) {
ethereumListener.onNodeDiscovered(ret.getNode());
}
} else if (ret.getNode().isDiscoveryNode() && !n.isDiscoveryNode()) {
// we found discovery node with same host:port,
// replace node with correct nodeId
ret.node = n;
if (!n.getHexId().equals(homeNode.getHexId())) {
ethereumListener.onNodeDiscovered(ret.getNode());
}
logger.debug(" +++ Found real nodeId for discovery endpoint {}", n);
}
return ret;
}
private void trimTable() {
if (nodeHandlerMap.size() > NODES_TRIM_THRESHOLD) {
List<NodeHandler> sorted = new ArrayList<>(nodeHandlerMap.values());
// reverse sort by reputation
Collections.sort(sorted, new Comparator<NodeHandler>() {
@Override
public int compare(NodeHandler o1, NodeHandler o2) {
return o1.getNodeStatistics().getReputation() - o2.getNodeStatistics().getReputation();
}
});
for (NodeHandler handler : sorted) {
nodeHandlerMap.remove(getKey(handler.getNode()));
if (nodeHandlerMap.size() <= MAX_NODES) break;
}
}
}
boolean hasNodeHandler(Node n) {
return nodeHandlerMap.containsKey(getKey(n));
}
public NodeTable getTable() {
return table;
}
public NodeStatistics getNodeStatistics(Node n) {
return getNodeHandler(n).getNodeStatistics();
}
@Override
public void accept(DiscoveryEvent discoveryEvent) {
handleInbound(discoveryEvent);
}
public void handleInbound(DiscoveryEvent discoveryEvent) {
Message m = discoveryEvent.getMessage();
InetSocketAddress sender = discoveryEvent.getAddress();
Node n = new Node(m.getNodeId(), sender.getHostString(), sender.getPort());
if (inboundOnlyFromKnownNodes && !hasNodeHandler(n)) {
logger.debug("=/=> (" + sender + "): inbound packet from unknown peer rejected due to config option.");
return;
}
NodeHandler nodeHandler = getNodeHandler(n);
logger.trace("===> ({}) {} [{}] {}", sender, m.getClass().getSimpleName(), nodeHandler, m);
byte type = m.getType()[0];
switch (type) {
case 1:
nodeHandler.handlePing((PingMessage) m);
break;
case 2:
nodeHandler.handlePong((PongMessage) m);
break;
case 3:
nodeHandler.handleFindNode((FindNodeMessage) m);
break;
case 4:
nodeHandler.handleNeighbours((NeighborsMessage) m);
break;
}
}
public void sendOutbound(DiscoveryEvent discoveryEvent) {
if (discoveryEnabled && messageSender != null) {
logger.trace(" <===({}) {} [{}] {}", discoveryEvent.getAddress(),
discoveryEvent.getMessage().getClass().getSimpleName(), this, discoveryEvent.getMessage());
messageSender.accept(discoveryEvent);
}
}
public void stateChanged(NodeHandler nodeHandler, NodeHandler.State oldState, NodeHandler.State newState) {
if (discoveryEnabled && peerConnectionManager != null) { // peerConnectionManager can be null if component not inited yet
peerConnectionManager.nodeStatusChanged(nodeHandler);
}
}
public synchronized List<NodeHandler> getNodes(int minReputation) {
List<NodeHandler> ret = new ArrayList<>();
for (NodeHandler nodeHandler : nodeHandlerMap.values()) {
if (nodeHandler.getNodeStatistics().getReputation() >= minReputation) {
ret.add(nodeHandler);
}
}
return ret;
}
/**
* Returns limited list of nodes matching {@code predicate} criteria<br>
* The nodes are sorted then by their totalDifficulties
*
* @param predicate only those nodes which are satisfied to its condition are included in results
* @param limit max size of returning list
*
* @return list of nodes matching criteria
*/
public List<NodeHandler> getNodes(
Functional.Predicate<NodeHandler> predicate,
int limit ) {
ArrayList<NodeHandler> filtered = new ArrayList<>();
synchronized (this) {
for (NodeHandler handler : nodeHandlerMap.values()) {
if (predicate.test(handler)) {
filtered.add(handler);
}
}
}
Collections.sort(filtered, new Comparator<NodeHandler>() {
@Override
public int compare(NodeHandler o1, NodeHandler o2) {
return o2.getNodeStatistics().getEthTotalDifficulty().compareTo(
o1.getNodeStatistics().getEthTotalDifficulty());
}
});
return CollectionUtils.truncate(filtered, limit);
}
private synchronized void processListeners() {
for (ListenerHandler handler : listeners.values()) {
try {
handler.checkAll();
} catch (Exception e) {
logger.error("Exception processing listener: " + handler, e);
}
}
}
/**
* Add a listener which is notified when the node statistics starts or stops meeting
* the criteria specified by [filter] param.
*/
public synchronized void addDiscoverListener(DiscoverListener listener, Functional.Predicate<NodeStatistics> filter) {
listeners.put(listener, new ListenerHandler(listener, filter));
}
public synchronized void removeDiscoverListener(DiscoverListener listener) {
listeners.remove(listener);
}
public synchronized String dumpAllStatistics() {
List<NodeHandler> l = new ArrayList<>(nodeHandlerMap.values());
Collections.sort(l, new Comparator<NodeHandler>() {
public int compare(NodeHandler o1, NodeHandler o2) {
return -(o1.getNodeStatistics().getReputation() - o2.getNodeStatistics().getReputation());
}
});
StringBuilder sb = new StringBuilder();
int zeroReputCount = 0;
for (NodeHandler nodeHandler : l) {
if (nodeHandler.getNodeStatistics().getReputation() > 0) {
sb.append(nodeHandler).append("\t").append(nodeHandler.getNodeStatistics()).append("\n");
} else {
zeroReputCount++;
}
}
sb.append("0 reputation: ").append(zeroReputCount).append(" nodes.\n");
return sb.toString();
}
/**
* @return home node if config defines it as public, otherwise null
*/
Node getPublicHomeNode() {
if (config.isPublicHomeNode()) {
return homeNode;
}
return null;
}
public void close() {
peerConnectionManager.close();
try {
nodeManagerTasksTimer.cancel();
if (PERSIST) {
try {
dbWrite();
} catch (Throwable e) { // IllegalAccessError is expected
// NOTE: logback stops context right after shutdown initiated. It is problematic to see log output
// System out could help
logger.warn("Problem during NodeManager persist in close: " + e.getMessage());
}
}
} catch (Exception e) {
logger.warn("Problems canceling nodeManagerTasksTimer", e);
}
try {
logger.info("Cancelling pongTimer");
pongTimer.shutdownNow();
} catch (Exception e) {
logger.warn("Problems cancelling pongTimer", e);
}
try {
logStatsTimer.cancel();
} catch (Exception e) {
logger.warn("Problems canceling logStatsTimer", e);
}
}
private class ListenerHandler {
Map<NodeHandler, Object> discoveredNodes = new IdentityHashMap<>();
DiscoverListener listener;
Functional.Predicate<NodeStatistics> filter;
ListenerHandler(DiscoverListener listener, Functional.Predicate<NodeStatistics> filter) {
this.listener = listener;
this.filter = filter;
}
void checkAll() {
for (NodeHandler handler : nodeHandlerMap.values()) {
boolean has = discoveredNodes.containsKey(handler);
boolean test = filter.test(handler.getNodeStatistics());
if (!has && test) {
listener.nodeAppeared(handler);
discoveredNodes.put(handler, null);
} else if (has && !test) {
listener.nodeDisappeared(handler);
discoveredNodes.remove(handler);
}
}
}
}
}