diff options
author | Ava Chow <github@achow101.com> | 2024-02-13 12:58:16 -0500 |
---|---|---|
committer | Ava Chow <github@achow101.com> | 2024-02-13 13:08:30 -0500 |
commit | 128b4a80387d322a7810d8716eccdac95ff9b8cd (patch) | |
tree | d51c8877c078fa1cace99c8b4686edb9a93c1446 | |
parent | d7dabdbfcd4de560bb7a2ba1d326999c0e3c249f (diff) | |
parent | 77331aa2a198b708372a5c6cdf331faf7e2968ef (diff) |
Merge bitcoin/bitcoin#29403: wallet: batch erase procedures and improve 'EraseRecords' performance
77331aa2a198b708372a5c6cdf331faf7e2968ef wallet: simplify EraseRecords by using 'ErasePrefix' (furszy)
33757814ceb04102141d3fd5ef2f87abf0422310 wallet: bdb batch 'ErasePrefix', do not create txn internally (furszy)
cf4d72a75e9603e947b3d47e1f3ac7c7f37bb401 wallet: db, introduce 'RunWithinTxn()' helper function (furszy)
Pull request description:
Seeks to optimize and simplify `WalletBatch::EraseRecords`. Currently, this process opens a cursor to iterate over the entire database, searching for records that match the type prefixes, to then call the `WalletBatch::Erase` function for each of the matching records.
This PR rewrites this 40-line manual process into a single line; instead of performing all of those actions manually, we can simply utilize the `ErasePrefix()` functionality. The result is 06216b344dea6ad6c385fda0b37808ff9ae5273b.
Moreover, it expands the test coverage for the `ErasePrefix` functionality and documents the db txn requirement for `BerkeleyBatch::ErasePrefix` .
ACKs for top commit:
achow101:
reACK 77331aa2a198b708372a5c6cdf331faf7e2968ef
josibake:
code review ACK https://github.com/bitcoin/bitcoin/pull/29403/commits/77331aa2a198b708372a5c6cdf331faf7e2968ef
Tree-SHA512: 9f78dda658677ff19b5979ba0efd11cf9fabf3d315feb79ed1160526f010fe843c41903fc18c0b092f78aa88bc874cf24edad8fc1ea6e96aabdc4fd1daf21ca5
-rw-r--r-- | src/wallet/bdb.cpp | 9 | ||||
-rw-r--r-- | src/wallet/test/db_tests.cpp | 33 | ||||
-rw-r--r-- | src/wallet/test/wallet_tests.cpp | 8 | ||||
-rw-r--r-- | src/wallet/wallet.cpp | 5 | ||||
-rw-r--r-- | src/wallet/walletdb.cpp | 75 | ||||
-rw-r--r-- | src/wallet/walletdb.h | 14 |
6 files changed, 96 insertions, 48 deletions
diff --git a/src/wallet/bdb.cpp b/src/wallet/bdb.cpp index cbf6c9b1ea..38cca32f80 100644 --- a/src/wallet/bdb.cpp +++ b/src/wallet/bdb.cpp @@ -887,7 +887,12 @@ bool BerkeleyBatch::HasKey(DataStream&& key) bool BerkeleyBatch::ErasePrefix(Span<const std::byte> prefix) { - if (!TxnBegin()) return false; + // Because this function erases records one by one, ensure that it is executed within a txn context. + // Otherwise, consistency is at risk; it's possible that certain records are removed while others + // remain due to an internal failure during the procedure. + // Additionally, the Dbc::del() cursor delete call below would fail without an active transaction. + if (!Assume(activeTxn)) return false; + auto cursor{std::make_unique<BerkeleyCursor>(m_database, *this)}; // const_cast is safe below even though prefix_key is an in/out parameter, // because we are not using the DB_DBT_USERMEM flag, so BDB will allocate @@ -901,7 +906,7 @@ bool BerkeleyBatch::ErasePrefix(Span<const std::byte> prefix) ret = cursor->dbc()->del(0); } cursor.reset(); - return TxnCommit() && (ret == 0 || ret == DB_NOTFOUND); + return ret == 0 || ret == DB_NOTFOUND; } void BerkeleyDatabase::AddRef() diff --git a/src/wallet/test/db_tests.cpp b/src/wallet/test/db_tests.cpp index adbbb94318..c933366354 100644 --- a/src/wallet/test/db_tests.cpp +++ b/src/wallet/test/db_tests.cpp @@ -228,6 +228,39 @@ BOOST_AUTO_TEST_CASE(db_availability_after_write_error) } } +// Verify 'ErasePrefix' functionality using db keys similar to the ones used by the wallet. +// Keys are in the form of std::pair<TYPE, ENTRY_ID> +BOOST_AUTO_TEST_CASE(erase_prefix) +{ + const std::string key = "key"; + const std::string key2 = "key2"; + const std::string value = "value"; + const std::string value2 = "value_2"; + auto make_key = [](std::string type, std::string id) { return std::make_pair(type, id); }; + + for (const auto& database : TestDatabases(m_path_root)) { + std::unique_ptr<DatabaseBatch> batch = database->MakeBatch(); + + // Write two entries with the same key type prefix, a third one with a different prefix + // and a fourth one with the type-id values inverted + BOOST_CHECK(batch->Write(make_key(key, value), value)); + BOOST_CHECK(batch->Write(make_key(key, value2), value2)); + BOOST_CHECK(batch->Write(make_key(key2, value), value)); + BOOST_CHECK(batch->Write(make_key(value, key), value)); + + // Erase the ones with the same prefix and verify result + BOOST_CHECK(batch->TxnBegin()); + BOOST_CHECK(batch->ErasePrefix(DataStream() << key)); + BOOST_CHECK(batch->TxnCommit()); + + BOOST_CHECK(!batch->Exists(make_key(key, value))); + BOOST_CHECK(!batch->Exists(make_key(key, value2))); + // Also verify that entries with a different prefix were not erased + BOOST_CHECK(batch->Exists(make_key(key2, value))); + BOOST_CHECK(batch->Exists(make_key(value, key))); + } +} + #ifdef USE_SQLITE // Test-only statement execution error diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 7396a43c50..27a81b3669 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -445,9 +445,11 @@ BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup) auto requests = wallet->GetAddressReceiveRequests(); auto erequests = {"val_rr11", "val_rr20"}; BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests)); - WalletBatch batch{wallet->GetDatabase()}; - BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false)); - BOOST_CHECK(batch.EraseAddressData(ScriptHash())); + RunWithinTxn(wallet->GetDatabase(), /*process_desc*/"test", [](WalletBatch& batch){ + BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false)); + BOOST_CHECK(batch.EraseAddressData(ScriptHash())); + return true; + }); }); TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) { BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash())); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 11203ca3a4..db5f54cbb7 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2411,8 +2411,9 @@ bool CWallet::SetAddressBook(const CTxDestination& address, const std::string& s bool CWallet::DelAddressBook(const CTxDestination& address) { - WalletBatch batch(GetDatabase()); - return DelAddressBookWithDB(batch, address); + return RunWithinTxn(GetDatabase(), /*process_desc=*/"address book entry removal", [&](WalletBatch& batch){ + return DelAddressBookWithDB(batch, address); + }); } bool CWallet::DelAddressBookWithDB(WalletBatch& batch, const CTxDestination& address) diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 6e37bc2930..45c3e90220 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -1230,6 +1230,35 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet) return result; } +static bool RunWithinTxn(WalletBatch& batch, std::string_view process_desc, const std::function<bool(WalletBatch&)>& func) +{ + if (!batch.TxnBegin()) { + LogPrint(BCLog::WALLETDB, "Error: cannot create db txn for %s\n", process_desc); + return false; + } + + // Run procedure + if (!func(batch)) { + LogPrint(BCLog::WALLETDB, "Error: %s failed\n", process_desc); + batch.TxnAbort(); + return false; + } + + if (!batch.TxnCommit()) { + LogPrint(BCLog::WALLETDB, "Error: cannot commit db txn for %s\n", process_desc); + return false; + } + + // All good + return true; +} + +bool RunWithinTxn(WalletDatabase& database, std::string_view process_desc, const std::function<bool(WalletBatch&)>& func) +{ + WalletBatch batch(database); + return RunWithinTxn(batch, process_desc, func); +} + void MaybeCompactWalletDB(WalletContext& context) { static std::atomic<bool> fOneThread(false); @@ -1292,47 +1321,11 @@ bool WalletBatch::WriteWalletFlags(const uint64_t flags) bool WalletBatch::EraseRecords(const std::unordered_set<std::string>& types) { - // Begin db txn - if (!m_batch->TxnBegin()) return false; - - // Get cursor - std::unique_ptr<DatabaseCursor> cursor = m_batch->GetNewCursor(); - if (!cursor) - { - return false; - } - - // Iterate the DB and look for any records that have the type prefixes - while (true) { - // Read next record - DataStream key{}; - DataStream value{}; - DatabaseCursor::Status status = cursor->Next(key, value); - if (status == DatabaseCursor::Status::DONE) { - break; - } else if (status == DatabaseCursor::Status::FAIL) { - cursor.reset(nullptr); - m_batch->TxnAbort(); // abort db txn - return false; - } - - // Make a copy of key to avoid data being deleted by the following read of the type - const SerializeData key_data{key.begin(), key.end()}; - - std::string type; - key >> type; - - if (types.count(type) > 0) { - if (!m_batch->Erase(Span{key_data})) { - cursor.reset(nullptr); - m_batch->TxnAbort(); - return false; // erase failed - } - } - } - // Finish db txn - cursor.reset(nullptr); - return m_batch->TxnCommit(); + return RunWithinTxn(*this, "erase records", [&types](WalletBatch& self) { + return std::all_of(types.begin(), types.end(), [&self](const std::string& type) { + return self.m_batch->ErasePrefix(DataStream() << type); + }); + }); } bool WalletBatch::TxnBegin() diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 62449eb64e..9474a59660 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -294,6 +294,20 @@ private: WalletDatabase& m_database; }; +/** + * Executes the provided function 'func' within a database transaction context. + * + * This function ensures that all db modifications performed within 'func()' are + * atomically committed to the db at the end of the process. And, in case of a + * failure during execution, all performed changes are rolled back. + * + * @param database The db connection instance to perform the transaction on. + * @param process_desc A description of the process being executed, used for logging purposes in the event of a failure. + * @param func The function to be executed within the db txn context. It returns a boolean indicating whether to commit or roll back the txn. + * @return true if the db txn executed successfully, false otherwise. + */ +bool RunWithinTxn(WalletDatabase& database, std::string_view process_desc, const std::function<bool(WalletBatch&)>& func); + //! Compacts BDB state so that wallet.dat is self-contained (if there are changes) void MaybeCompactWalletDB(WalletContext& context); |