Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wallet: batch erase procedures and improve 'EraseRecords' performance #29403

Merged
merged 3 commits into from Feb 13, 2024

Conversation

furszy
Copy link
Member

@furszy furszy commented Feb 7, 2024

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 06216b3.

Moreover, it expands the test coverage for the ErasePrefix functionality and documents the db txn requirement for BerkeleyBatch::ErasePrefix .

@DrahtBot
Copy link
Contributor

DrahtBot commented Feb 7, 2024

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK achow101, josibake
Stale ACK ryanofsky

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

No conflicts as of last run.

@DrahtBot DrahtBot added the Wallet label Feb 7, 2024
src/wallet/wallet.cpp Outdated Show resolved Hide resolved
@furszy
Copy link
Member Author

furszy commented Feb 7, 2024

Updated per feedback. Thanks Marko.

@furszy furszy force-pushed the 2024_wallet_simplify_EraseRecords branch from b7b5e5b to 69c5bb5 Compare February 7, 2024 14:18
@DrahtBot
Copy link
Contributor

DrahtBot commented Feb 7, 2024

🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.

Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.

Leave a comment here, if you need help tracking down a confusing failure.

Debug: https://github.com/bitcoin/bitcoin/runs/21321086502

@furszy
Copy link
Member Author

furszy commented Feb 7, 2024

Green CI. Ready.

@DrahtBot DrahtBot removed the CI failed label Feb 7, 2024
Copy link
Member

@achow101 achow101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 69c5bb5

src/wallet/wallet.cpp Outdated Show resolved Hide resolved
src/wallet/bdb.cpp Outdated Show resolved Hide resolved
Copy link
Member Author

@furszy furszy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated per feedback. Thanks achow!

src/wallet/wallet.cpp Outdated Show resolved Hide resolved
src/wallet/bdb.cpp Outdated Show resolved Hide resolved
@furszy furszy force-pushed the 2024_wallet_simplify_EraseRecords branch from 0fd7b0d to 784034f Compare February 8, 2024 14:11
Copy link
Member Author

@furszy furszy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated per feedback. Thanks achow.
Small diff, only changed assert for Assume.

@furszy furszy force-pushed the 2024_wallet_simplify_EraseRecords branch 2 times, most recently from d6bf468 to 757d651 Compare February 8, 2024 16:16
Copy link
Member Author

@furszy furszy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased after #26836 merge. Ready to go.

Comment on lines +252 to +254
BOOST_CHECK(batch->TxnBegin());
BOOST_CHECK(batch->ErasePrefix(DataStream() << key));
BOOST_CHECK(batch->TxnCommit());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 5235257 "wallet: bdb batch 'ErasePrefix', do not create txn internally"

nit: Could use RunWithinTxn().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 5235257 "wallet: bdb batch 'ErasePrefix', do not create txn internally"

nit: Could use RunWithinTxn().

I did not used it because it needs walletdb.h dependency (RunWithinTxn provides access to WalletBatch). And this tests (db_tests.cpp) run on a lower level, using DatabaseBatch.

src/wallet/test/wallet_tests.cpp Outdated Show resolved Hide resolved
Comment on lines +890 to +892
// 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 5235257 "wallet: bdb batch 'ErasePrefix', do not create txn internally"

I think it would be relevant to mention that BDB returns errors on del() if it is not in a transaction.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be relevant to mention that BDB returns errors on del() if it is not in a transaction.

Sure, we could mention that the read-write operation is currently disallowed by our current bdb configurations.

Still, JFYI, I fixed it locally by subdividing the read-write operations. However, since we currently ensure that all callers start a transaction, and this division adds an extra loop over the matching entries (see furszy@4e7b5c0), I didn't see it as worth pushing it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #29403 (comment)

In commit "wallet: bdb batch 'ErasePrefix', do not create txn internally" (96eb527)

The new comment "Additionally, read-write operations performed outside a db txn context are disallowed by current BDB configurations" seems vague to me and I don't think would be comprehensible without reading this review thread. Would suggest just using achow's comment or combining both comments with something like "The Dbc::del() cursor delete call below would fail without an active transaction.".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #29403 (comment)

In commit "wallet: bdb batch 'ErasePrefix', do not create txn internally" (96eb527)

The new comment "Additionally, read-write operations performed outside a db txn context are disallowed by current BDB configurations" seems vague to me and I don't think would be comprehensible without reading this review thread. Would suggest just using achow's comment or combining both comments with something like "The Dbc::del() cursor delete call below would fail without an active transaction.".

