Skip to content

Commit 128b4a8

Browse files
committed
Merge bitcoin/bitcoin#29403: wallet: batch erase procedures and improve 'EraseRecords' performance
77331aa wallet: simplify EraseRecords by using 'ErasePrefix' (furszy) 3375781 wallet: bdb batch 'ErasePrefix', do not create txn internally (furszy) cf4d72a 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 77331aa josibake: code review ACK bitcoin/bitcoin@77331aa Tree-SHA512: 9f78dda658677ff19b5979ba0efd11cf9fabf3d315feb79ed1160526f010fe843c41903fc18c0b092f78aa88bc874cf24edad8fc1ea6e96aabdc4fd1daf21ca5
2 parents d7dabdb + 77331aa commit 128b4a8

File tree

6 files changed

+96
-48
lines changed

6 files changed

+96
-48
lines changed

src/wallet/bdb.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,12 @@ bool BerkeleyBatch::HasKey(DataStream&& key)
887887

888888
bool BerkeleyBatch::ErasePrefix(Span<const std::byte> prefix)
889889
{
890-
if (!TxnBegin()) return false;
890+
// Because this function erases records one by one, ensure that it is executed within a txn context.
891+
// Otherwise, consistency is at risk; it's possible that certain records are removed while others
892+
// remain due to an internal failure during the procedure.
893+
// Additionally, the Dbc::del() cursor delete call below would fail without an active transaction.
894+
if (!Assume(activeTxn)) return false;
895+
891896
auto cursor{std::make_unique<BerkeleyCursor>(m_database, *this)};
892897
// const_cast is safe below even though prefix_key is an in/out parameter,
893898
// 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)
901906
ret = cursor->dbc()->del(0);
902907
}
903908
cursor.reset();
904-
return TxnCommit() && (ret == 0 || ret == DB_NOTFOUND);
909+
return ret == 0 || ret == DB_NOTFOUND;
905910
}
906911

907912
void BerkeleyDatabase::AddRef()

src/wallet/test/db_tests.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,39 @@ BOOST_AUTO_TEST_CASE(db_availability_after_write_error)
228228
}
229229
}
230230

231+
// Verify 'ErasePrefix' functionality using db keys similar to the ones used by the wallet.
232+
// Keys are in the form of std::pair<TYPE, ENTRY_ID>
233+
BOOST_AUTO_TEST_CASE(erase_prefix)
234+
{
235+
const std::string key = "key";
236+
const std::string key2 = "key2";
237+
const std::string value = "value";
238+
const std::string value2 = "value_2";
239+
auto make_key = [](std::string type, std::string id) { return std::make_pair(type, id); };
240+
241+
for (const auto& database : TestDatabases(m_path_root)) {
242+
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
243+
244+
// Write two entries with the same key type prefix, a third one with a different prefix
245+
// and a fourth one with the type-id values inverted
246+
BOOST_CHECK(batch->Write(make_key(key, value), value));
247+
BOOST_CHECK(batch->Write(make_key(key, value2), value2));
248+
BOOST_CHECK(batch->Write(make_key(key2, value), value));
249+
BOOST_CHECK(batch->Write(make_key(value, key), value));
250+
251+
// Erase the ones with the same prefix and verify result
252+
BOOST_CHECK(batch->TxnBegin());
253+
BOOST_CHECK(batch->ErasePrefix(DataStream() << key));
254+
BOOST_CHECK(batch->TxnCommit());
255+
256+
BOOST_CHECK(!batch->Exists(make_key(key, value)));
257+
BOOST_CHECK(!batch->Exists(make_key(key, value2)));
258+
// Also verify that entries with a different prefix were not erased
259+
BOOST_CHECK(batch->Exists(make_key(key2, value)));
260+
BOOST_CHECK(batch->Exists(make_key(value, key)));
261+
}
262+
}
263+
231264
#ifdef USE_SQLITE
232265

233266
// Test-only statement execution error

src/wallet/test/wallet_tests.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,9 +445,11 @@ BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup)
445445
auto requests = wallet->GetAddressReceiveRequests();
446446
auto erequests = {"val_rr11", "val_rr20"};
447447
BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
448-
WalletBatch batch{wallet->GetDatabase()};
449-
BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false));
450-
BOOST_CHECK(batch.EraseAddressData(ScriptHash()));
448+
RunWithinTxn(wallet->GetDatabase(), /*process_desc*/"test", [](WalletBatch& batch){
449+
BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false));
450+
BOOST_CHECK(batch.EraseAddressData(ScriptHash()));
451+
return true;
452+
});
451453
});
452454
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
453455
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));

