From 1907b3f56caefa9da555f3182f8094711d765e06 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 Github-Pull: #35465 Rebased-From: 394e473d42ba1383dfec45a3eafa8a73a09dbe8b --- 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 b43be32b73..1780e97e65 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -1067,7 +1067,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 50d7e3248e..11b0724a9a 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -12,10 +12,14 @@ #include #include #include +#include #include #include +#include #include +#include +#include #include #include @@ -51,11 +55,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(); @@ -161,12 +176,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 4db61187f0..8944fbc1ac 100644 --- a/src/txdb.h +++ b/src/txdb.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -40,9 +41,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; bool HaveCoin(const COutPoint &outpoint) const override; @@ -56,13 +61,13 @@ 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); //! @returns filesystem path to on-disk storage or std::nullopt if in memory. std::optional StoragePath() { return m_db->StoragePath(); } - //! 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 0341eabf72..ff281bc1a6 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2952,9 +2952,11 @@ bool Chainstate::FlushStateToDisk( m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), m_chain.GetLocator()); } - if (!m_chainman.m_interrupt && m_chainman.GetAll().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()); } } }