From 394e473d42ba1383dfec45a3eafa8a73a09dbe8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Mon, 8 Jun 2026 16:15:21 +0200 Subject: [PATCH] coins: compact chainstate in background Full chainstate compaction can take minutes on large databases. Move `CCoinsViewDB::CompactFull()` to a named `utxocompact` one-shot background thread so validation only schedules the work. When validation selects compaction after a full flush, the chainstate was just written and another write is less likely to be needed immediately. The coins view destructor waits for completion, and a mutex prevents compaction from using `m_db` while `ResizeCache()` replaces it. Co-authored-by: Andrew Toth --- src/test/coins_tests.cpp | 2 +- src/txdb.cpp | 34 ++++++++++++++++++++++++++++++---- src/txdb.h | 11 ++++++++--- src/validation.cpp | 6 ++++-- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp index 4c5ed748847..3487a1789da 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -1078,7 +1078,7 @@ BOOST_FIXTURE_TEST_CASE(coins_db_leveldb_layout, FlushTest) cache.Sync(); BOOST_CHECK_EQUAL(level2_files(base), 0); - WITH_LOCK(::cs_main, base.CompactFull()); + WITH_LOCK(::cs_main, return base.CompactFull()).wait(); BOOST_CHECK_EQUAL(level2_files(base), 1); BOOST_CHECK(*Assert(base.GetCoin(outpoint)) == coin); diff --git a/src/txdb.cpp b/src/txdb.cpp index 62dc49a5f81..632c554cd03 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -14,10 +14,14 @@ #include #include #include +#include #include #include +#include #include +#include +#include #include #include @@ -56,11 +60,22 @@ CCoinsViewDB::CCoinsViewDB(DBParams db_params, CoinsViewOptions options) : m_options{std::move(options)}, m_db{std::make_unique(m_db_params)} { } +CCoinsViewDB::~CCoinsViewDB() +{ + if (m_compaction.valid()) { + if (m_compaction.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + LogInfo("Waiting for background chainstate compaction of %s", fs::PathToString(m_db_params.path)); + } + m_compaction.wait(); + } +} + void CCoinsViewDB::ResizeCache(size_t new_cache_size) { // We can't do this operation with an in-memory DB since we'll lose all the coins upon // reset. if (!m_db_params.memory_only) { + LOCK(m_db_mutex); // Have to do a reset first to get the original `m_db` state to release its // filesystem lock. m_db.reset(); @@ -180,12 +195,23 @@ std::optional CCoinsViewDB::GetDBProperty(const std::string& proper return m_db->GetProperty(property); } -void CCoinsViewDB::CompactFull() +std::shared_future 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)); + if (m_compaction.valid() && m_compaction.wait_for(std::chrono::seconds{0}) != std::future_status::ready) return m_compaction; + m_compaction = std::async(std::launch::async, [this] { + try { + util::ThreadRename("utxocompact"); + LOCK(m_db_mutex); + + 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)); + } catch (const std::exception& e) { + LogWarning("Failed chainstate compaction (%s)", e.what()); + } + }).share(); + return m_compaction; } /** Specialization of CCoinsViewCursor to iterate over a CCoinsViewDB */ diff --git a/src/txdb.h b/src/txdb.h index 648adc85e55..e89864213e0 100644 --- a/src/txdb.h +++ b/src/txdb.h @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -37,9 +38,13 @@ class CCoinsViewDB final : public CCoinsView protected: DBParams m_db_params; CoinsViewOptions m_options; + //! Prevents CompactFull() from using m_db while ResizeCache() replaces it. + Mutex m_db_mutex; std::unique_ptr m_db; + std::shared_future m_compaction; public: explicit CCoinsViewDB(DBParams db_params, CoinsViewOptions options); + ~CCoinsViewDB() override; std::optional GetCoin(const COutPoint& outpoint) const override; std::optional PeekCoin(const COutPoint& outpoint) const override; @@ -54,10 +59,10 @@ public: size_t EstimateSize() const override; //! Dynamically alter the underlying leveldb cache size. - void ResizeCache(size_t new_cache_size) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + void ResizeCache(size_t new_cache_size) EXCLUSIVE_LOCKS_REQUIRED(cs_main, !m_db_mutex); - //! Perform a blocking full compaction of the underlying LevelDB. - void CompactFull() EXCLUSIVE_LOCKS_REQUIRED(cs_main); + //! Perform a full compaction of the underlying LevelDB on a one-shot background thread. + std::shared_future CompactFull() EXCLUSIVE_LOCKS_REQUIRED(cs_main, !m_db_mutex); //! 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 df4bd8673cf..0836b3073c3 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2838,9 +2838,11 @@ bool Chainstate::FlushStateToDisk( 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())) { + if (!m_chainman.m_interrupt && ShouldCompactChainstate(m_chainman.IsInitialBlockDownload())) { + try { CoinsDB().CompactFull(); + } catch (const std::exception& e) { + LogWarning("Failed to start chainstate compaction (%s)", e.what()); } } }