Skip to content

Commit 76064a0

Browse files
achow101vijaydasmp
authored andcommitted
Merge 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@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
1 parent 733e48d commit 76064a0

6 files changed

Lines changed: 130 additions & 3 deletions

File tree

src/node/blockstorage.cpp

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,23 @@ bool BlockManager::LoadBlockIndexDB()
400400
return true;
401401
}
402402

403+
void BlockManager::ScanAndUnlinkAlreadyPrunedFiles()
404+
{
405+
AssertLockHeld(::cs_main);
406+
if (!m_have_pruned) {
407+
return;
408+
}
409+
410+
std::set<int> block_files_to_prune;
411+
for (int file_number = 0; file_number < m_last_blockfile; file_number++) {
412+
if (m_blockfile_info[file_number].nSize == 0) {
413+
block_files_to_prune.insert(file_number);
414+
}
415+
}
416+
417+
UnlinkPrunedFiles(block_files_to_prune);
418+
}
419+
403420
const CBlockIndex* BlockManager::GetLastCheckpoint(const CCheckpointData& data)
404421
{
405422
const MapCheckpoints& checkpoints = data.mapCheckpoints;
@@ -575,11 +592,14 @@ uint64_t BlockManager::CalculateCurrentUsage()
575592

576593
void UnlinkPrunedFiles(const std::set<int>& setFilesToPrune)
577594
{
595+
std::error_code ec;
578596
for (std::set<int>::iterator it = setFilesToPrune.begin(); it != setFilesToPrune.end(); ++it) {
579597
FlatFilePos pos(*it, 0);
580-
fs::remove(BlockFileSeq().FileName(pos));
581-
fs::remove(UndoFileSeq().FileName(pos));
582-
LogPrint(BCLog::BLOCKSTORE, "Prune: %s deleted blk/rev (%05u)\n", __func__, *it);
598+
const bool removed_blockfile{fs::remove(BlockFileSeq().FileName(pos), ec)};
599+
const bool removed_undofile{fs::remove(UndoFileSeq().FileName(pos), ec)};
600+
if (removed_blockfile || removed_undofile) {
601+
LogPrint(BCLog::BLOCKSTORE, "Prune: %s deleted blk/rev (%05u)\n", __func__, *it);
602+
}
583603
}
584604
}
585605

src/node/blockstorage.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,13 @@ class BlockManager
169169
bool WriteBlockIndexDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
170170
bool LoadBlockIndexDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
171171

172+
/**
173+
* Remove any pruned block & undo files that are still on disk.
174+
* This could happen on some systems if the file was still being read while unlinked,
175+
* or if we crash before unlinking.
176+
*/
177+
void ScanAndUnlinkAlreadyPrunedFiles() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
178+
172179
CBlockIndex* AddToBlockIndex(const CBlockHeader& block, const uint256& hash, CBlockIndex*& best_header,
173180
enum BlockStatus nStatus = BLOCK_VALID_TREE)
174181
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)
@@ -42,4 +44,45 @@ BOOST_AUTO_TEST_CASE(blockmanager_find_block_pos)
4244
BOOST_CHECK_EQUAL(actual.nPos, BLOCK_SERIALIZATION_HEADER_SIZE + ::GetSerializeSize(params->GenesisBlock(), CLIENT_VERSION) + BLOCK_SERIALIZATION_HEADER_SIZE);
4345
}
4446

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

src/validation.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4693,6 +4693,8 @@ bool ChainstateManager::LoadBlockIndex()
46934693
bool ret{m_blockman.LoadBlockIndexDB()};
46944694
if (!ret) return false;
46954695

4696+
m_blockman.ScanAndUnlinkAlreadyPrunedFiles();
4697+
46964698
std::vector<CBlockIndex*> vSortedByHeight{m_blockman.GetAllBlockIndices()};
46974699
std::sort(vSortedByHeight.begin(), vSortedByHeight.end(),
46984700
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
@@ -402,6 +402,7 @@
402402
'p2p_permissions.py',
403403
'feature_blocksdir.py',
404404
'wallet_startup.py',
405+
'feature_remove_pruned_files_on_startup.py',
405406
'p2p_i2p_ports.py',
406407
'p2p_i2p_sessions.py',
407408
'feature_config_args.py',

0 commit comments

Comments
 (0)