Skip to content

Commit aef8c7c

Browse files
committed
Merge bitcoin/bitcoin#23289: test: add stress tests for initialization
d9803f7 test: add stress tests for initialization (James O'Beirne) 23f8561 test: add node.chain_path and node.debug_log_path (James O'Beirne) Pull request description: In the course of coming up with a test plan for #23280, I thought it would be neat to include a Python snippet showing how I tested the initialization process. I quickly realized I was reinventing the functional test framework... so here's a new test. This change bangs init around like the Fonz hitting a jukebox. It adds some interesting (read: lazy and random) coverage for the initialization process by - interrupting init with SIGTERM after certain log statements, - interrupting init at random points, and - starting init with some essential data missing (block files, block indices, etc.) to test init error paths. As far as I can tell, some of these code paths are uncovered otherwise (namely the startup errors). --- Incidentally, I think I may have uncovered some kind of a bug or race condition with indexing initialization based on an intermittent failure in this testcase. This test sometimes fails after shutting down immediately after `loadblk` thread start: ``` 2021-10-15T21:14:51.295000Z TestFramework (INFO): Starting node and will exit after line 'loadblk thread start' 36 │ 2021-10-15T21:14:51.296000Z TestFramework.node0 (DEBUG): bitcoind started, waiting for RPC to come up 37 │ 2021-10-15T21:14:51.493000Z TestFramework (INFO): terminating node after 110 log lines seen 38 │ 2021-10-15T21:14:51.625000Z TestFramework (INFO): Starting node and will exit after line 'txindex thread start' 39 │ 2021-10-15T21:14:51.625000Z TestFramework.node0 (DEBUG): bitcoind started, waiting for RPC to come up ------> [[ FAILURE HERE ]] 2021-10-15T21:15:21.626000Z TestFramework (WARNING): missed line {bail_line}; bailing now after {num_lines} lines ``` and then fails to start up afterwards. Combined logs showing `Error: txindex best block of the index goes beyond pruned data`, when the node under test is not pruned: ``` node0 2021-10-15T21:16:51.848439Z [shutoff] [validationinterface.cpp:244] [ChainStateFlushed] Enqueuing ChainStateFlushed: block hash=1014bc4ff4917602ae53d10e9dfe230af4b7d52a6cdaa8a47798b9c288180907 node0 2021-10-15T21:16:51.848954Z [shutoff] [init.cpp:302] [Shutdown] Shutdown: done test 2021-10-15T21:16:51.882000Z TestFramework (ERROR): Unexpected exception caught during testing Traceback (most recent call last): File "/home/james/src/bitcoin/test/functional/test_framework/test_framework.py", line 132, in main self.run_test() File "/home/james/src/bitcoin/./test/functional/stress_init.py", line 87, in run_test check_clean_start() File "/home/james/src/bitcoin/./test/functional/stress_init.py", line 60, in check_clean_start node.wait_for_rpc_connection() File "/home/james/src/bitcoin/test/functional/test_framework/test_node.py", line 224, in wait_for_rpc_connection raise FailedToStartError(self._node_msg( test_framework.test_node.FailedToStartError: [node 0] bitcoind exited with status 1 during initialization test 2021-10-15T21:16:51.882000Z TestFramework (DEBUG): Closing down network thread test 2021-10-15T21:16:51.933000Z TestFramework (INFO): Stopping nodes test 2021-10-15T21:16:51.933000Z TestFramework.node0 (DEBUG): Stopping node node0 stderr Error: txindex best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again) node0 stderr Error: txindex best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again) node0 stderr Error: txindex best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again) node0 stderr Error: txindex best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again) node0 stderr Error: txindex best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again) ``` ACKs for top commit: laanwj: Code review ACK d9803f7 Tree-SHA512: 4d80dc399daf199a6222e81e47d12d830dc7af07355eddbb7f52479a676a645b8d3d45093ff54a9295f01a163b2f4fe0e038e83fc269969e03d4cfda69eaf111
2 parents c5712d1 + d9803f7 commit aef8c7c

File tree

3 files changed

+194
-3
lines changed

3 files changed

+194
-3
lines changed

