mirror of
https://github.com/dogecoin/dogecoin.git
synced 2026-01-31 10:30:52 +00:00
The algorithm here is important and took some time to get right. Instead of comparing whether the current number of connected nodes minus the number of unevictable nodes is greater than the number of max connections, check that: * there are any evictable nodes (connected nodes minus unevictable nodes) * there are more nodes connected than requested (connected nodes minus max connections) While we could wait for nodes to disconnect organically, it's more important to run the eviction logic frequently enough that we can tell when it will have an effect. Whitelisted connections and protected inbound connections are unevictable, and max connections should account for inbound connections. Because the evictor will never evict protected inbound connections, the maximum connection count should always be at least as large as the protected connection count. Note that the tests for this use a delay and test that the delay has not expired. This helps improve determinism in the testing. Otherwise, a strict test for a fixed number of disconnections is susceptible to things like CPU jitter, especially when running through CI. Patrick ran this test for 1000 runs on busy CPUs and saw no failures.
191 lines
6.5 KiB
Python
191 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) 2021 The Dogecoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
#
|
|
# Exercise setmaxconnections RPC command
|
|
#
|
|
# from the constants MAX_ADDNODE_CONNECTIONS and PROTECTED_INBOUND_PEERS in src/net.h
|
|
MAX_ADDNODE_CONNECTIONS = 8
|
|
PROTECTED_INBOUND_PEERS = 4 + 8 + 4 + 4
|
|
MINIMUM_CONNECTIONS = MAX_ADDNODE_CONNECTIONS
|
|
MINIMUM_CONNECTIONS_INTERNAL = MAX_ADDNODE_CONNECTIONS + PROTECTED_INBOUND_PEERS
|
|
|
|
from test_framework.mininode import *
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import *
|
|
|
|
class TestNode(NodeConnCB):
|
|
def __init__(self):
|
|
NodeConnCB.__init__(self)
|
|
self.connection = None
|
|
self.peer_disconnected = False
|
|
self.ping_counter = 0
|
|
|
|
def add_connection(self, conn):
|
|
self.connection = conn
|
|
|
|
def close(self):
|
|
self.connection.handle_close()
|
|
self.peer_disconnected = True
|
|
|
|
class SetMaxConnectionCountTest (BitcoinTestFramework):
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setup_clean_chain = True
|
|
self.nodes = []
|
|
self.client_nodes = []
|
|
self.num_nodes = 1
|
|
|
|
def create_testnode(self, node_idx=0):
|
|
node = TestNode()
|
|
conn = NodeConn('127.0.0.1', p2p_port(node_idx), self.nodes[node_idx], node)
|
|
node.add_connection(conn)
|
|
return node
|
|
|
|
def setup_network(self):
|
|
self.nodes.append(start_node(0, self.options.tmpdir, ["-debug", "-nmaxconnections=0"]))
|
|
self.is_network_split = False
|
|
NetworkThread().start()
|
|
|
|
def wait_for_verack(self, nodes, timeout_seconds=5):
|
|
def nodes_veracked():
|
|
for node in nodes:
|
|
if not node.verack_received:
|
|
return False
|
|
|
|
return True
|
|
wait_until(nodes_veracked, timeout=timeout_seconds)
|
|
|
|
def connect_nodes(self, nodes, count):
|
|
for i in range(0, count):
|
|
nodes.append(self.create_testnode())
|
|
|
|
self.wait_for_verack(nodes)
|
|
|
|
def run_test(self):
|
|
self.test_rpc_argument_validation()
|
|
|
|
# these numbers must meet or exceed PROTECTED_INBOUND_CONNECTIONS
|
|
# otherwise there aren't enough nodes to disconnect
|
|
self.test_node_connection_changes(20)
|
|
self.test_node_connection_changes(30)
|
|
|
|
# max_count has to be at least 20
|
|
# min_count can be closer to 20
|
|
self.test_node_disconnections(40, 20)
|
|
|
|
def test_rpc_argument_validation(self):
|
|
first_node = self.nodes[0]
|
|
|
|
try:
|
|
first_node.setmaxconnections()
|
|
raise AssertionError("Must check for no parameter provided")
|
|
except JSONRPCException as e:
|
|
assert("1. maxconnectioncount" in e.error['message'])
|
|
|
|
try:
|
|
first_node.setmaxconnections("good doge bad doge")
|
|
raise AssertionError("Must check for no numeric parameter provided")
|
|
except JSONRPCException as e:
|
|
assert("JSON value is not an integer as expected" in e.error['message'])
|
|
|
|
try:
|
|
first_node.setmaxconnections(-1)
|
|
raise AssertionError(f"Must check for parameter value >= {MINIMUM_CONNECTIONS}")
|
|
except JSONRPCException as e:
|
|
assert(f"maxconnectioncount must be >= {MINIMUM_CONNECTIONS}" in e.error['message'])
|
|
|
|
try:
|
|
first_node.setmaxconnections(7)
|
|
raise AssertionError(f"Must check for parameter value >= {MINIMUM_CONNECTIONS}")
|
|
except JSONRPCException as e:
|
|
assert(f"maxconnectioncount must be >= {MINIMUM_CONNECTIONS}" in e.error['message'])
|
|
|
|
try:
|
|
first_node.setmaxconnections(MINIMUM_CONNECTIONS)
|
|
assert(True)
|
|
except JSONRPCException as e:
|
|
raise AssertionError(f"Must allow parameter value >= {MINIMUM_CONNECTIONS}")
|
|
|
|
def wait_for_n_disconnections(self, nodes, count, timeout):
|
|
def disconnected():
|
|
closed_conns = [node.peer_disconnected for node in nodes]
|
|
print(f'Len {len(closed_conns)}, waiting for {count} of {len(nodes)} => {len(closed_conns) >= count}')
|
|
return len(closed_conns) >= count
|
|
return wait_until(disconnected, timeout=timeout)
|
|
|
|
def test_node_connection_changes(self, extras):
|
|
first_node = self.nodes[0]
|
|
|
|
# MINIMUM_CONNECTIONS outgoing connections plus 1 feeler
|
|
max_connections = 1 + MINIMUM_CONNECTIONS + extras
|
|
first_node.setmaxconnections(max_connections)
|
|
client_nodes = []
|
|
|
|
self.connect_nodes(client_nodes, extras)
|
|
x = first_node.getconnectioncount()
|
|
assert(x <= extras)
|
|
|
|
# attempt to add more nodes
|
|
self.connect_nodes(client_nodes, 3)
|
|
|
|
# the new nodes should not increase the connection count
|
|
x = first_node.getconnectioncount()
|
|
assert(x <= extras)
|
|
|
|
first_node.setmaxconnections(MINIMUM_CONNECTIONS)
|
|
|
|
|
|
disconnectable_connections=max_connections - MINIMUM_CONNECTIONS_INTERNAL
|
|
assert(self.wait_for_n_disconnections(client_nodes, count=disconnectable_connections, timeout=30))
|
|
|
|
# disconnect to clean up for the next test
|
|
for node in client_nodes:
|
|
node.close()
|
|
|
|
def test_node_disconnections(self, max_count, min_count):
|
|
first_node = self.nodes[0]
|
|
|
|
attempted_nodes = []
|
|
|
|
# MINIMUM_CONNECTIONS outgoing connections plus 1 feeler
|
|
# plus 20 connections protected from eviction
|
|
first_node.setmaxconnections(20 + 1 + MINIMUM_CONNECTIONS + max_count)
|
|
client_nodes = []
|
|
|
|
self.connect_nodes(client_nodes, max_count)
|
|
x = first_node.getconnectioncount()
|
|
assert(x <= max_count)
|
|
|
|
first_node.setmaxconnections(min_count)
|
|
|
|
def nodes_disconnected():
|
|
disc_count = 0
|
|
|
|
for node in attempted_nodes:
|
|
if node.peer_disconnected:
|
|
disc_count += 1
|
|
else:
|
|
node.sync_with_ping(0.1)
|
|
|
|
if disc_count < max_count - min_count:
|
|
return False
|
|
return True
|
|
wait_until(nodes_disconnected, timeout=30)
|
|
|
|
# try asserting this two ways, for debugging the test
|
|
x = first_node.getconnectioncount()
|
|
assert(x < max_count)
|
|
actual_min = max(min_count, MINIMUM_CONNECTIONS_INTERNAL)
|
|
assert(x == actual_min)
|
|
|
|
# disconnect to clean up for the next test
|
|
for node in attempted_nodes:
|
|
node.close()
|
|
|
|
if __name__ == '__main__':
|
|
SetMaxConnectionCountTest().main()
|