I wrote it in this way to (try to) inform readers about current bdb config limitations in case anyone changes this code and doesn't understand why it fails. But, I agree that it is not immediately clear. Will change it for you line.
Hopefully no one else will change this again.

@@ -1337,6 +1336,12 @@ bool RunWithinTxn(WalletDatabase& database, const std::string& process_desc, con
return true;
}

bool RunWithinTxn(WalletDatabase& database, const std::string& process_desc, const std::function<bool(WalletBatch&)>& func)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 757d651 "wallet: simplify EraseRecords by using 'ErasePrefix'"

This overload feels unnecessary given that there are only 2 places this function is really used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 757d651 "wallet: simplify EraseRecords by using 'ErasePrefix'"

This overload feels unnecessary given that there are only 2 places this function is really used.

The idea is to use this function as a top-level wrapper for wallet functions and scope the other function for internal walletdb.cpp usage mostly/only. This approach avoids creating WalletBatch objects across the sources and makes use of the error-handling code in the WalletBatch destructor.
Functions such as DescriptorScriptPubKeyMan::TopUp, the two CWallet::SetupDescriptorScriptPubKeyMans, and the final state of the batched migration process can utilize this overload in a follow-up.

@@ -1314,9 +1314,8 @@ DBErrors WalletBatch::ZapSelectTx(std::vector<uint256>& vTxHashIn, std::vector<u
return DBErrors::LOAD_OK;
}

bool RunWithinTxn(WalletDatabase& database, const std::string& process_desc, const std::function<bool(WalletBatch&)>& func)
static bool RunWithinTxn(WalletBatch& batch, const std::string& process_desc, const std::function<bool(WalletBatch&)>& func)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 757d651 "wallet: simplify EraseRecords by using 'ErasePrefix'"

This could be done in the commit introducing RunWithinTxn() rather than changing it in a later commit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done in the commit introducing RunWithinTxn() rather than changing it in a later commit.

This is merely an organizational discussion, but.. I'm not a big fan of introducing a function overload in a commit that does not utilizes it. Yet, if you have a strong opinion about it, I could move this to the first commit. np.

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Light code review ACK 757d651, nice simplification of EraseRecords

src/wallet/walletdb.cpp Outdated Show resolved Hide resolved
Copy link
Member Author

@furszy furszy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated per feedback. Thanks both!

Comment on lines +890 to +892
// 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.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be relevant to mention that BDB returns errors on del() if it is not in a transaction.

Sure, we could mention that the read-write operation is currently disallowed by our current bdb configurations.

Still, JFYI, I fixed it locally by subdividing the read-write operations. However, since we currently ensure that all callers start a transaction, and this division adds an extra loop over the matching entries (see furszy@4e7b5c0), I didn't see it as worth pushing it.

Comment on lines +252 to +254
BOOST_CHECK(batch->TxnBegin());
BOOST_CHECK(batch->ErasePrefix(DataStream() << key));
BOOST_CHECK(batch->TxnCommit());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 5235257 "wallet: bdb batch 'ErasePrefix', do not create txn internally"

nit: Could use RunWithinTxn().

I did not used it because it needs walletdb.h dependency (RunWithinTxn provides access to WalletBatch). And this tests (db_tests.cpp) run on a lower level, using DatabaseBatch.

@@ -1337,6 +1336,12 @@ bool RunWithinTxn(WalletDatabase& database, const std::string& process_desc, con
return true;
}

bool RunWithinTxn(WalletDatabase& database, const std::string& process_desc, const std::function<bool(WalletBatch&)>& func)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 757d651 "wallet: simplify EraseRecords by using 'ErasePrefix'"

This overload feels unnecessary given that there are only 2 places this function is really used.

The idea is to use this function as a top-level wrapper for wallet functions and scope the other function for internal walletdb.cpp usage mostly/only. This approach avoids creating WalletBatch objects across the sources and makes use of the error-handling code in the WalletBatch destructor.
Functions such as DescriptorScriptPubKeyMan::TopUp, the two CWallet::SetupDescriptorScriptPubKeyMans, and the final state of the batched migration process can utilize this overload in a follow-up.

@@ -1314,9 +1314,8 @@ DBErrors WalletBatch::ZapSelectTx(std::vector<uint256>& vTxHashIn, std::vector<u
return DBErrors::LOAD_OK;
}

