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
refactor: Add util::Result failure values, multiple error and warning messages #25665
base: master
Are you sure you want to change the base?
Conversation
Draft PR since I want to add a commit taking advantages of ability to return chain results and return warnings. |
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code CoverageFor detailed information about the code coverage, see the test coverage report. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated 7a4741e -> 28e2081 (pr/bresult2.52
-> pr/bresult2.53
, compare) just improving some comments
src/util/result.h
Outdated
if (dst && src) { | ||
// dst and src both hold success values, so merge them and return | ||
if constexpr (!std::is_same_v<typename SrcResult::SuccessType, void>) { | ||
ResultTraits<typename SrcResult::SuccessType>::MergeInto(*dst, *src); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re: #25665 (comment)
Reviewers might be wondering what the benefits of the ResultTraits are. They are useful for potential future extensions of the move behaviour: 3951afc .
Thanks I added a comment to the traits struct to make this clearer. Traits are an old pattern in c++ and are commonly used. The standard library has std::allocator_traits, std::iterator_traits, std::pointer_traits, std::hash, std::formatter, std::tuple_element, and std::tuple_size trait structs which allow generic library functions to work with user defined types. They all work the same way, by defining a template struct in the library that user code can specialize to control the behavior of library functions when called with custom types. This is an old explanation of traits which may be helpful: http://erdani.org/publications/traits.html
Converted to draft since first 2 commits were moved to a separate PR, #29906 Rebased 28e2081 -> 990f9d6 ( |
Posting my response to part of your comment on #29906 here, since
Had just a quick look, but this looks like how I'd expect
I don't think this is good usage of git diff on 1376583diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp
index 0671f75515..8cc43c53e5 100644
--- a/src/node/chainstate.cpp
+++ b/src/node/chainstate.cpp
@@ -40,7 +40,6 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
const CacheSizes& cache_sizes,
const ChainstateLoadOptions& options) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
{
- util::Result<InterruptResult, ChainstateLoadError> result;
auto& pblocktree{chainman.m_blockman.m_block_tree_db};
// new BlockTreeDB tries to delete the existing file, which
// fails if it's still open from the previous loop. Close it first:
@@ -61,8 +60,7 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
}
if (chainman.m_interrupt) {
- result.Update(Interrupted{});
- return result;
+ return Interrupted{};
}
// LoadBlockIndex will load m_have_pruned if we've ever removed a
@@ -71,26 +69,23 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
// From here on, fReindex and options.reindex values may be different!
if (!chainman.LoadBlockIndex()) {
if (chainman.m_interrupt) {
- result.Update(Interrupted{});
+ return Interrupted{};
} else {
- result.Update({util::Error{_("Error loading block database")}, ChainstateLoadError::FAILURE});
+ return {util::Error{_("Error loading block database")}, ChainstateLoadError::FAILURE};
}
- return result;
}
if (!chainman.BlockIndex().empty() &&
!chainman.m_blockman.LookupBlockIndex(chainman.GetConsensus().hashGenesisBlock)) {
// If the loaded chain has a wrong genesis, bail out immediately
// (we're likely using a testnet datadir, or the other way around).
- result.Update({util::Error{_("Incorrect or no genesis block found. Wrong datadir for network?")}, ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
- return result;
+ return {util::Error{_("Incorrect or no genesis block found. Wrong datadir for network?")}, ChainstateLoadError::FAILURE_INCOMPATIBLE_DB};
}
// Check for changed -prune state. What we are concerned about is a user who has pruned blocks
// in the past, but is now trying to run unpruned.
if (chainman.m_blockman.m_have_pruned && !options.prune) {
- result.Update({util::Error{_("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")}, ChainstateLoadError::FAILURE});
- return result;
+ return {util::Error{_("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")}, ChainstateLoadError::FAILURE};
}
// At this point blocktree args are consistent with what's on disk.
@@ -98,8 +93,7 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
// (otherwise we use the one already on disk).
// This is called again in ImportBlocks after the reindex completes.
if (!fReindex && !chainman.ActiveChainstate().LoadGenesisBlock()) {
- result.Update({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
- return result;
+ return {util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE};
}
auto is_coinsview_empty = [&](Chainstate* chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
@@ -133,17 +127,15 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
// Refuse to load unsupported database format.
// This is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
if (chainstate->CoinsDB().NeedsUpgrade()) {
- result.Update({util::Error{_("Unsupported chainstate database format found. "
+ return {util::Error{_("Unsupported chainstate database format found. "
"Please restart with -reindex-chainstate. This will "
"rebuild the chainstate database.")},
- ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
- return result;
+ ChainstateLoadError::FAILURE_INCOMPATIBLE_DB};
}
// ReplayBlocks is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
if (!chainstate->ReplayBlocks()) {
- result.Update({util::Error{_("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")}, ChainstateLoadError::FAILURE});
- return result;
+ return {util::Error{_("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")}, ChainstateLoadError::FAILURE};
}
// The on-disk coinsdb is now in a good state, create the cache
@@ -153,8 +145,7 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
if (!is_coinsview_empty(chainstate)) {
// LoadChainTip initializes the chain based on CoinsTip()'s best block
if (!chainstate->LoadChainTip()) {
- result.Update({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
- return result;
+ return {util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE};
}
assert(chainstate->m_chain.Tip() != nullptr);
}
@@ -164,9 +155,8 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
auto chainstates{chainman.GetAll()};
if (std::any_of(chainstates.begin(), chainstates.end(),
[](const Chainstate* cs) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { return cs->NeedsRedownload(); })) {
- result.Update({util::Error{strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
- chainman.GetConsensus().SegwitHeight)}, ChainstateLoadError::FAILURE});
- return result;
+ return {util::Error{strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
+ chainman.GetConsensus().SegwitHeight)}, ChainstateLoadError::FAILURE};
};
}
@@ -175,7 +165,7 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
// on the condition of each chainstate.
chainman.MaybeRebalanceCaches();
- return result;
+ return {};
}
util::Result<InterruptResult, ChainstateLoadError> LoadChainstate(ChainstateManager& chainman, const CacheSizes& cache_sizes,
|
re: #25665 (comment)
The diff you suggested is actually the way this function was implemented before #29700. The problem is that #29700 updates some validation functions to return If you think this pattern is not readable enough or too error prone, we could probably come up with an alternate pattern that doesn't require updating a variable (but may require copying around data more manually). In general, I'd agree that all other things being equal, it's better to initialize variables once than change them over time. But in this case, if the goal is to accumulate error and warning messages, I think a pattern where you declare a single variable at the top of the function holding the messages, and update the variable over the course of the function, and then return the variable at the end is a pretty simple and readable pattern. I'm sure other patterns could work too, though. I guess I'd want to know if there's another pattern you'd prefer, or what more specific problems you see with the current code. |
I see, that was not apparent in the link you shared earlier. Would it make sense to do that refactoring in #29700 then, instead of in this PR? Because in this PR, I don't think the change makes a lot of sense on its own.
I agree, I think that's a reasonable pattern. For this situation, I would prefer using two functions: Combining both would be suitable in this case, I think. Pseudo-code diff (since git diff on 4d2c9dediff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp
index 014f6915ad..bc8d547153 100644
--- a/src/node/chainstate.cpp
+++ b/src/node/chainstate.cpp
@@ -64,7 +64,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
}
if (chainman.m_interrupt) {
- result.Update(Interrupted{});
+ result.Set(Interrupted{});
return result;
}
@@ -85,14 +85,14 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
!chainman.m_blockman.LookupBlockIndex(chainman.GetConsensus().hashGenesisBlock)) {
// If the loaded chain has a wrong genesis, bail out immediately
// (we're likely using a testnet datadir, or the other way around).
- result.Update({util::Error{_("Incorrect or no genesis block found. Wrong datadir for network?")}, ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
+ result.Set({util::Error{_("Incorrect or no genesis block found. Wrong datadir for network?")}, ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
return result;
}
// Check for changed -prune state. What we are concerned about is a user who has pruned blocks
// in the past, but is now trying to run unpruned.
if (chainman.m_blockman.m_have_pruned && !options.prune) {
- result.Update({util::Error{_("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")}, ChainstateLoadError::FAILURE});
+ result.Set({util::Error{_("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")}, ChainstateLoadError::FAILURE});
return result;
}
@@ -101,7 +101,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
// (otherwise we use the one already on disk).
// This is called again in ImportBlocks after the reindex completes.
if (!fReindex && !chainman.ActiveChainstate().LoadGenesisBlock()) {
- result.Update({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
+ result.Set({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
return result;
}
@@ -136,7 +136,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
// Refuse to load unsupported database format.
// This is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
if (chainstate->CoinsDB().NeedsUpgrade()) {
- result.Update({util::Error{_("Unsupported chainstate database format found. "
+ result.Set({util::Error{_("Unsupported chainstate database format found. "
"Please restart with -reindex-chainstate. This will "
"rebuild the chainstate database.")},
ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
@@ -145,7 +145,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
// ReplayBlocks is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
if (!chainstate->ReplayBlocks()) {
- result.Update({util::Error{_("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")}, ChainstateLoadError::FAILURE});
+ result.Set({util::Error{_("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")}, ChainstateLoadError::FAILURE});
return result;
}
@@ -156,7 +156,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
if (!is_coinsview_empty(chainstate)) {
// LoadChainTip initializes the chain based on CoinsTip()'s best block
if (!chainstate->LoadChainTip()) {
- result.Update({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
+ result.Set({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
return result;
}
assert(chainstate->m_chain.Tip() != nullptr);
@@ -167,7 +167,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
auto chainstates{chainman.GetAll()};
if (std::any_of(chainstates.begin(), chainstates.end(),
[](const Chainstate* cs) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { return cs->NeedsRedownload(); })) {
- result.Update({util::Error{strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
+ result.Set({util::Error{strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
chainman.GetConsensus().SegwitHeight)}, ChainstateLoadError::FAILURE});
return result;
};
It's kind of a similar situation to having a I've ranted about this already a bit too long, so please do let me know if you (dis)agree but I'll stop commenting further until I've (re)-reviewed the rest of this PR and I maybe get new insights. |
6a8b2be refactor: Avoid copying util::Result values (Ryan Ofsky) 834f65e refactor: Drop util::Result operator= (Ryan Ofsky) Pull request description: This PR just contains the first two commits of #25665. It disables copying of `util::Result` objects because unnecessary copies are inefficient and not possible after #25665, which makes `util::Result` object move-only. It disables the assignment operator and replaces it with an `Update()` method, because #25665 adds more information to `util::Result` objects (warning and error messages and failure values) and having an assignment operator that overwrites data instead of merging it would make it easy to accidentally erase existing information while trying to assign new information. ACKs for top commit: stickies-v: re-ACK 6a8b2be achow101: ACK 6a8b2be furszy: re-ACK 6a8b2be Tree-SHA512: 3f21af9031d50d6c68cca69133de03080f69b1ddcf8b140bdeb762069f14645209b2586037236d15b6ebd8973af0fbefd7e83144aeb7b84078a4cb4df812f984
Add util::Result support for returning more error information. For better error handling, adds the ability to return a value on failure, not just a value on success. This is a key missing feature that made the result class not useful for functions like LoadChainstate() which produce different errors that need to be handled differently. This change also makes some more minor improvements: - Smaller type size. util::Result<int> is 16 bytes, and util::Result<void> is 8 bytes. Previously util::Result<int> and util::Result<void> were 72 bytes. - More documentation and tests.
Add util::Result Update method to make it possible to change the value of an existing Result object. This will be useful for functions that can return multiple error and warning messages, and want to be able to change the result value while keeping existing messages.
Add util::Result support for returning warning messages and multiple errors, not just a single error string. This provides a way for functions to report errors and warnings in a standard way, and simplifies interfaces. The functionality is unit tested here, and put to use in followup PR bitcoin#25722
Suggested by Martin Leitner-Ankerl <martin.ankerl@gmail.com> bitcoin#25722 (comment) Co-authored-by: Martin Leitner-Ankerl <martin.ankerl@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebased 0c8a1bb -> db91dbb (pr/bresult2.56
-> pr/bresult2.57
, compare) after #29906 was merged
re: #25665 (comment) Thanks @stickies-v. I think since On your latest feedback, I think I understand the scenario you are concerned about. It seems like you are worried about future uses of I think I might have agreed with that before writing #25722 and #29700. But after writing these PRs, I don't think a Basically, I think the |
🐙 This pull request conflicts with the target branch and needs rebase. |
Add
util::Result
support for returning more error information and make use of it in LoadChainstate method as an initial application. Followup PR #25722 uses it more broadly to return errors and warnings from wallet loading functions as well.This change adds two major features to the result class:
LoadChainstate()
which produce different errors that need to be handled differently 1.For better error reporting, adds the ability to return warning messages and multiple errors, not just a single error string. This provides a way for functions to report errors and warnings in a standard way, and simplifies interfaces:
This change also makes some more minor improvements:
Smaller type size.
util::Result<int>
is 16 bytes, andutil::Result<void>
is 8 bytes. Previously both were 72 bytes.Broader type compatibility. Supports noncopyable and nonmovable success and error types.
Alternatives & design notes
The main goal for the
util::Result
class is to provide a standard way for functions to report error and warnings strings. The interface tries to make it easy to provide detailed error feedback to end users, without cluttering code or making it harder to return other result information. There have been multiple iterations of the design using different internal representations and providing different capabilities:tuple<T,optional<bilingual_str>>
variant<T,bilingual_str>
variant<monostate,T,F>
tuple<variant<T,F>,bilingual_str>
tuple<T,optional<bilingual_str>>
tuple<T,unique_ptr<tuple<F,bilingual_str>>
unique_ptr
to reduce result type size, and avoids heap allocation in the happy path.Prior discussions & history
furszy introduced a
BResult
class providing a standard error reporting mechanism in #25218. It was renamed toutil::Result
in #25721 but kept the same representation and capabilities.MarcoFalke suggested using
BResult
for theLoadChainstate
function in #25308 (comment). Inability to useBResult
there due to lack of support for failure values led to initial work on #25608.w0xlt wrote a
StructuredResult
class in #25308 adding the ability to return failure values but sacrificing standard error reporting, which led to more work on #25608.martinus suggested a space optimization in #25608 (comment) that also made it easier to support distinct failure & success types, leading to #25665 as a replacement for #25608.
Footnotes
Ability to return error values was actually present in the original implementation of #25218, but unfortunately removed in later iterations. ↩