From aa021b26f39fd231b2a3aac5780d5113a4aea639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Mon, 8 Jun 2026 15:46:45 +0200 Subject: [PATCH] validation: randomly compact chainstate Full chainstate flushes are convenient maintenance points for long-term LevelDB cleanup because the chainstate was just written. Randomize the trigger so nodes that flush near the same height do not compact together. Add blocking chainstate compaction through `CCoinsViewDB::CompactFull()` and give each post-IBD full flush on the normal chainstate a 1/320 chance to start compaction. With hourly flushes this averages roughly every two weeks and makes a six-month miss about one in a million. This keeps the schedule stateless and leaves last-compaction height or timestamp bookkeeping out of chainstate metadata. Co-authored-by: Andrew Toth --- src/dbwrapper.cpp | 4 +++- src/dbwrapper.h | 3 +++ src/test/coins_tests.cpp | 2 ++ src/txdb.cpp | 8 ++++++++ src/txdb.h | 3 +++ src/validation.cpp | 21 ++++++++++++++++++--- 6 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index d51a347c627..ffe6f267a6b 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -256,7 +256,7 @@ CDBWrapper::CDBWrapper(const DBParams& params) if (params.options.force_compact) { LogInfo("Starting database compaction of %s", fs::PathToString(params.path)); - DBContext().pdb->CompactRange(nullptr, nullptr); + CompactFull(); LogInfo("Finished database compaction of %s", fs::PathToString(params.path)); } @@ -307,6 +307,8 @@ std::optional CDBWrapper::GetProperty(const std::string& property) return std::nullopt; } +void CDBWrapper::CompactFull() { DBContext().pdb->CompactRange(nullptr, nullptr); } + size_t CDBWrapper::DynamicMemoryUsage() const { std::optional parsed; diff --git a/src/dbwrapper.h b/src/dbwrapper.h index a5f631162a9..83da6febe75 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -258,6 +258,9 @@ public: void WriteBatch(CDBBatch& batch, bool fSync = false); + //! Perform a blocking full compaction of the underlying LevelDB. + void CompactFull(); + //! Return a LevelDB property value, if available. std::optional GetProperty(const std::string& property) const; diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp index 847297e5148..4c5ed748847 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -1078,6 +1078,8 @@ BOOST_FIXTURE_TEST_CASE(coins_db_leveldb_layout, FlushTest) cache.Sync(); BOOST_CHECK_EQUAL(level2_files(base), 0); + WITH_LOCK(::cs_main, base.CompactFull()); + BOOST_CHECK_EQUAL(level2_files(base), 1); BOOST_CHECK(*Assert(base.GetCoin(outpoint)) == coin); BOOST_CHECK_EQUAL(base.GetBestBlock(), block_hash); diff --git a/src/txdb.cpp b/src/txdb.cpp index 7f45a7c0683..62dc49a5f81 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -180,6 +180,14 @@ std::optional CCoinsViewDB::GetDBProperty(const std::string& proper return m_db->GetProperty(property); } +void CCoinsViewDB::CompactFull() +{ + AssertLockHeld(::cs_main); + LogDebug(BCLog::COINDB, "Starting chainstate compaction of %s", fs::PathToString(m_db_params.path)); + m_db->CompactFull(); + LogDebug(BCLog::COINDB, "Finished chainstate compaction of %s", fs::PathToString(m_db_params.path)); +} + /** Specialization of CCoinsViewCursor to iterate over a CCoinsViewDB */ class CCoinsViewDBCursor: public CCoinsViewCursor { diff --git a/src/txdb.h b/src/txdb.h index 827b36f3fa6..648adc85e55 100644 --- a/src/txdb.h +++ b/src/txdb.h @@ -56,6 +56,9 @@ public: //! Dynamically alter the underlying leveldb cache size. void ResizeCache(size_t new_cache_size) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + //! Perform a blocking full compaction of the underlying LevelDB. + void CompactFull() EXCLUSIVE_LOCKS_REQUIRED(cs_main); + //! Return an underlying LevelDB property value, if available. std::optional GetDBProperty(const std::string& property); }; diff --git a/src/validation.cpp b/src/validation.cpp index 77e91999b6c..df4bd8673cf 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -113,6 +113,13 @@ const std::vector CHECKLEVEL_DOC { * */ static constexpr int PRUNE_LOCK_BUFFER{10}; +// Return whether the completed full flush should compact chainstate +static bool ShouldCompactChainstate(bool in_ibd) +{ + static constexpr uint32_t flush_ratio{320}; // Roughly every 2 weeks with hourly flushes + return !in_ibd && FastRandomContext().randrange(flush_ratio) == 0; +} + TRACEPOINT_SEMAPHORE(validation, block_connected); TRACEPOINT_SEMAPHORE(utxocache, flush); TRACEPOINT_SEMAPHORE(mempool, replaced); @@ -2825,9 +2832,17 @@ bool Chainstate::FlushStateToDisk( m_next_write = FastRandomContext().rand_uniform_delay(NodeClock::now() + DATABASE_WRITE_INTERVAL_MIN, range); } } - if (full_flush_completed && m_chainman.m_options.signals) { - // Update best block in wallet (so we can detect restored wallets). - m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(m_chain.Tip())); + if (full_flush_completed) { + if (m_chainman.m_options.signals) { + // Update best block in wallet (so we can detect restored wallets). + m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(m_chain.Tip())); + } + + if (!m_chainman.m_interrupt && m_chainman.m_chainstates.size() == 1) { // Skip AssumeUTXO + if (ShouldCompactChainstate(m_chainman.IsInitialBlockDownload())) { + CoinsDB().CompactFull(); + } + } } } catch (const std::runtime_error& e) { return FatalError(m_chainman.GetNotifications(), state, strprintf(_("System error while flushing: %s"), e.what()));