test/functional/feature_init.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2021 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Stress tests related to node initialization."""
6+
import random
7+
import time
8+
import os
9+
from pathlib import Path
10+
11+
from test_framework.test_framework import BitcoinTestFramework, SkipTest
12+
from test_framework.test_node import ErrorMatch
13+
from test_framework.util import assert_equal
14+
15+
16+
class InitStressTest(BitcoinTestFramework):
17+
"""
18+
Ensure that initialization can be interrupted at a number of points and not impair
19+
subsequent starts.
20+
"""
21+
22+
def set_test_params(self):
23+
self.setup_clean_chain = False
24+
self.num_nodes = 1
25+
26+
def run_test(self):
27+
"""
28+
- test terminating initialization after seeing a certain log line.
29+
- test terminating init after seeing a random number of log lines.
30+
- test removing certain essential files to test startup error paths.
31+
"""
32+
# TODO: skip Windows for now since it isn't clear how to SIGTERM.
33+
#
34+
# Windows doesn't support `process.terminate()`.
35+
# and other approaches (like below) don't work:
36+
#
37+
# os.kill(node.process.pid, signal.CTRL_C_EVENT)
38+
if os.name == 'nt':
39+
raise SkipTest("can't SIGTERM on Windows")
40+
41+
self.stop_node(0)
42+
node = self.nodes[0]
43+
44+
def sigterm_node():
45+
node.process.terminate()
46+
node.process.wait()
47+
node.debug_log_path.unlink()
48+
node.debug_log_path.touch()
49+
50+
def check_clean_start():
51+
"""Ensure that node restarts successfully after various interrupts."""
52+
# TODO: add -txindex=1 to fully test index initiatlization.
53+
# See https://github.com/bitcoin/bitcoin/pull/23289#discussion_r735159180 for
54+
# a discussion of the related bug.
55+
node.start()
56+
node.wait_for_rpc_connection()
57+
assert_equal(200, node.getblockcount())
58+
59+
lines_to_terminate_after = [
60+
'scheduler thread start',
61+
'Loading P2P addresses',
62+
'Loading banlist',
63+
'Loading block index',
64+
'Switching active chainstate',
65+
'Loaded best chain:',
66+
'init message: Verifying blocks',
67+
'loadblk thread start',
68+
# TODO: reenable - see above TODO
69+
# 'txindex thread start',
70+
'net thread start',
71+
'addcon thread start',
72+
'msghand thread start',
73+
]
74+
if self.is_wallet_compiled():
75+
lines_to_terminate_after.append('Verifying wallet')
76+
77+
for terminate_line in lines_to_terminate_after:
78+
self.log.info(f"Starting node and will exit after line '{terminate_line}'")
79+
node.start(
80+
# TODO: add -txindex=1 to fully test index initiatlization.
81+
# extra_args=['-txindex=1'],
82+
)
83+
logfile = open(node.debug_log_path, 'r', encoding='utf8')
84+
85+
MAX_SECS_TO_WAIT = 30
86+
start = time.time()
87+
num_lines = 0
88+
89+
while True:
90+
line = logfile.readline()
91+
if line:
92+
num_lines += 1
93+
94+
if line and terminate_line.lower() in line.lower():
95+
self.log.debug(f"Terminating node after {num_lines} log lines seen")
96+
sigterm_node()
97+
break
98+
99+
if (time.time() - start) > MAX_SECS_TO_WAIT:
100+
raise AssertionError(
101+
f"missed line {terminate_line}; terminating now after {num_lines} lines")
102+
103+
if node.process.poll() is not None:
104+
raise AssertionError(f"node failed to start (line: '{terminate_line}')")
105+
106+
check_clean_start()
107+
num_total_logs = len(node.debug_log_path.read_text().splitlines())
108+
self.stop_node(0)
109+
110+
self.log.info(
111+
f"Terminate at some random point in the init process (max logs: {num_total_logs})")
112+
113+
for _ in range(40):
114+
terminate_after = random.randint(1, num_total_logs)
115+
self.log.debug(f"Starting node and will exit after {terminate_after} lines")
116+
node.start(
117+
# TODO: add -txindex=1 to fully test index initiatlization.
118+
# extra_args=['-txindex=1'],
119+
)
120+
logfile = open(node.debug_log_path, 'r', encoding='utf8')
121+
122+
MAX_SECS_TO_WAIT = 10
123+
start = time.time()
124+
num_lines = 0
125+
126+
while True:
127+
line = logfile.readline()
128+
if line:
129+
num_lines += 1
130+
131+
if num_lines >= terminate_after or (time.time() - start) > MAX_SECS_TO_WAIT:
132+
self.log.debug(f"Terminating node after {num_lines} log lines seen")
133+
sigterm_node()
134+
break
135+
136+
if node.process.poll() is not None:
137+
raise AssertionError("node failed to start")
138+
139+
check_clean_start()
140+
self.stop_node(0)
141+
142+
self.log.info("Test startup errors after removing certain essential files")
143+
144+
files_to_disturb = {
145+
'blocks/index/*.ldb': 'Error opening block database.',
146+
'chainstate/*.ldb': 'Error opening block database.',
147+
'blocks/blk*.dat': 'Error loading block database.',
148+
}
149+
150+
for file_patt, err_fragment in files_to_disturb.items():
151+
target_file = list(node.chain_path.glob(file_patt))[0]
152+
153+
self.log.info(f"Tweaking file to ensure failure {target_file}")
154+
bak_path = str(target_file) + ".bak"
155+
target_file.rename(bak_path)
156+
157+
# TODO: at some point, we should test perturbing the files instead of removing
158+
# them, e.g.
159+
#
160+
# contents = target_file.read_bytes()
161+
# tweaked_contents = bytearray(contents)
162+
# tweaked_contents[50:250] = b'1' * 200
163+
# target_file.write_bytes(bytes(tweaked_contents))
164+
#
165+
# At the moment I can't get this to work (bitcoind loads successfully?) so
166+
# investigate doing this later.
167+
168+
node.assert_start_raises_init_error(
169+
# TODO: add -txindex=1 to fully test index initiatlization.
170+
# extra_args=['-txindex=1'],
171+
expected_msg=err_fragment,
172+
match=ErrorMatch.PARTIAL_REGEX,
173+
)
174+
175+
self.log.info(f"Restoring file from {bak_path} and restarting")
176+
Path(bak_path).rename(target_file)
177+
check_clean_start()
178+
self.stop_node(0)
179+
180+
181+
if __name__ == '__main__':
182+
InitStressTest().main()

test/functional/test_framework/test_node.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import collections
2121
import shlex
2222
import sys
23+
from pathlib import Path
2324

2425
from .authproxy import JSONRPCException
2526
from .descriptors import descsum_create
@@ -380,21 +381,28 @@ def is_node_stopped(self):
380381
def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT):
381382
wait_until_helper(self.is_node_stopped, timeout=timeout, timeout_factor=self.timeout_factor)
382383

384+
@property
385+
def chain_path(self) -> Path:
386+
return Path(self.datadir) / self.chain
387+
388+
@property
389+
def debug_log_path(self) -> Path:
390+
return self.chain_path / 'debug.log'
391+
383392
@contextlib.contextmanager
384393
def assert_debug_log(self, expected_msgs, unexpected_msgs=None, timeout=2):
385394
if unexpected_msgs is None:
386395
unexpected_msgs = []
387396
time_end = time.time() + timeout * self.timeout_factor
388-
debug_log = os.path.join(self.datadir, self.chain, 'debug.log')
389-
with open(debug_log, encoding='utf-8') as dl:
397+
with open(self.debug_log_path, encoding='utf-8') as dl:
390398
dl.seek(0, 2)
391399
prev_size = dl.tell()
392400

393401
yield
394402

395403
while True:
396404
found = True
397-
with open(debug_log, encoding='utf-8') as dl:
405+
with open(self.debug_log_path, encoding='utf-8') as dl:
398406
dl.seek(prev_size)
399407
log = dl.read()
400408
print_log = " - " + "\n - ".join(log.splitlines())

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@
278278
'wallet_taproot.py',
279279
'p2p_fingerprint.py',
280280
'feature_uacomment.py',
281+
'feature_init.py',
281282
'wallet_coinbase_category.py --legacy-wallet',
282283
'wallet_coinbase_category.py --descriptors',
283284
'feature_filelock.py',

0 commit comments

Comments
 (0)