/*
* 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 com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.commons.codec.binary.Hex;
import org.ethereum.config.SystemProperties;
import org.ethereum.net.client.PeerClient;
import org.ethereum.net.rlpx.Node;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.*;
/**
* Makes test RLPx connection to the peers to acquire statistics
*
* Created by Anton Nashatyrev on 17.07.2015.
*/
@Component
public class PeerConnectionTester {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger("discover");
private int ConnectThreads;
private long ReconnectPeriod;
private long ReconnectMaxPeers;
@Autowired
private PeerClient peerClient;
private SystemProperties config = SystemProperties.getDefault();
// NodeHandler instance should be unique per Node instance
private Map<NodeHandler, ?> connectedCandidates = Collections.synchronizedMap(new IdentityHashMap());
// executor with Queue which picks up the Node with the best reputation
private ExecutorService peerConnectionPool;
private Timer reconnectTimer = new Timer("DiscoveryReconnectTimer");
private int reconnectPeersCount = 0;
private class ConnectTask implements Runnable {
NodeHandler nodeHandler;
public ConnectTask(NodeHandler nodeHandler) {
this.nodeHandler = nodeHandler;
}
@Override
public void run() {
try {
if (nodeHandler != null) {
nodeHandler.getNodeStatistics().rlpxConnectionAttempts.add();
logger.debug("Trying node connection: " + nodeHandler);
Node node = nodeHandler.getNode();
peerClient.connect(node.getHost(), node.getPort(),
Hex.encodeHexString(node.getId()), true);
logger.debug("Terminated node connection: " + nodeHandler);
nodeHandler.getNodeStatistics().disconnected();
if (!nodeHandler.getNodeStatistics().getEthTotalDifficulty().equals(BigInteger.ZERO) &&
ReconnectPeriod > 0 && (reconnectPeersCount < ReconnectMaxPeers || ReconnectMaxPeers == -1)) {
// trying to keep good peers information up-to-date
reconnectPeersCount++;
reconnectTimer.schedule(new TimerTask() {
@Override
public void run() {
logger.debug("Trying the node again: " + nodeHandler);
peerConnectionPool.execute(new ConnectTask(nodeHandler));
reconnectPeersCount--;
}
}, ReconnectPeriod);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
connectedCandidates.remove(nodeHandler);
}
}
}
@Autowired
public PeerConnectionTester(final SystemProperties config) {
this.config = config;
ConnectThreads = config.peerDiscoveryWorkers();
ReconnectPeriod = config.peerDiscoveryTouchPeriod() * 1000;
ReconnectMaxPeers = config.peerDiscoveryTouchMaxNodes();
peerConnectionPool = new ThreadPoolExecutor(ConnectThreads,
ConnectThreads, 0L, TimeUnit.SECONDS,
new MutablePriorityQueue<Runnable, ConnectTask>(new Comparator<ConnectTask>() {
@Override
public int compare(ConnectTask h1, ConnectTask h2) {
return h2.nodeHandler.getNodeStatistics().getReputation() -
h1.nodeHandler.getNodeStatistics().getReputation();
}
}), new ThreadFactoryBuilder().setDaemon(true).setNameFormat("discovery-tester-%d").build());
}
public void close() {
logger.info("Closing PeerConnectionTester...");
try {
peerConnectionPool.shutdownNow();
} catch (Exception e) {
logger.warn("Problems closing PeerConnectionTester", e);
}
try {
reconnectTimer.cancel();
} catch (Exception e) {
logger.warn("Problems cancelling reconnectTimer", e);
}
}
public void nodeStatusChanged(final NodeHandler nodeHandler) {
if (peerConnectionPool.isShutdown()) return;
if (connectedCandidates.size() < NodeManager.MAX_NODES
&& !connectedCandidates.containsKey(nodeHandler)
&& !nodeHandler.getNode().isDiscoveryNode()) {
logger.debug("Submitting node for RLPx connection : " + nodeHandler);
connectedCandidates.put(nodeHandler, null);
peerConnectionPool.execute(new ConnectTask(nodeHandler));
}
}
/**
* The same as PriorityBlockQueue but with assumption that elements are mutable
* and priority changes after enqueueing, thus the list is sorted by priority
* each time the head queue element is requested.
* The class has poor synchronization since the prioritization might be approximate
* though the implementation should be inheritedly thread-safe
*/
public static class MutablePriorityQueue<T, C extends T> extends LinkedBlockingQueue<T> {
Comparator<C> comparator;
public MutablePriorityQueue(Comparator<C> comparator) {
this.comparator = comparator;
}
@Override
public synchronized T take() throws InterruptedException {
if (isEmpty()) {
return super.take();
} else {
T ret = Collections.min(this, (Comparator<? super T>) comparator);
remove(ret);
return ret;
}
}
@Override
public synchronized T poll(long timeout, TimeUnit unit) throws InterruptedException {
if (isEmpty()) {
return super.poll(timeout, unit);
} else {
T ret = Collections.min(this, (Comparator<? super T>) comparator);
remove(ret);
return ret;
}
}
@Override
public synchronized T poll() {
if (isEmpty()) {
return super.poll();
} else {
T ret = Collections.min(this, (Comparator<? super T>) comparator);
remove(ret);
return ret;
}
}
@Override
public synchronized T peek() {
if (isEmpty()) {
return super.peek();
} else {
T ret = Collections.min(this, (Comparator<? super T>) comparator);
return ret;
}
}
}
}