Skip to content

Commit bb136aa

Browse files
committed
Merge bitcoin/bitcoin#26533: prune: scan and unlink already pruned block files on startup
3141eab test: add functional test for ScanAndUnlinkAlreadyPrunedFiles (Andrew Toth) e252909 test: add unit test for ScanAndUnlinkAlreadyPrunedFiles (Andrew Toth) 77557dd prune: scan and unlink already pruned block files on startup (Andrew Toth) Pull request description: There are a few cases where we can mark a block and undo file as pruned in our block index, but not actually remove the files from disk. 1. If we call `FindFilesToPrune` or `FindFilesToPruneManual` and crash before `UnlinkPrunedFiles`. 2. If on Windows there is an open file handle to the file somewhere else when calling `fs::remove` in `UnlinkPrunedFiles` (https://en.cppreference.com/w/cpp/filesystem/remove, https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-deletefilew#remarks). This could be from another process, or if we are calling `ReadBlockFromDisk`/`ReadRawBlockFromDisk` without having a lock on `cs_main` (which has been allowed since bitcoin/bitcoin@ccd8ef6). This PR mitigates this by scanning all pruned block files on startup after `LoadBlockIndexDB` and unlinking them again. ACKs for top commit: achow101: ACK 3141eab pablomartin4btc: re-ACK with added functional test 3141eab. furszy: Code review ACK 3141eab theStack: Code-review ACK 3141eab Tree-SHA512: 6c73bc57838ad1b7e5d441af3c4d6bf4c61c4382e2b86485e57fbb74a61240710c0ceeceb8b4834e610ecfa3175c6955c81ea4b2285fee11ca6383f472979d8d
2 parents 519ec26 + 3141eab commit bb136aa

File tree

6 files changed

+130
-3
lines changed

6 files changed

+130
-3
lines changed

src/node/blockstorage.cpp

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,23 @@ bool BlockManager::LoadBlockIndexDB(const Consensus::Params& consensus_params)
371371
return true;
372372
}
373373

374+
void BlockManager::ScanAndUnlinkAlreadyPrunedFiles()
375+
{
376+
AssertLockHeld(::cs_main);
377+
if (!m_have_pruned) {
378+
return;
379+
}
380+
381+
std::set<int> block_files_to_prune;
382+
for (int file_number = 0; file_number < m_last_blockfile; file_number++) {
383+
if (m_blockfile_info[file_number].nSize == 0) {
384+
block_files_to_prune.insert(file_number);
385+
}
386+
}
387+
388+
UnlinkPrunedFiles(block_files_to_prune);
389+
}
390+
374391
const CBlockIndex* BlockManager::GetLastCheckpoint(const CCheckpointData& data)
375392
{
376393
const MapCheckpoints& checkpoints = data.mapCheckpoints;
@@ -556,11 +573,14 @@ uint64_t BlockManager::CalculateCurrentUsage()
556573

557574
void UnlinkPrunedFiles(const std::set<int>& setFilesToPrune)
558575
{
576+
std::error_code ec;
559577
for (std::set<int>::iterator it = setFilesToPrune.begin(); it != setFilesToPrune.end(); ++it) {
560578
FlatFilePos pos(*it, 0);
561-
fs::remove(BlockFileSeq().FileName(pos));
562-
fs::remove(UndoFileSeq().FileName(pos));
563-
LogPrint(BCLog::BLOCKSTORE, "Prune: %s deleted blk/rev (%05u)\n", __func__, *it);
579+
const bool removed_blockfile{fs::remove(BlockFileSeq().FileName(pos), ec)};
580+
const bool removed_undofile{fs::remove(UndoFileSeq().FileName(pos), ec)};
581+
if (removed_blockfile || removed_undofile) {
582+
LogPrint(BCLog::BLOCKSTORE, "Prune: %s deleted blk/rev (%05u)\n", __func__, *it);
583+
}
564584
}
565585
}
566586

src/node/blockstorage.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ class BlockManager
154154
bool WriteBlockIndexDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
155155
bool LoadBlockIndexDB(const Consensus::Params& consensus_params) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
156156

157+
/**
158+
* Remove any pruned block & undo files that are still on disk.
159+
* This could happen on some systems if the file was still being read while unlinked,
160+
* or if we crash before unlinking.
161+
*/
162+
void ScanAndUnlinkAlreadyPrunedFiles() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
163+
157164
CBlockIndex* AddToBlockIndex(const CBlockHeader& block, CBlockIndex*& best_header) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
158165
/** Create a new block index entry for a given block hash */
159166
CBlockIndex* InsertBlockIndex(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main);

src/test/blockmanager_tests.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
using node::BlockManager;
1414
using node::BLOCK_SERIALIZATION_HEADER_SIZE;
15+
using node::MAX_BLOCKFILE_SIZE;
16+
using node::OpenBlockFile;
1517

1618
// use BasicTestingSetup here for the data directory configuration, setup, and cleanup
1719
BOOST_FIXTURE_TEST_SUITE(blockmanager_tests, BasicTestingSetup)
@@ -39,4 +41,45 @@ BOOST_AUTO_TEST_CASE(blockmanager_find_block_pos)
3941
BOOST_CHECK_EQUAL(actual.nPos, BLOCK_SERIALIZATION_HEADER_SIZE + ::GetSerializeSize(params->GenesisBlock(), CLIENT_VERSION) + BLOCK_SERIALIZATION_HEADER_SIZE);
4042
}
4143