bool RunWithinTxn(WalletDatabase& database, const std::string& process_desc, const std::function<bool(WalletBatch&)>& func)
static bool RunWithinTxn(WalletBatch& batch, const std::string& process_desc, const std::function<bool(WalletBatch&)>& func)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done in the commit introducing RunWithinTxn() rather than changing it in a later commit.

This is merely an organizational discussion, but.. I'm not a big fan of introducing a function overload in a commit that does not utilizes it. Yet, if you have a strong opinion about it, I could move this to the first commit. np.

@achow101
Copy link
Member

achow101 commented Feb 9, 2024

ACK 8330145

@DrahtBot DrahtBot requested review from ryanofsky and removed request for achow101 February 9, 2024 18:41
Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK 8330145. Looks pretty good, but would suggest cleaning up some things


// Run procedure, errors are logged internally
if (!func(batch)) {
batch.TxnAbort();
Copy link
Contributor

@ryanofsky ryanofsky Feb 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "wallet: db, introduce 'RunWithinTxn()' helper function" (b6dcfca)

I think it's bad for debugging that this function inconsistently logs errors when an operation fails. For example it seems like there is no error if the new eraserecords function fails? It would be better to add LogPrint(BCLog::WALLETDB, "Error: %s failed\n", process_desc); here.

I understand that in the future there may be cases where we may want to abort without logging errors but I think a better way to handle those cases would be to accept kernel::Interrupted return values here and use the kernel::IsInterrupted() helper. (We can move these from kernel to util without breaking compatibility.)

The comment above on line 1325 "errors are logged internally" is also not accurate and impossible to enforce with an anonymous callback, so would suggest dropping that and logging unconditionally here. (Without logging, the most the comment could say would be "errors need to be logged by func callback" or "errors should be logged by func callback")

Copy link
Member Author

@furszy furszy Feb 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, using util::Interrupted to skip the failure log in the future sounds great. Error logging added.

src/wallet/walletdb.h Outdated Show resolved Hide resolved
Comment on lines +890 to +892
// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #29403 (comment)

In commit "wallet: bdb batch 'ErasePrefix', do not create txn internally" (96eb527)

The new comment "Additionally, read-write operations performed outside a db txn context are disallowed by current BDB configurations" seems vague to me and I don't think would be comprehensible without reading this review thread. Would suggest just using achow's comment or combining both comments with something like "The Dbc::del() cursor delete call below would fail without an active transaction.".

@furszy furszy force-pushed the 2024_wallet_simplify_EraseRecords branch from 8330145 to 2095ccd Compare February 9, 2024 20:21
Copy link
Member Author

@furszy furszy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated per feedback. Thanks ryanofsky!. Small diff.
Per request, now RunWithinTxn logs an error when the provided function requires to abort the db txn. As well as two comments have been improved for clarity.

'RunWithinTxn()' provides a way to execute db operations within a
transactional context. It avoids writing repetitive boilerplate code for
starting and committing the database transaction.
Transactions are intended to be started on upper layers rather than
internally by the bdb batch object. This enables us to consolidate
different write operations within a procedure in the same db txn,
improving consistency due to the atomic property of the transaction,
as well as its performance due to the reduction of disk write
operations.

Important Note:
This approach also ensures that the BerkeleyBatch::ErasePrefix
function behaves exactly as the SQLiteBatch::ErasePrefix function,
which does not create a db txn internally.

Furthermore, since the `BerkeleyBatch::ErasePrefix' implementation
erases records one by one (by traversing the db), this change
ensures that the function is always called within an active txn
context. Without this measure, there's a potential risk to consistency;
certain records may be removed while others could persist due to an
internal failure during the procedure.
@achow101
Copy link
Member

reACK 77331aa

Copy link
Member

@josibake josibake left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code review ACK 77331aa

If you end up retouching, I think it's better to keep all of the RunWithinTxn changes in the first commit. Makes it easier to review and avoids introducing the function with one signature and then changing the signature in a later commit.

Comment on lines 1233 to 1235
static bool RunWithinTxn(WalletBatch& batch, std::string_view process_desc, const std::function<bool(WalletBatch&)>& func)
{
WalletBatch batch(database);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in "wallet: simplify EraseRecords by using 'ErasePrefix'" (77331aa):

nit: these changes should be in the first commit

src/wallet/walletdb.cpp Show resolved Hide resolved
@achow101 achow101 merged commit 128b4a8 into bitcoin:master Feb 13, 2024
16 checks passed
@furszy furszy deleted the 2024_wallet_simplify_EraseRecords branch February 13, 2024 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants