forked from Qortal/qortal
New network threading model
Instead of 3 threads per peer: 1. peer main thread 2. peer's unsolicited messages processor 3. peer pinger We now use a Jetty-style Execute-Produce-Consume server threading model. For 60 connected peers, we no longer have 180 threads but typically only the usual ~6 threads. Also in this commit: * merging peers locking changed from lock() to tryLock() * PROOF handshake maximum time difference increased from 2000ms to 5000ms * Peers still handshaking after 60s are considered stuck and hence disconnected * We now use NIO SocketChannels instead of raw sockets
This commit is contained in:
parent
67c245bb9d
commit
0d85a60c54
@ -625,7 +625,7 @@ public class Controller extends Thread {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// We want to update atomically so use lock
|
// We want to update atomically so use lock
|
||||||
ReentrantLock peerLock = connectedPeer.getPeerLock();
|
ReentrantLock peerLock = connectedPeer.getPeerDataLock();
|
||||||
peerLock.lock();
|
peerLock.lock();
|
||||||
try {
|
try {
|
||||||
connectedPeer.setLastHeight(heightV2Message.getHeight());
|
connectedPeer.setLastHeight(heightV2Message.getHeight());
|
||||||
|
@ -86,7 +86,7 @@ public class Synchronizer {
|
|||||||
int peerHeight;
|
int peerHeight;
|
||||||
byte[] peersLastBlockSignature;
|
byte[] peersLastBlockSignature;
|
||||||
|
|
||||||
ReentrantLock peerLock = peer.getPeerLock();
|
ReentrantLock peerLock = peer.getPeerDataLock();
|
||||||
peerLock.lockInterruptibly();
|
peerLock.lockInterruptibly();
|
||||||
try {
|
try {
|
||||||
peerHeight = peer.getLastHeight();
|
peerHeight = peer.getLastHeight();
|
||||||
|
@ -173,7 +173,8 @@ public enum Handshake {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(Handshake.class);
|
private static final Logger LOGGER = LogManager.getLogger(Handshake.class);
|
||||||
|
|
||||||
private static final long MAX_TIMESTAMP_DELTA = 2000; // ms
|
/** Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */
|
||||||
|
private static final long MAX_TIMESTAMP_DELTA = 5000; // ms
|
||||||
|
|
||||||
public final MessageType expectedMessageType;
|
public final MessageType expectedMessageType;
|
||||||
|
|
||||||
|
@ -4,20 +4,27 @@ import java.io.IOException;
|
|||||||
import java.net.Inet6Address;
|
import java.net.Inet6Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.StandardSocketOptions;
|
||||||
import java.net.Socket;
|
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.nio.channels.CancelledKeyException;
|
||||||
|
import java.nio.channels.ClosedChannelException;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.nio.channels.ServerSocketChannel;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@ -26,6 +33,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qora.block.Block;
|
||||||
import org.qora.controller.Controller;
|
import org.qora.controller.Controller;
|
||||||
import org.qora.data.block.BlockData;
|
import org.qora.data.block.BlockData;
|
||||||
import org.qora.data.network.PeerData;
|
import org.qora.data.network.PeerData;
|
||||||
@ -65,6 +73,11 @@ public class Network extends Thread {
|
|||||||
private static final long OLD_PEER_ATTEMPTED_PERIOD = 24 * 60 * 60 * 1000; // ms
|
private static final long OLD_PEER_ATTEMPTED_PERIOD = 24 * 60 * 60 * 1000; // ms
|
||||||
/** Maximum time since last successful connection before a peer is potentially considered "old", in milliseconds. */
|
/** Maximum time since last successful connection before a peer is potentially considered "old", in milliseconds. */
|
||||||
private static final long OLD_PEER_CONNECTION_PERIOD = 7 * 24 * 60 * 60 * 1000; // ms
|
private static final long OLD_PEER_CONNECTION_PERIOD = 7 * 24 * 60 * 60 * 1000; // ms
|
||||||
|
/** Maximum time allowed for handshake to complete, in milliseconds. */
|
||||||
|
private static final long HANDSHAKE_TIMEOUT = 60 * 1000; // ms
|
||||||
|
|
||||||
|
/** Maximum message size (bytes). Needs to be at least maximum block size + MAGIC + message type, etc. */
|
||||||
|
/* package */ static final int MAXIMUM_MESSAGE_SIZE = 4 + 1 + 4 + Block.MAX_BLOCK_BYTES;
|
||||||
|
|
||||||
private static final byte[] MAINNET_MESSAGE_MAGIC = new byte[] { 0x12, 0x34, 0x56, 0x78 };
|
private static final byte[] MAINNET_MESSAGE_MAGIC = new byte[] { 0x12, 0x34, 0x56, 0x78 };
|
||||||
private static final byte[] TESTNET_MESSAGE_MAGIC = new byte[] { 0x78, 0x56, 0x34, 0x12 };
|
private static final byte[] TESTNET_MESSAGE_MAGIC = new byte[] { 0x78, 0x56, 0x34, 0x12 };
|
||||||
@ -87,11 +100,18 @@ public class Network extends Thread {
|
|||||||
private volatile boolean isStopping = false;
|
private volatile boolean isStopping = false;
|
||||||
private List<Peer> connectedPeers;
|
private List<Peer> connectedPeers;
|
||||||
private List<PeerAddress> selfPeers;
|
private List<PeerAddress> selfPeers;
|
||||||
private ServerSocket listenSocket;
|
|
||||||
|
private ExecutorService networkingExecutor;
|
||||||
|
private static Selector channelSelector;
|
||||||
|
private static ServerSocketChannel serverChannel;
|
||||||
|
private static AtomicBoolean isIterationInProgress = new AtomicBoolean(false);
|
||||||
|
private static Iterator<SelectionKey> channelIterator = null;
|
||||||
|
private static volatile boolean hasThreadPending = false;
|
||||||
|
private static AtomicInteger activeThreads = new AtomicInteger(0);
|
||||||
|
private static AtomicBoolean generalTaskLock = new AtomicBoolean(false);
|
||||||
|
|
||||||
private int minOutboundPeers;
|
private int minOutboundPeers;
|
||||||
private int maxPeers;
|
private int maxPeers;
|
||||||
private ExecutorService peerExecutor;
|
|
||||||
private ExecutorService mergePeersExecutor;
|
|
||||||
private ExecutorService broadcastExecutor;
|
private ExecutorService broadcastExecutor;
|
||||||
/** Timestamp (ms) for next general info broadcast to all connected peers. Based on <tt>System.currentTimeMillis()</tt>. */
|
/** Timestamp (ms) for next general info broadcast to all connected peers. Based on <tt>System.currentTimeMillis()</tt>. */
|
||||||
private long nextBroadcast;
|
private long nextBroadcast;
|
||||||
@ -108,11 +128,14 @@ public class Network extends Thread {
|
|||||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, listenPort);
|
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, listenPort);
|
||||||
|
|
||||||
|
channelSelector = Selector.open();
|
||||||
|
|
||||||
// Set up listen socket
|
// Set up listen socket
|
||||||
listenSocket = new ServerSocket();
|
serverChannel = ServerSocketChannel.open();
|
||||||
listenSocket.setReuseAddress(true);
|
serverChannel.configureBlocking(false);
|
||||||
listenSocket.setSoTimeout(1); // accept() calls block for at most 1ms
|
serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
|
||||||
listenSocket.bind(endpoint, LISTEN_BACKLOG);
|
serverChannel.bind(endpoint, LISTEN_BACKLOG);
|
||||||
|
serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
LOGGER.error("Can't bind listen socket to address " + Settings.getInstance().getBindAddress());
|
LOGGER.error("Can't bind listen socket to address " + Settings.getInstance().getBindAddress());
|
||||||
throw new RuntimeException("Can't bind listen socket to address");
|
throw new RuntimeException("Can't bind listen socket to address");
|
||||||
@ -130,13 +153,14 @@ public class Network extends Thread {
|
|||||||
minOutboundPeers = Settings.getInstance().getMinOutboundPeers();
|
minOutboundPeers = Settings.getInstance().getMinOutboundPeers();
|
||||||
maxPeers = Settings.getInstance().getMaxPeers();
|
maxPeers = Settings.getInstance().getMaxPeers();
|
||||||
|
|
||||||
peerExecutor = Executors.newCachedThreadPool();
|
|
||||||
|
|
||||||
broadcastExecutor = Executors.newCachedThreadPool();
|
broadcastExecutor = Executors.newCachedThreadPool();
|
||||||
nextBroadcast = System.currentTimeMillis();
|
nextBroadcast = System.currentTimeMillis();
|
||||||
|
|
||||||
mergePeersLock = new ReentrantLock();
|
mergePeersLock = new ReentrantLock();
|
||||||
mergePeersExecutor = Executors.newCachedThreadPool();
|
|
||||||
|
// Start up first networking thread
|
||||||
|
networkingExecutor = Executors.newCachedThreadPool();
|
||||||
|
networkingExecutor.execute(new NetworkProcessor());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters / setters
|
// Getters / setters
|
||||||
@ -245,89 +269,200 @@ public class Network extends Thread {
|
|||||||
|
|
||||||
// Main thread
|
// Main thread
|
||||||
|
|
||||||
|
class NetworkProcessor implements Runnable {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Network");
|
Thread.currentThread().setName("Network");
|
||||||
|
|
||||||
|
activeThreads.incrementAndGet();
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s, hasThreadPending: %s, activeThreads now: %d", Thread.currentThread().getId(), (hasThreadPending ? "yes" : "no"), activeThreads.get()));
|
||||||
|
hasThreadPending = false;
|
||||||
|
|
||||||
// Maintain long-term connections to various peers' API applications
|
// Maintain long-term connections to various peers' API applications
|
||||||
try {
|
try {
|
||||||
while (!isStopping) {
|
while (!isStopping) {
|
||||||
acceptConnections();
|
if (!isIterationInProgress.compareAndSet(false, true)) {
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s NOT producing (some other thread is) - exiting", Thread.currentThread().getId()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
pruneOldPeers();
|
LOGGER.trace(() -> String.format("Network thread %s is producing...", Thread.currentThread().getId()));
|
||||||
|
|
||||||
|
final SelectionKey nextSelectionKey;
|
||||||
|
try {
|
||||||
|
// anything to do?
|
||||||
|
if (channelIterator == null) {
|
||||||
|
channelSelector.select(1000L);
|
||||||
|
|
||||||
|
if (Thread.currentThread().isInterrupted())
|
||||||
|
break;
|
||||||
|
|
||||||
|
channelIterator = channelSelector.selectedKeys().iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelIterator.hasNext()) {
|
||||||
|
nextSelectionKey = channelIterator.next();
|
||||||
|
channelIterator.remove();
|
||||||
|
} else {
|
||||||
|
nextSelectionKey = null;
|
||||||
|
channelIterator = null; // Nothing to do so reset iterator to cause new select
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s produced %s, iterator now %s",
|
||||||
|
Thread.currentThread().getId(),
|
||||||
|
(nextSelectionKey == null ? "null" : nextSelectionKey.channel()),
|
||||||
|
(channelIterator == null ? "null" : channelIterator.toString())));
|
||||||
|
|
||||||
|
// Spawn another thread in case we need help
|
||||||
|
if (!hasThreadPending) {
|
||||||
|
hasThreadPending = true;
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s spawning", Thread.currentThread().getId()));
|
||||||
|
networkingExecutor.execute(this);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s done producing", Thread.currentThread().getId()));
|
||||||
|
isIterationInProgress.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// process
|
||||||
|
if (nextSelectionKey == null) {
|
||||||
|
// no pending tasks, but we're last remaining thread so maybe connect a new peer or do a broadcast
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s has no pending tasks", Thread.currentThread().getId()));
|
||||||
|
|
||||||
|
if (!generalTaskLock.compareAndSet(false, true))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s performing general tasks", Thread.currentThread().getId()));
|
||||||
|
|
||||||
|
pingPeers();
|
||||||
|
|
||||||
|
prunePeers();
|
||||||
|
|
||||||
createConnection();
|
createConnection();
|
||||||
|
|
||||||
if (System.currentTimeMillis() >= this.nextBroadcast) {
|
if (System.currentTimeMillis() >= nextBroadcast) {
|
||||||
this.nextBroadcast = System.currentTimeMillis() + BROADCAST_INTERVAL;
|
nextBroadcast = System.currentTimeMillis() + BROADCAST_INTERVAL;
|
||||||
|
|
||||||
// Controller can decide what to broadcast
|
// Controller can decide what to broadcast
|
||||||
Controller.getInstance().doNetworkBroadcast();
|
Controller.getInstance().doNetworkBroadcast();
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s finished general tasks", Thread.currentThread().getId()));
|
||||||
|
generalTaskLock.set(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s has pending channel: %s, with ops %d",
|
||||||
|
Thread.currentThread().getId(), nextSelectionKey.channel(), nextSelectionKey.readyOps()));
|
||||||
|
|
||||||
// Sleep for a while
|
// process pending channel task
|
||||||
Thread.sleep(1000);
|
if (nextSelectionKey.isReadable()) {
|
||||||
|
connectionRead((SocketChannel) nextSelectionKey.channel());
|
||||||
|
} else if (nextSelectionKey.isAcceptable()) {
|
||||||
|
acceptConnection((ServerSocketChannel) nextSelectionKey.channel());
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s processed channel: %s", Thread.currentThread().getId(), nextSelectionKey.channel()));
|
||||||
|
} catch (CancelledKeyException e) {
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s encountered cancelled channel: %s", Thread.currentThread().getId(), nextSelectionKey.channel()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// Fall-through to shutdown
|
// Fall-through to shutdown
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
LOGGER.warn("Repository issue while running network", e);
|
LOGGER.warn("Repository issue while running network", e);
|
||||||
// Fall-through to shutdown
|
// Fall-through to shutdown
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("resource")
|
|
||||||
private void acceptConnections() throws InterruptedException {
|
|
||||||
Socket socket;
|
|
||||||
|
|
||||||
do {
|
|
||||||
try {
|
|
||||||
socket = this.listenSocket.accept();
|
|
||||||
} catch (SocketTimeoutException e) {
|
|
||||||
// No connections to accept
|
|
||||||
return;
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Something went wrong or listen socket was closed due to shutdown
|
// Fall-through to shutdown
|
||||||
|
} finally {
|
||||||
|
activeThreads.decrementAndGet();
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s ending, activeThreads now: %d", Thread.currentThread().getId(), activeThreads.get()));
|
||||||
|
Thread.currentThread().setName("Network (dormant)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptConnection(ServerSocketChannel serverSocketChannel) throws InterruptedException {
|
||||||
|
SocketChannel socketChannel;
|
||||||
|
|
||||||
|
try {
|
||||||
|
socketChannel = serverSocketChannel.accept();
|
||||||
|
} catch (IOException e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Peer newPeer = null;
|
// No connection actually accepted?
|
||||||
|
if (socketChannel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Peer newPeer;
|
||||||
|
|
||||||
|
try {
|
||||||
synchronized (this.connectedPeers) {
|
synchronized (this.connectedPeers) {
|
||||||
if (connectedPeers.size() >= maxPeers) {
|
if (connectedPeers.size() >= maxPeers) {
|
||||||
// We have enough peers
|
// We have enough peers
|
||||||
LOGGER.trace(String.format("Connection discarded from peer %s", socket.getRemoteSocketAddress()));
|
LOGGER.trace(String.format("Connection discarded from peer %s", socketChannel.getRemoteAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
LOGGER.debug(String.format("Connection accepted from peer %s", socketChannel.getRemoteAddress()));
|
||||||
socket.close();
|
|
||||||
|
newPeer = new Peer(socketChannel);
|
||||||
|
this.connectedPeers.add(newPeer);
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Not important
|
if (socketChannel.isOpen())
|
||||||
|
try {
|
||||||
|
socketChannel.close();
|
||||||
|
} catch (IOException ce) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.debug(String.format("Connection accepted from peer %s", socket.getRemoteSocketAddress()));
|
|
||||||
newPeer = new Peer(socket);
|
|
||||||
this.connectedPeers.add(newPeer);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
peerExecutor.execute(newPeer);
|
socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
|
||||||
} catch (RejectedExecutionException e) {
|
socketChannel.configureBlocking(false);
|
||||||
// Can't execute - probably because we're shutting down, so ignore
|
socketChannel.register(channelSelector, SelectionKey.OP_READ);
|
||||||
}
|
} catch (IOException e) {
|
||||||
} while (true);
|
// Remove from connected peers
|
||||||
|
synchronized (this.connectedPeers) {
|
||||||
|
this.connectedPeers.remove(newPeer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pruneOldPeers() throws InterruptedException, DataException {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onPeerReady(newPeer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pingPeers() {
|
||||||
|
for (Peer peer : this.getConnectedPeers())
|
||||||
|
peer.pingCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prunePeers() throws InterruptedException, DataException {
|
||||||
|
final long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Disconnect peers that are stuck during handshake
|
||||||
|
List<Peer> handshakePeers = this.getConnectedPeers();
|
||||||
|
|
||||||
|
// Disregard peers that have completed handshake or only connected recently
|
||||||
|
handshakePeers.removeIf(peer -> peer.getHandshakeStatus() == Handshake.COMPLETED || peer.getConnectionTimestamp() == null || peer.getConnectionTimestamp() > now - HANDSHAKE_TIMEOUT);
|
||||||
|
|
||||||
|
for (Peer peer : handshakePeers)
|
||||||
|
peer.disconnect("handshake timeout");
|
||||||
|
|
||||||
|
// Prune 'old' peers from repository...
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
// Fetch all known peers
|
// Fetch all known peers
|
||||||
List<PeerData> peers = repository.getNetworkRepository().getAllPeers();
|
List<PeerData> peers = repository.getNetworkRepository().getAllPeers();
|
||||||
|
|
||||||
// "Old" peers:
|
// 'Old' peers:
|
||||||
// we have attempted to connect within the last day
|
// we have attempted to connect within the last day
|
||||||
// we last managed to connect over a week ago
|
// we last managed to connect over a week ago
|
||||||
final long now = System.currentTimeMillis();
|
|
||||||
Predicate<PeerData> isNotOldPeer = peerData -> {
|
Predicate<PeerData> isNotOldPeer = peerData -> {
|
||||||
if (peerData.getLastAttempted() == null || peerData.getLastAttempted() < now - OLD_PEER_ATTEMPTED_PERIOD)
|
if (peerData.getLastAttempted() == null || peerData.getLastAttempted() < now - OLD_PEER_ATTEMPTED_PERIOD)
|
||||||
return true;
|
return true;
|
||||||
@ -338,6 +473,7 @@ public class Network extends Thread {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Disregard peers that are NOT 'old'
|
||||||
peers.removeIf(isNotOldPeer);
|
peers.removeIf(isNotOldPeer);
|
||||||
|
|
||||||
// Don't consider already connected peers (simple address match)
|
// Don't consider already connected peers (simple address match)
|
||||||
@ -426,7 +562,8 @@ public class Network extends Thread {
|
|||||||
repository.saveChanges();
|
repository.saveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPeer.connect())
|
SocketChannel socketChannel = newPeer.connect();
|
||||||
|
if (socketChannel == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.isInterrupted())
|
if (this.isInterrupted())
|
||||||
@ -437,12 +574,41 @@ public class Network extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
peerExecutor.execute(newPeer);
|
socketChannel.register(channelSelector, SelectionKey.OP_READ);
|
||||||
} catch (RejectedExecutionException e) {
|
} catch (ClosedChannelException e) {
|
||||||
// Can't execute - probably because we're shutting down, so ignore
|
// If channel has somehow already closed then remove from connectedPeers
|
||||||
|
synchronized (this.connectedPeers) {
|
||||||
|
this.connectedPeers.remove(newPeer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.onPeerReady(newPeer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectionRead(SocketChannel socketChannel) {
|
||||||
|
Peer peer = getPeerFromChannel(socketChannel);
|
||||||
|
if (peer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
peer.readMessages();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.trace(() -> String.format("Network thread %s encountered I/O error: %s", Thread.currentThread().getId(), e.getMessage()), e);
|
||||||
|
peer.disconnect("I/O error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Peer getPeerFromChannel(SocketChannel socketChannel) {
|
||||||
|
synchronized (this.connectedPeers) {
|
||||||
|
for (Peer peer : this.connectedPeers)
|
||||||
|
if (peer.getSocketChannel() == socketChannel)
|
||||||
|
return peer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Peer callbacks
|
// Peer callbacks
|
||||||
|
|
||||||
/** Called when Peer's thread has setup and is ready to process messages */
|
/** Called when Peer's thread has setup and is ready to process messages */
|
||||||
@ -817,23 +983,9 @@ public class Network extends Thread {
|
|||||||
// Network-wide calls
|
// Network-wide calls
|
||||||
|
|
||||||
private void mergePeers(String addedBy, List<PeerAddress> peerAddresses) {
|
private void mergePeers(String addedBy, List<PeerAddress> peerAddresses) {
|
||||||
// This can block (due to lock) so fire off in separate thread
|
|
||||||
class PeersMerger implements Runnable {
|
|
||||||
private String addedBy;
|
|
||||||
private List<PeerAddress> peerAddresses;
|
|
||||||
|
|
||||||
public PeersMerger(String addedBy, List<PeerAddress> peerAddresses) {
|
|
||||||
this.addedBy = addedBy;
|
|
||||||
this.peerAddresses = peerAddresses;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Thread.currentThread().setName(String.format("Merging peers from %s", this.addedBy));
|
|
||||||
|
|
||||||
// Serialize using lock to prevent repository deadlocks
|
// Serialize using lock to prevent repository deadlocks
|
||||||
try {
|
if (!mergePeersLock.tryLock())
|
||||||
mergePeersLock.lockInterruptibly();
|
return;
|
||||||
|
|
||||||
final long addedWhen = System.currentTimeMillis();
|
final long addedWhen = System.currentTimeMillis();
|
||||||
|
|
||||||
@ -848,6 +1000,8 @@ public class Network extends Thread {
|
|||||||
|
|
||||||
peerAddresses.removeIf(isKnownAddress);
|
peerAddresses.removeIf(isKnownAddress);
|
||||||
|
|
||||||
|
repository.discardChanges();
|
||||||
|
|
||||||
// Save the rest into database
|
// Save the rest into database
|
||||||
for (PeerAddress peerAddress : peerAddresses) {
|
for (PeerAddress peerAddress : peerAddresses) {
|
||||||
PeerData peerData = new PeerData(peerAddress, addedWhen, addedBy);
|
PeerData peerData = new PeerData(peerAddress, addedWhen, addedBy);
|
||||||
@ -862,19 +1016,6 @@ public class Network extends Thread {
|
|||||||
} finally {
|
} finally {
|
||||||
mergePeersLock.unlock();
|
mergePeersLock.unlock();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e1) {
|
|
||||||
// We're exiting anyway...
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.currentThread().setName("Merging peers (dormant)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
mergePeersExecutor.execute(new PeersMerger(addedBy, peerAddresses));
|
|
||||||
} catch (RejectedExecutionException e) {
|
|
||||||
// Can't execute - probably because we're shutting down, so ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void broadcast(Function<Peer, Message> peerMessageBuilder) {
|
public void broadcast(Function<Peer, Message> peerMessageBuilder) {
|
||||||
@ -894,11 +1035,11 @@ public class Network extends Thread {
|
|||||||
Random random = new Random();
|
Random random = new Random();
|
||||||
|
|
||||||
for (Peer peer : targetPeers) {
|
for (Peer peer : targetPeers) {
|
||||||
// Very short sleep to reduce strain, improve multithreading and catch interrupts
|
// Very short sleep to reduce strain, improve multi-threading and catch interrupts
|
||||||
try {
|
try {
|
||||||
Thread.sleep(random.nextInt(20) + 20);
|
Thread.sleep(random.nextInt(20) + 20);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Message message = peerMessageBuilder.apply(peer);
|
Message message = peerMessageBuilder.apply(peer);
|
||||||
@ -909,6 +1050,8 @@ public class Network extends Thread {
|
|||||||
if (!peer.sendMessage(message))
|
if (!peer.sendMessage(message))
|
||||||
peer.disconnect("failed to broadcast message");
|
peer.disconnect("failed to broadcast message");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Thread.currentThread().setName("Network Broadcast (dormant)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -925,28 +1068,20 @@ public class Network extends Thread {
|
|||||||
this.isStopping = true;
|
this.isStopping = true;
|
||||||
|
|
||||||
// Close listen socket to prevent more incoming connections
|
// Close listen socket to prevent more incoming connections
|
||||||
if (!this.listenSocket.isClosed())
|
if (serverChannel.isOpen())
|
||||||
try {
|
try {
|
||||||
this.listenSocket.close();
|
serverChannel.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Not important
|
// Not important
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop our run() thread
|
// Stop processing threads
|
||||||
this.interrupt();
|
this.networkingExecutor.shutdownNow();
|
||||||
try {
|
try {
|
||||||
this.join();
|
if (!this.networkingExecutor.awaitTermination(5000, TimeUnit.MILLISECONDS))
|
||||||
|
LOGGER.debug("Network threads failed to terminate");
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
LOGGER.debug("Interrupted while waiting for networking thread to terminate");
|
LOGGER.debug("Interrupted while waiting for networking threads to terminate");
|
||||||
}
|
|
||||||
|
|
||||||
// Give up merging peer lists
|
|
||||||
this.mergePeersExecutor.shutdownNow();
|
|
||||||
try {
|
|
||||||
if (!this.mergePeersExecutor.awaitTermination(1000, TimeUnit.MILLISECONDS))
|
|
||||||
LOGGER.debug("Peer-list merging threads failed to terminate");
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LOGGER.debug("Interrupted while waiting for peer-list merging threads failed to terminate");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop broadcasts
|
// Stop broadcasts
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
package org.qora.network;
|
package org.qora.network;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
|
||||||
import java.io.EOFException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.net.StandardSocketOptions;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -16,9 +15,6 @@ import java.util.Map;
|
|||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
@ -37,7 +33,7 @@ import com.google.common.net.HostAndPort;
|
|||||||
import com.google.common.net.InetAddresses;
|
import com.google.common.net.InetAddresses;
|
||||||
|
|
||||||
// For managing one peer
|
// For managing one peer
|
||||||
public class Peer extends Thread {
|
public class Peer {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(Peer.class);
|
private static final Logger LOGGER = LogManager.getLogger(Peer.class);
|
||||||
|
|
||||||
@ -50,31 +46,19 @@ public class Peer extends Thread {
|
|||||||
/**
|
/**
|
||||||
* Interval between PING messages to a peer. (ms)
|
* Interval between PING messages to a peer. (ms)
|
||||||
* <p>
|
* <p>
|
||||||
* Just under every 30s is usually ideal to keep NAT mappings refreshed,<br>
|
* Just under every 30s is usually ideal to keep NAT mappings refreshed.
|
||||||
* BUT must be lower than {@link Peer#SOCKET_TIMEOUT}!
|
|
||||||
*/
|
*/
|
||||||
private static final int PING_INTERVAL = 8000; // ms
|
private static final int PING_INTERVAL = 8000; // ms
|
||||||
|
|
||||||
/** Maximum time a socket <tt>read()</tt> will block before closing connection due to timeout. (ms) */
|
|
||||||
private static final int SOCKET_TIMEOUT = 10000; // ms
|
|
||||||
|
|
||||||
private static final int UNSOLICITED_MESSAGE_QUEUE_CAPACITY = 10;
|
|
||||||
|
|
||||||
private volatile boolean isStopping = false;
|
private volatile boolean isStopping = false;
|
||||||
|
|
||||||
private Socket socket = null;
|
private SocketChannel socketChannel = null;
|
||||||
private InetSocketAddress resolvedAddress = null;
|
private InetSocketAddress resolvedAddress = null;
|
||||||
/** True if remote address is loopback/link-local/site-local, false otherwise. */
|
/** True if remote address is loopback/link-local/site-local, false otherwise. */
|
||||||
private boolean isLocal;
|
private boolean isLocal;
|
||||||
private OutputStream out;
|
private ByteBuffer byteBuffer;
|
||||||
|
|
||||||
private Map<Integer, BlockingQueue<Message>> replyQueues;
|
private Map<Integer, BlockingQueue<Message>> replyQueues;
|
||||||
|
|
||||||
private BlockingQueue<Message> unsolicitedQueue;
|
|
||||||
private ExecutorService messageExecutor;
|
|
||||||
|
|
||||||
private ScheduledExecutorService pingExecutor;
|
|
||||||
|
|
||||||
/** True if we created connection to peer, false if we accepted incoming connection from peer. */
|
/** True if we created connection to peer, false if we accepted incoming connection from peer. */
|
||||||
private final boolean isOutbound;
|
private final boolean isOutbound;
|
||||||
/** Numeric protocol version, typically 1 or 2. */
|
/** Numeric protocol version, typically 1 or 2. */
|
||||||
@ -88,20 +72,29 @@ public class Peer extends Thread {
|
|||||||
private byte[] verificationCodeExpected;
|
private byte[] verificationCodeExpected;
|
||||||
|
|
||||||
private PeerData peerData = null;
|
private PeerData peerData = null;
|
||||||
private final ReentrantLock peerLock = new ReentrantLock();
|
private final ReentrantLock peerDataLock = new ReentrantLock();
|
||||||
|
|
||||||
/** Timestamp of when socket was accepted, or connected. */
|
/** Timestamp of when socket was accepted, or connected. */
|
||||||
private Long connectionTimestamp = null;
|
private Long connectionTimestamp = null;
|
||||||
|
|
||||||
/** Version info as reported by peer. */
|
/** Version info as reported by peer. */
|
||||||
private VersionMessage versionMessage = null;
|
private VersionMessage versionMessage = null;
|
||||||
|
|
||||||
/** Last PING message round-trip time (ms). */
|
/** Last PING message round-trip time (ms). */
|
||||||
private Long lastPing = null;
|
private Long lastPing = null;
|
||||||
|
/** When last PING message was sent, or null if pings not started yet. */
|
||||||
|
private Long lastPingSent;
|
||||||
|
private final ReentrantLock pingLock = new ReentrantLock();
|
||||||
|
|
||||||
/** Latest block height as reported by peer. */
|
/** Latest block height as reported by peer. */
|
||||||
private Integer lastHeight;
|
private Integer lastHeight;
|
||||||
|
|
||||||
/** Latest block signature as reported by peer. */
|
/** Latest block signature as reported by peer. */
|
||||||
private byte[] lastBlockSignature;
|
private byte[] lastBlockSignature;
|
||||||
|
|
||||||
/** Latest block timestamp as reported by peer. */
|
/** Latest block timestamp as reported by peer. */
|
||||||
private Long lastBlockTimestamp;
|
private Long lastBlockTimestamp;
|
||||||
|
|
||||||
/** Latest block generator public key as reported by peer. */
|
/** Latest block generator public key as reported by peer. */
|
||||||
private byte[] lastBlockGenerator;
|
private byte[] lastBlockGenerator;
|
||||||
|
|
||||||
@ -114,19 +107,24 @@ public class Peer extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Construct Peer using existing, connected socket */
|
/** Construct Peer using existing, connected socket */
|
||||||
public Peer(Socket socket) {
|
public Peer(SocketChannel socketChannel) throws IOException {
|
||||||
this.isOutbound = false;
|
this.isOutbound = false;
|
||||||
this.socket = socket;
|
this.socketChannel = socketChannel;
|
||||||
|
sharedSetup();
|
||||||
|
|
||||||
this.resolvedAddress = ((InetSocketAddress) socket.getRemoteSocketAddress());
|
this.resolvedAddress = ((InetSocketAddress) socketChannel.socket().getRemoteSocketAddress());
|
||||||
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
|
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
|
||||||
|
|
||||||
PeerAddress peerAddress = PeerAddress.fromSocket(socket);
|
PeerAddress peerAddress = PeerAddress.fromSocket(socketChannel.socket());
|
||||||
this.peerData = new PeerData(peerAddress);
|
this.peerData = new PeerData(peerAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters / setters
|
// Getters / setters
|
||||||
|
|
||||||
|
public SocketChannel getSocketChannel() {
|
||||||
|
return this.socketChannel;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isStopping() {
|
public boolean isStopping() {
|
||||||
return this.isStopping;
|
return this.isStopping;
|
||||||
}
|
}
|
||||||
@ -247,14 +245,13 @@ public class Peer extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the lock used for synchronizing access to peer info. */
|
/** Returns the lock used for synchronizing access to peer info. */
|
||||||
public ReentrantLock getPeerLock() {
|
public ReentrantLock getPeerDataLock() {
|
||||||
return this.peerLock;
|
return this.peerDataLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Easier, and nicer output, than peer.getRemoteSocketAddress()
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
// Easier, and nicer output, than peer.getRemoteSocketAddress()
|
||||||
return this.peerData.getAddress().toString();
|
return this.peerData.getAddress().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,153 +265,123 @@ public class Peer extends Thread {
|
|||||||
new SecureRandom().nextBytes(verificationCodeExpected);
|
new SecureRandom().nextBytes(verificationCodeExpected);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageProcessor implements Runnable {
|
private void sharedSetup() throws IOException {
|
||||||
private Peer peer;
|
|
||||||
private BlockingQueue<Message> blockingQueue;
|
|
||||||
|
|
||||||
public MessageProcessor(Peer peer, BlockingQueue<Message> blockingQueue) {
|
|
||||||
this.peer = peer;
|
|
||||||
this.blockingQueue = blockingQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Thread.currentThread().setName("Peer UMP " + this.peer);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
Message message = blockingQueue.poll(1000L, TimeUnit.MILLISECONDS);
|
|
||||||
if (message != null)
|
|
||||||
Network.getInstance().onMessage(peer, message);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// Shutdown
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setup() throws IOException {
|
|
||||||
this.socket.setSoTimeout(SOCKET_TIMEOUT);
|
|
||||||
this.out = this.socket.getOutputStream();
|
|
||||||
this.connectionTimestamp = System.currentTimeMillis();
|
this.connectionTimestamp = System.currentTimeMillis();
|
||||||
|
this.socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
|
||||||
|
this.socketChannel.configureBlocking(false);
|
||||||
|
this.byteBuffer = ByteBuffer.allocate(Network.MAXIMUM_MESSAGE_SIZE);
|
||||||
this.replyQueues = Collections.synchronizedMap(new HashMap<Integer, BlockingQueue<Message>>());
|
this.replyQueues = Collections.synchronizedMap(new HashMap<Integer, BlockingQueue<Message>>());
|
||||||
|
|
||||||
this.unsolicitedQueue = new ArrayBlockingQueue<>(UNSOLICITED_MESSAGE_QUEUE_CAPACITY);
|
|
||||||
this.messageExecutor = Executors.newSingleThreadExecutor();
|
|
||||||
this.messageExecutor.execute(new MessageProcessor(this, this.unsolicitedQueue));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean connect() {
|
public SocketChannel connect() {
|
||||||
LOGGER.trace(String.format("Connecting to peer %s", this));
|
LOGGER.trace(String.format("Connecting to peer %s", this));
|
||||||
this.socket = new Socket();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.resolvedAddress = this.peerData.getAddress().toSocketAddress();
|
this.resolvedAddress = this.peerData.getAddress().toSocketAddress();
|
||||||
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
|
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
|
||||||
|
|
||||||
this.socket.connect(resolvedAddress, CONNECT_TIMEOUT);
|
this.socketChannel = SocketChannel.open();
|
||||||
|
this.socketChannel.socket().connect(resolvedAddress, CONNECT_TIMEOUT);
|
||||||
|
|
||||||
LOGGER.debug(String.format("Connected to peer %s", this));
|
LOGGER.debug(String.format("Connected to peer %s", this));
|
||||||
|
sharedSetup();
|
||||||
|
return socketChannel;
|
||||||
} catch (SocketTimeoutException e) {
|
} catch (SocketTimeoutException e) {
|
||||||
LOGGER.trace(String.format("Connection timed out to peer %s", this));
|
LOGGER.trace(String.format("Connection timed out to peer %s", this));
|
||||||
return false;
|
return null;
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
LOGGER.trace(String.format("Connection failed to unresolved peer %s", this));
|
LOGGER.trace(String.format("Connection failed to unresolved peer %s", this));
|
||||||
return false;
|
return null;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.trace(String.format("Connection failed to peer %s", this));
|
LOGGER.trace(String.format("Connection failed to peer %s", this));
|
||||||
return false;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main thread
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Thread.currentThread().setName("Peer " + this);
|
|
||||||
|
|
||||||
try (DataInputStream in = new DataInputStream(socket.getInputStream())) {
|
|
||||||
setup();
|
|
||||||
|
|
||||||
Network.getInstance().onPeerReady(this);
|
|
||||||
|
|
||||||
while (!isStopping) {
|
|
||||||
// Wait (up to INACTIVITY_TIMEOUT) for, and parse, incoming message
|
|
||||||
Message message = Message.fromStream(in);
|
|
||||||
if (message == null) {
|
|
||||||
this.disconnect("null message");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.trace(() -> String.format("Received %s message with ID %d from peer %s", message.getType().name(), message.getId(), this));
|
|
||||||
|
|
||||||
// Find potential blocking queue for this id (expect null if id is -1)
|
|
||||||
BlockingQueue<Message> queue = this.replyQueues.get(message.getId());
|
|
||||||
if (queue != null) {
|
|
||||||
// Adding message to queue will unblock thread waiting for response
|
|
||||||
this.replyQueues.get(message.getId()).add(message);
|
|
||||||
} else {
|
|
||||||
// Nothing waiting for this message (unsolicited) - queue up for network
|
|
||||||
|
|
||||||
// Queue full?
|
|
||||||
if (unsolicitedQueue.remainingCapacity() == 0) {
|
|
||||||
LOGGER.debug(String.format("No room for %s message with ID %s from peer %s", message.getType().name(), message.getId(), this));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsolicitedQueue.add(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (MessageException e) {
|
|
||||||
LOGGER.debug(String.format("%s, from peer %s", e.getMessage(), this));
|
|
||||||
this.disconnect(e.getMessage());
|
|
||||||
} catch (SocketTimeoutException e) {
|
|
||||||
this.disconnect("timeout");
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (isStopping) {
|
|
||||||
// If isStopping is true then our shutdown() has already been called, so no need to call it again
|
|
||||||
LOGGER.debug(String.format("Peer %s stopping...", this));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// More informative logging
|
|
||||||
if (e instanceof EOFException) {
|
|
||||||
this.disconnect("EOF");
|
|
||||||
} else if (e.getMessage().contains("onnection reset")) { // Can't import/rely on sun.net.ConnectionResetException
|
|
||||||
this.disconnect("Connection reset");
|
|
||||||
} else {
|
|
||||||
this.disconnect("I/O error");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Thread.currentThread().setName("disconnected peer");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to send Message to peer
|
* Attempt to read Message from peer.
|
||||||
|
*
|
||||||
|
* @return message, or null if no message or there was a problem
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void readMessages() throws IOException {
|
||||||
|
while(true) {
|
||||||
|
Message message;
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
if (!this.socketChannel.isOpen() || this.socketChannel.socket().isClosed())
|
||||||
|
break;
|
||||||
|
|
||||||
|
int bytesRead = this.socketChannel.read(this.byteBuffer);
|
||||||
|
if (bytesRead == -1) {
|
||||||
|
this.disconnect("EOF");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.trace(() -> String.format("Receiving message from peer %s", this));
|
||||||
|
|
||||||
|
// Can we build a message from buffer now?
|
||||||
|
try {
|
||||||
|
message = Message.fromByteBuffer(this.byteBuffer);
|
||||||
|
} catch (MessageException e) {
|
||||||
|
LOGGER.debug(String.format("%s, from peer %s", e.getMessage(), this));
|
||||||
|
this.disconnect(e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOGGER.trace(() -> String.format("Received %s message with ID %d from peer %s", message.getType().name(), message.getId(), this));
|
||||||
|
|
||||||
|
BlockingQueue<Message> queue = this.replyQueues.get(message.getId());
|
||||||
|
if (queue != null) {
|
||||||
|
// Adding message to queue will unblock thread waiting for response
|
||||||
|
this.replyQueues.get(message.getId()).add(message);
|
||||||
|
// Consumed elsewhere
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No thread waiting for message so pass up to network layer
|
||||||
|
Network.getInstance().onMessage(this, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to send Message to peer.
|
||||||
*
|
*
|
||||||
* @param message
|
* @param message
|
||||||
* @return <code>true</code> if message successfully sent; <code>false</code> otherwise
|
* @return <code>true</code> if message successfully sent; <code>false</code> otherwise
|
||||||
*/
|
*/
|
||||||
public boolean sendMessage(Message message) {
|
public boolean sendMessage(Message message) {
|
||||||
if (this.socket.isClosed())
|
if (!this.socketChannel.isOpen())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Send message
|
// Send message
|
||||||
LOGGER.trace(() -> String.format("Sending %s message with ID %d to peer %s", message.getType().name(), message.getId(), this));
|
LOGGER.trace(() -> String.format("Sending %s message with ID %d to peer %s", message.getType().name(), message.getId(), this));
|
||||||
|
|
||||||
synchronized (this.out) {
|
ByteBuffer outputBuffer = ByteBuffer.wrap(message.toBytes());
|
||||||
this.out.write(message.toBytes());
|
|
||||||
this.out.flush();
|
synchronized (this.socketChannel) {
|
||||||
|
while (outputBuffer.hasRemaining()) {
|
||||||
|
int bytesWritten = this.socketChannel.write(outputBuffer);
|
||||||
|
|
||||||
|
if (bytesWritten == 0)
|
||||||
|
// Underlying socket's internal buffer probably full,
|
||||||
|
// so wait a short while for bytes to actually be transmitted over the wire
|
||||||
|
Thread.sleep(1L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (MessageException e) {
|
} catch (MessageException e) {
|
||||||
LOGGER.warn(String.format("Failed to send %s message with ID %d to peer %s: %s", message.getType().name(), message.getId(), this, e.getMessage()));
|
LOGGER.warn(String.format("Failed to send %s message with ID %d to peer %s: %s", message.getType().name(), message.getId(), this, e.getMessage()));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Send failure
|
// Send failure
|
||||||
return false;
|
return false;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Likely shutdown scenario - so exit
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sent OK
|
// Sent OK
|
||||||
@ -461,39 +428,45 @@ public class Peer extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void startPings() {
|
public void startPings() {
|
||||||
class Pinger implements Runnable {
|
// Replacing initial null value allows pingCheck() to start sending pings.
|
||||||
private Peer peer;
|
LOGGER.trace(() -> String.format("Enabling pings for peer %s", this));
|
||||||
|
this.lastPingSent = 0L; //System.currentTimeMillis();
|
||||||
public Pinger(Peer peer) {
|
|
||||||
this.peer = peer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/* package */ void pingCheck() {
|
||||||
public void run() {
|
LOGGER.trace(() -> String.format("Ping check for peer %s", this));
|
||||||
Thread.currentThread().setName("Pinger " + this.peer);
|
|
||||||
|
|
||||||
PingMessage pingMessage = new PingMessage();
|
if (!this.pingLock.tryLock())
|
||||||
|
return; // Some other thread is already checking ping status for this peer
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final long before = System.currentTimeMillis();
|
// Pings not enabled yet?
|
||||||
Message message = peer.getResponse(pingMessage);
|
if (this.lastPingSent == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
final long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Time to send another ping?
|
||||||
|
if (now < this.lastPingSent + PING_INTERVAL)
|
||||||
|
return; // Not yet
|
||||||
|
|
||||||
|
this.lastPingSent = now;
|
||||||
|
PingMessage pingMessage = new PingMessage();
|
||||||
|
Message message = this.getResponse(pingMessage);
|
||||||
final long after = System.currentTimeMillis();
|
final long after = System.currentTimeMillis();
|
||||||
|
|
||||||
if (message == null || message.getType() != MessageType.PING)
|
if (message == null || message.getType() != MessageType.PING) {
|
||||||
peer.disconnect("no ping received");
|
this.disconnect("no ping received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
peer.setLastPing(after - before);
|
this.setLastPing(after - now);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// Shutdown
|
// Shutdown situation
|
||||||
|
} finally {
|
||||||
|
this.pingLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Random random = new Random();
|
|
||||||
long initialDelay = random.nextInt(PING_INTERVAL);
|
|
||||||
this.pingExecutor = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
this.pingExecutor.scheduleWithFixedDelay(new Pinger(this), initialDelay, PING_INTERVAL, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void disconnect(String reason) {
|
public void disconnect(String reason) {
|
||||||
LOGGER.debug(String.format("Disconnecting peer %s: %s", this, reason));
|
LOGGER.debug(String.format("Disconnecting peer %s: %s", this, reason));
|
||||||
@ -505,46 +478,14 @@ public class Peer extends Thread {
|
|||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
LOGGER.debug(() -> String.format("Shutting down peer %s", this));
|
LOGGER.debug(() -> String.format("Shutting down peer %s", this));
|
||||||
this.isStopping = true;
|
|
||||||
|
|
||||||
// Shut down pinger
|
if (this.socketChannel.isOpen()) {
|
||||||
if (this.pingExecutor != null) {
|
|
||||||
this.pingExecutor.shutdownNow();
|
|
||||||
try {
|
try {
|
||||||
if (!this.pingExecutor.awaitTermination(1000, TimeUnit.MILLISECONDS))
|
this.socketChannel.close();
|
||||||
LOGGER.debug(String.format("Pinger for peer %s failed to terminate", this));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LOGGER.debug(String.format("Interrupted while terminating pinger for peer %s", this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shut down unsolicited message processor
|
|
||||||
if (this.messageExecutor != null) {
|
|
||||||
this.messageExecutor.shutdownNow();
|
|
||||||
try {
|
|
||||||
if (!this.messageExecutor.awaitTermination(5000, TimeUnit.MILLISECONDS))
|
|
||||||
LOGGER.debug(String.format("Message processor for peer %s failed to terminate", this));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LOGGER.debug(String.format("Interrupted while terminating message processor for peer %s", this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.debug(() -> String.format("Interrupting peer %s", this));
|
|
||||||
this.interrupt();
|
|
||||||
|
|
||||||
// Close socket, which should trigger run() to exit
|
|
||||||
if (!this.socket.isClosed()) {
|
|
||||||
try {
|
|
||||||
this.socket.close();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
LOGGER.debug(String.format("IOException while trying to close peer %s", this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
this.join();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LOGGER.debug(String.format("Interrupted while waiting for peer %s to shutdown", this));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
|
@ -11,11 +11,9 @@ import static java.util.Arrays.stream;
|
|||||||
import static java.util.stream.Collectors.toMap;
|
import static java.util.stream.Collectors.toMap;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.DataInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
import java.nio.BufferUnderflowException;
|
import java.nio.BufferUnderflowException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -104,12 +102,12 @@ public abstract class Message {
|
|||||||
return map.get(value);
|
return map.get(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Message fromBytes(int id, byte[] data) throws MessageException {
|
public Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||||
if (this.fromByteBuffer == null)
|
if (this.fromByteBuffer == null)
|
||||||
throw new MessageException("Unsupported message type [" + value + "] during conversion from bytes");
|
throw new MessageException("Unsupported message type [" + value + "] during conversion from bytes");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return (Message) this.fromByteBuffer.invoke(null, id, data == null ? null : ByteBuffer.wrap(data));
|
return (Message) this.fromByteBuffer.invoke(null, id, byteBuffer);
|
||||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||||
if (e.getCause() instanceof BufferUnderflowException)
|
if (e.getCause() instanceof BufferUnderflowException)
|
||||||
throw new MessageException("Byte data too short for " + name() + " message");
|
throw new MessageException("Byte data too short for " + name() + " message");
|
||||||
@ -147,47 +145,65 @@ public abstract class Message {
|
|||||||
return this.type;
|
return this.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Message fromStream(DataInputStream in) throws MessageException, IOException {
|
/**
|
||||||
|
* Attempt to read a message from byte buffer.
|
||||||
|
*
|
||||||
|
* @param byteBuffer
|
||||||
|
* @return null if no complete message can be read
|
||||||
|
* @throws MessageException
|
||||||
|
*/
|
||||||
|
public static Message fromByteBuffer(ByteBuffer byteBuffer) throws MessageException {
|
||||||
try {
|
try {
|
||||||
|
byteBuffer.flip();
|
||||||
|
|
||||||
|
ByteBuffer readBuffer = byteBuffer.asReadOnlyBuffer();
|
||||||
|
|
||||||
// Read only enough bytes to cover Message "magic" preamble
|
// Read only enough bytes to cover Message "magic" preamble
|
||||||
byte[] messageMagic = new byte[MAGIC_LENGTH];
|
byte[] messageMagic = new byte[MAGIC_LENGTH];
|
||||||
in.readFully(messageMagic);
|
readBuffer.get(messageMagic);
|
||||||
|
|
||||||
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic()))
|
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic()))
|
||||||
// Didn't receive correct Message "magic"
|
// Didn't receive correct Message "magic"
|
||||||
throw new MessageException("Received incorrect message 'magic'");
|
throw new MessageException("Received incorrect message 'magic'");
|
||||||
|
|
||||||
int typeValue = in.readInt();
|
// Find supporting object
|
||||||
|
int typeValue = readBuffer.getInt();
|
||||||
MessageType messageType = MessageType.valueOf(typeValue);
|
MessageType messageType = MessageType.valueOf(typeValue);
|
||||||
if (messageType == null)
|
if (messageType == null)
|
||||||
// Unrecognised message type
|
// Unrecognised message type
|
||||||
throw new MessageException(String.format("Received unknown message type [%d]", typeValue));
|
throw new MessageException(String.format("Received unknown message type [%d]", typeValue));
|
||||||
|
|
||||||
// Find supporting object
|
// Optional message ID
|
||||||
|
byte hasId = readBuffer.get();
|
||||||
int hasId = in.read();
|
|
||||||
int id = -1;
|
int id = -1;
|
||||||
if (hasId != 0) {
|
if (hasId != 0) {
|
||||||
id = in.readInt();
|
id = readBuffer.getInt();
|
||||||
|
|
||||||
if (id <= 0)
|
if (id <= 0)
|
||||||
// Invalid ID
|
// Invalid ID
|
||||||
throw new MessageException("Invalid negative ID");
|
throw new MessageException("Invalid negative ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
int dataSize = in.readInt();
|
int dataSize = readBuffer.getInt();
|
||||||
|
|
||||||
if (dataSize > MAX_DATA_SIZE)
|
if (dataSize > MAX_DATA_SIZE)
|
||||||
// Too large
|
// Too large
|
||||||
throw new MessageException(String.format("Declared data length %d larger than max allowed %d", dataSize, MAX_DATA_SIZE));
|
throw new MessageException(String.format("Declared data length %d larger than max allowed %d", dataSize, MAX_DATA_SIZE));
|
||||||
|
|
||||||
byte[] data = null;
|
ByteBuffer dataSlice = null;
|
||||||
if (dataSize > 0) {
|
if (dataSize > 0) {
|
||||||
byte[] expectedChecksum = new byte[CHECKSUM_LENGTH];
|
byte[] expectedChecksum = new byte[CHECKSUM_LENGTH];
|
||||||
in.readFully(expectedChecksum);
|
readBuffer.get(expectedChecksum);
|
||||||
|
|
||||||
data = new byte[dataSize];
|
// Remember this position in readBuffer so we can pass to Message subclass
|
||||||
in.readFully(data);
|
dataSlice = readBuffer.slice();
|
||||||
|
|
||||||
|
// Consume data from buffer
|
||||||
|
byte[] data = new byte[dataSize];
|
||||||
|
readBuffer.get(data);
|
||||||
|
|
||||||
|
// We successfully read all the data bytes, so we can set limit on dataSlice
|
||||||
|
dataSlice.limit(dataSize);
|
||||||
|
|
||||||
// Test checksum
|
// Test checksum
|
||||||
byte[] actualChecksum = generateChecksum(data);
|
byte[] actualChecksum = generateChecksum(data);
|
||||||
@ -195,11 +211,17 @@ public abstract class Message {
|
|||||||
throw new MessageException("Message checksum incorrect");
|
throw new MessageException("Message checksum incorrect");
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageType.fromBytes(id, data);
|
Message message = messageType.fromByteBuffer(id, dataSlice);
|
||||||
} catch (SocketTimeoutException e) {
|
|
||||||
throw e;
|
// We successfully read a message, so bump byteBuffer's position to reflect this
|
||||||
} catch (IOException e) {
|
byteBuffer.position(readBuffer.position());
|
||||||
throw e;
|
|
||||||
|
return message;
|
||||||
|
} catch (BufferUnderflowException e) {
|
||||||
|
// Not enough bytes to fully decode message...
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
byteBuffer.compact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,7 +231,7 @@ public abstract class Message {
|
|||||||
|
|
||||||
public byte[] toBytes() throws MessageException {
|
public byte[] toBytes() throws MessageException {
|
||||||
try {
|
try {
|
||||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
|
||||||
|
|
||||||
// Magic
|
// Magic
|
||||||
bytes.write(Network.getInstance().getMessageMagic());
|
bytes.write(Network.getInstance().getMessageMagic());
|
||||||
|
Loading…
Reference in New Issue
Block a user