Force a disconnect of each peer when the connection age reaches the maximum allowed time.

Connection limits are defined in settings (denominated in seconds):
"minPeerConnectionTime": 120,
"maxPeerConnectionTime": 3600

Peers will disconnect after a randomly chosen amount of time between the min and the max. The default range is 2 minutes to 1 hour, as above.

This encourages nodes to connect to a wider range of peers across the course of each day, rather than staying connected to an "island" of peers for an extended period of time. Hopefully this will reduce the amount of parallel chains that can form due to permanently connected clusters of peers.

We may find that we need to reduce the defaults to get optimal results, however it is best to do this incrementally, with the option for reducing further via each node's settings. Being too aggressive here could cause some of the earlier problems (e.g. 20% missing blocks minted) to reappear. We can re-evaluate this in the next version. Note that if defaults are reduced significantly, we may need to add code to prevent this from happening mid-sync. With higher defaults, this is less of an issue.

Thanks to @szisti for supplying some base code for this commit, and also to @CWDSYSTEMS for diagnosing the original problem.
This commit is contained in:
CalDescent 2021-06-19 13:03:46 +01:00
parent 4cff03e7fe
commit 73cc3dcb92
3 changed files with 56 additions and 0 deletions

View File

@ -80,6 +80,8 @@ public class Network {
public static final int MAX_SIGNATURES_PER_REPLY = 500;
public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500;
private static final long DISCONNECTION_CHECK_INTERVAL = 10 * 1000L; // milliseconds
// Generate our node keys / ID
private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
@ -89,6 +91,8 @@ public class Network {
private final int minOutboundPeers;
private final int maxPeers;
private long nextDisconnectionCheck = 0L;
private final List<PeerData> allKnownPeers = new ArrayList<>();
private final List<Peer> connectedPeers = new ArrayList<>();
private final List<PeerAddress> selfPeers = new ArrayList<>();
@ -576,6 +580,8 @@ public class Network {
// Don't consider already connected peers (resolved address match)
// XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
peers.removeIf(isResolvedAsConnectedPeer);
this.checkLongestConnection(now);
}
// Any left?
@ -633,6 +639,24 @@ public class Network {
return null;
}
private void checkLongestConnection(Long now) {
if (now == null || now < nextDisconnectionCheck) {
return;
}
// Find peers that have reached their maximum connection age, and disconnect them
List<Peer> peersToDisconnect = this.connectedPeers.stream().filter(peer -> peer.hasReachedMaxConnectionAge()).collect(Collectors.toList());
if (peersToDisconnect != null && peersToDisconnect.size() > 0) {
for (Peer peer : peersToDisconnect) {
LOGGER.debug("Forcing disconnect of peer {} because connection age ({} ms) has reached the maximum ({} ms)", peer, peer.getConnectionAge(), peer.getMaxConnectionAge());
peer.disconnect("Connection age too old");
}
}
// Check again after a minimum fixed interval
nextDisconnectionCheck = now + DISCONNECTION_CHECK_INTERVAL;
}
// Peer callbacks
protected void wakeupChannelSelector() {

View File

@ -79,6 +79,7 @@ public class Peer {
private Handshake handshakeStatus = Handshake.STARTED;
private volatile boolean handshakeMessagePending = false;
private long handshakeComplete = -1L;
private long maxConnectionAge = 0L;
/**
* Timestamp of when socket was accepted, or connected.
@ -192,10 +193,24 @@ public class Peer {
this.handshakeStatus = handshakeStatus;
if (handshakeStatus.equals(Handshake.COMPLETED)) {
this.handshakeComplete = System.currentTimeMillis();
this.generateRandomMaxConnectionAge();
}
}
}
private void generateRandomMaxConnectionAge() {
// Retrieve the min and max connection time from the settings, and calculate the range
final int minPeerConnectionTime = Settings.getInstance().getMinPeerConnectionTime();
final int maxPeerConnectionTime = Settings.getInstance().getMaxPeerConnectionTime();
final int peerConnectionTimeRange = maxPeerConnectionTime - minPeerConnectionTime;
// Generate a random number between the min and the max
Random random = new Random();
this.maxConnectionAge = (random.nextInt(peerConnectionTimeRange) + minPeerConnectionTime) * 1000L;
LOGGER.debug(String.format("Generated max connection age for peer %s. Min: %ds, max: %ds, range: %ds, random max: %dms", this, minPeerConnectionTime, maxPeerConnectionTime, peerConnectionTimeRange, this.maxConnectionAge));
}
protected void resetHandshakeMessagePending() {
this.handshakeMessagePending = false;
}
@ -777,4 +792,12 @@ public class Peer {
}
return handshakeComplete;
}
public long getMaxConnectionAge() {
return maxConnectionAge;
}
public boolean hasReachedMaxConnectionAge() {
return this.getConnectionAge() > this.getMaxConnectionAge();
}
}

View File

@ -132,6 +132,11 @@ public class Settings {
* If false, sync will be blocked both ways, and they will not appear in the peers list */
private boolean allowConnectionsWithOlderPeerVersions = true;
/** Minimum time (in seconds) that we should attempt to remain connected to a peer for */
private int minPeerConnectionTime = 2 * 60;
/** Maximum time (in seconds) that we should attempt to remain connected to a peer for */
private int maxPeerConnectionTime = 60 * 60;
// Which blockchains this node is running
private String blockchainConfig = null; // use default from resources
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
@ -423,6 +428,10 @@ public class Settings {
public boolean getAllowConnectionsWithOlderPeerVersions() { return this.allowConnectionsWithOlderPeerVersions; }
public int getMinPeerConnectionTime() { return this.minPeerConnectionTime; }
public int getMaxPeerConnectionTime() { return this.maxPeerConnectionTime; }
public String getBlockchainConfig() {
return this.blockchainConfig;
}