44+
BOOST_FIXTURE_TEST_CASE(blockmanager_scan_unlink_already_pruned_files, TestChain100Setup)
45+
{
46+
// Cap last block file size, and mine new block in a new block file.
47+
const auto& chainman = Assert(m_node.chainman);
48+
auto& blockman = chainman->m_blockman;
49+
const CBlockIndex* old_tip{WITH_LOCK(chainman->GetMutex(), return chainman->ActiveChain().Tip())};
50+
WITH_LOCK(chainman->GetMutex(), blockman.GetBlockFileInfo(old_tip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
51+
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
52+
53+
// Prune the older block file, but don't unlink it
54+
int file_number;
55+
{
56+
LOCK(chainman->GetMutex());
57+
file_number = old_tip->GetBlockPos().nFile;
58+
blockman.PruneOneBlockFile(file_number);
59+
}
60+
61+
const FlatFilePos pos(file_number, 0);
62+
63+
// Check that the file is not unlinked after ScanAndUnlinkAlreadyPrunedFiles
64+
// if m_have_pruned is not yet set
65+
WITH_LOCK(chainman->GetMutex(), blockman.ScanAndUnlinkAlreadyPrunedFiles());
66+
BOOST_CHECK(!AutoFile(OpenBlockFile(pos, true)).IsNull());
67+
68+
// Check that the file is unlinked after ScanAndUnlinkAlreadyPrunedFiles
69+
// once m_have_pruned is set
70+
blockman.m_have_pruned = true;
71+
WITH_LOCK(chainman->GetMutex(), blockman.ScanAndUnlinkAlreadyPrunedFiles());
72+
BOOST_CHECK(AutoFile(OpenBlockFile(pos, true)).IsNull());
73+
74+
// Check that calling with already pruned files doesn't cause an error
75+
WITH_LOCK(chainman->GetMutex(), blockman.ScanAndUnlinkAlreadyPrunedFiles());
76+
77+
// Check that the new tip file has not been removed
78+
const CBlockIndex* new_tip{WITH_LOCK(chainman->GetMutex(), return chainman->ActiveChain().Tip())};
79+
BOOST_CHECK_NE(old_tip, new_tip);
80+
const int new_file_number{WITH_LOCK(chainman->GetMutex(), return new_tip->GetBlockPos().nFile)};
81+
const FlatFilePos new_pos(new_file_number, 0);
82+
BOOST_CHECK(!AutoFile(OpenBlockFile(new_pos, true)).IsNull());
83+
}
84+
4285
BOOST_AUTO_TEST_SUITE_END()

src/validation.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4323,6 +4323,8 @@ bool ChainstateManager::LoadBlockIndex()
43234323
bool ret = m_blockman.LoadBlockIndexDB(GetConsensus());
43244324
if (!ret) return false;
43254325

4326+
m_blockman.ScanAndUnlinkAlreadyPrunedFiles();
4327+
43264328
std::vector<CBlockIndex*> vSortedByHeight{m_blockman.GetAllBlockIndices()};
43274329
std::sort(vSortedByHeight.begin(), vSortedByHeight.end(),
43284330
CBlockIndexHeightOnlyComparator());
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022 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+
"""Test removing undeleted pruned blk files on startup."""
6+
7+
import os
8+
from test_framework.test_framework import BitcoinTestFramework
9+
10+
class FeatureRemovePrunedFilesOnStartupTest(BitcoinTestFramework):
11+
def set_test_params(self):
12+
self.num_nodes = 1
13+
self.extra_args = [["-fastprune", "-prune=1"]]
14+
15+
def mine_batches(self, blocks):
16+
n = blocks // 250
17+
for _ in range(n):
18+
self.generate(self.nodes[0], 250)
19+
self.generate(self.nodes[0], blocks % 250)
20+
self.sync_blocks()
21+
22+
def run_test(self):
23+
blk0 = os.path.join(self.nodes[0].datadir, self.nodes[0].chain, 'blocks', 'blk00000.dat')
24+
rev0 = os.path.join(self.nodes[0].datadir, self.nodes[0].chain, 'blocks', 'rev00000.dat')
25+
blk1 = os.path.join(self.nodes[0].datadir, self.nodes[0].chain, 'blocks', 'blk00001.dat')
26+
rev1 = os.path.join(self.nodes[0].datadir, self.nodes[0].chain, 'blocks', 'rev00001.dat')
27+
self.mine_batches(800)
28+
fo1 = os.open(blk0, os.O_RDONLY)
29+
fo2 = os.open(rev1, os.O_RDONLY)
30+
fd1 = os.fdopen(fo1)
31+
fd2 = os.fdopen(fo2)
32+
self.nodes[0].pruneblockchain(600)
33+
34+
# Windows systems will not remove files with an open fd
35+
if os.name != 'nt':
36+
assert not os.path.exists(blk0)
37+
assert not os.path.exists(rev0)
38+
assert not os.path.exists(blk1)
39+
assert not os.path.exists(rev1)
40+
else:
41+
assert os.path.exists(blk0)
42+
assert not os.path.exists(rev0)
43+
assert not os.path.exists(blk1)
44+
assert os.path.exists(rev1)
45+
46+
# Check that the files are removed on restart once the fds are closed
47+
fd1.close()
48+
fd2.close()
49+
self.restart_node(0)
50+
assert not os.path.exists(blk0)
51+
assert not os.path.exists(rev1)
52+
53+
if __name__ == '__main__':
54+
FeatureRemovePrunedFilesOnStartupTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@
342342
'p2p_permissions.py',
343343
'feature_blocksdir.py',
344344
'wallet_startup.py',
345+
'feature_remove_pruned_files_on_startup.py',
345346
'p2p_i2p_ports.py',
346347
'p2p_i2p_sessions.py',
347348
'feature_config_args.py',

0 commit comments

Comments
 (0)