src/wallet/wallet.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2411,8 +2411,9 @@ bool CWallet::SetAddressBook(const CTxDestination& address, const std::string& s
24112411

24122412
bool CWallet::DelAddressBook(const CTxDestination& address)
24132413
{
2414-
WalletBatch batch(GetDatabase());
2415-
return DelAddressBookWithDB(batch, address);
2414+
return RunWithinTxn(GetDatabase(), /*process_desc=*/"address book entry removal", [&](WalletBatch& batch){
2415+
return DelAddressBookWithDB(batch, address);
2416+
});
24162417
}
24172418

24182419
bool CWallet::DelAddressBookWithDB(WalletBatch& batch, const CTxDestination& address)

src/wallet/walletdb.cpp

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,35 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
12301230
return result;
12311231
}
12321232

1233+
static bool RunWithinTxn(WalletBatch& batch, std::string_view process_desc, const std::function<bool(WalletBatch&)>& func)
1234+
{
1235+
if (!batch.TxnBegin()) {
1236+
LogPrint(BCLog::WALLETDB, "Error: cannot create db txn for %s\n", process_desc);
1237+
return false;
1238+
}
1239+
1240+
// Run procedure
1241+
if (!func(batch)) {
1242+
LogPrint(BCLog::WALLETDB, "Error: %s failed\n", process_desc);
1243+
batch.TxnAbort();
1244+
return false;
1245+
}
1246+
1247+
if (!batch.TxnCommit()) {
1248+
LogPrint(BCLog::WALLETDB, "Error: cannot commit db txn for %s\n", process_desc);
1249+
return false;
1250+
}
1251+
1252+
// All good
1253+
return true;
1254+
}
1255+
1256+
bool RunWithinTxn(WalletDatabase& database, std::string_view process_desc, const std::function<bool(WalletBatch&)>& func)
1257+
{
1258+
WalletBatch batch(database);
1259+
return RunWithinTxn(batch, process_desc, func);
1260+
}
1261+
12331262
void MaybeCompactWalletDB(WalletContext& context)
12341263
{
12351264
static std::atomic<bool> fOneThread(false);
@@ -1292,47 +1321,11 @@ bool WalletBatch::WriteWalletFlags(const uint64_t flags)
12921321

12931322
bool WalletBatch::EraseRecords(const std::unordered_set<std::string>& types)
12941323
{
1295-
// Begin db txn
1296-
if (!m_batch->TxnBegin()) return false;
1297-
1298-
// Get cursor
1299-
std::unique_ptr<DatabaseCursor> cursor = m_batch->GetNewCursor();
1300-
if (!cursor)
1301-
{
1302-
return false;
1303-
}
1304-
1305-
// Iterate the DB and look for any records that have the type prefixes
1306-
while (true) {
1307-
// Read next record
1308-
DataStream key{};
1309-
DataStream value{};
1310-
DatabaseCursor::Status status = cursor->Next(key, value);
1311-
if (status == DatabaseCursor::Status::DONE) {
1312-
break;
1313-
} else if (status == DatabaseCursor::Status::FAIL) {
1314-
cursor.reset(nullptr);
1315-
m_batch->TxnAbort(); // abort db txn
1316-
return false;
1317-
}
1318-
1319-
// Make a copy of key to avoid data being deleted by the following read of the type
1320-
const SerializeData key_data{key.begin(), key.end()};
1321-
1322-
std::string type;
1323-
key >> type;
1324-
1325-
if (types.count(type) > 0) {
1326-
if (!m_batch->Erase(Span{key_data})) {
1327-
cursor.reset(nullptr);
1328-
m_batch->TxnAbort();
1329-
return false; // erase failed
1330-
}
1331-
}
1332-
}
1333-
// Finish db txn
1334-
cursor.reset(nullptr);
1335-
return m_batch->TxnCommit();
1324+
return RunWithinTxn(*this, "erase records", [&types](WalletBatch& self) {
1325+
return std::all_of(types.begin(), types.end(), [&self](const std::string& type) {
1326+
return self.m_batch->ErasePrefix(DataStream() << type);
1327+
});
1328+
});
13361329
}
13371330

13381331
bool WalletBatch::TxnBegin()

src/wallet/walletdb.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,20 @@ class WalletBatch
294294
WalletDatabase& m_database;
295295
};
296296

297+
/**
298+
* Executes the provided function 'func' within a database transaction context.
299+
*
300+
* This function ensures that all db modifications performed within 'func()' are
301+
* atomically committed to the db at the end of the process. And, in case of a
302+
* failure during execution, all performed changes are rolled back.
303+
*
304+
* @param database The db connection instance to perform the transaction on.
305+
* @param process_desc A description of the process being executed, used for logging purposes in the event of a failure.
306+
* @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.
307+
* @return true if the db txn executed successfully, false otherwise.
308+
*/
309+
bool RunWithinTxn(WalletDatabase& database, std::string_view process_desc, const std::function<bool(WalletBatch&)>& func);
310+
297311
//! Compacts BDB state so that wallet.dat is self-contained (if there are changes)
298312
void MaybeCompactWalletDB(WalletContext& context);
299313

0 commit comments

Comments
 (0)