Have createNewBlock() wait for tip, make rpc handle shutdown during long poll and wait methods - #31785
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31785. 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.
If it's not too difficult, can we reproduce this with a test?
There was a problem hiding this comment.
This depends on the node initialization (and shutdown) sequence. I'm not sure how to reproduce that in a test. And we might change that sequence in a way that this never happens in the first place.
|
The Stratum v2 Template Provider that I implemented first calls See Sjors#49 ( But other implementers may not realise this, so having See this comment for more details on the init sequence: Lines 1817 to 1833 in 1172bc4 Also note that currently the IPC server starts listening in Lines 1358 to 1367 in 1172bc4 A good client application might wait for |
af85987 to
96ce223
Compare
commented
Feb 3, 2025
|
There were slightly more worms in this can. Instead I've now changed |
96ce223 to
be15f2e
Compare
There was a problem hiding this comment.
| * @retval empty if node is shutting down | |
| * @retval std::nullopt if the given `timeout` passes or the node is shut down before the tip is connected (there is no tip during startup). |
There was a problem hiding this comment.
re: #31785 (comment)
I don't think suggested text is accurate because if timeout is exceeded while waiting for a new tip, this returns information about the current tip. I don't think that behavior should be changed.
Returning null only when node is shutting down should be the simplest and best behavior.
There was a problem hiding this comment.
if timeout is exceeded while waiting for a new tip, this returns information about the current tip
True. What I meant was that if there is no tip within the timeout. The function does wait_for(... tip_hash && tip_hash != current_tip ...) - tip_hash to be set and to be different than the current. I meant that it could remain unset while the timeout passes. I see how the comment I suggested is misleading. What about:
@retval std::nullopt if the given `timeout` passes before the tip is
connected (there is no tip during startup) or the node is shut down.?
There was a problem hiding this comment.
Ah, I think I see the problem. There's a potential scenario where node startup is unusually slow, longer than timeout.
I think it's better in that case to ignore the timeout and instead keep waiting for the node to set a tip.
There was a problem hiding this comment.
Yes, this scenario I had in mind.
in that case to ignore the timeout and instead keep waiting for the node to set a tip
Hmm. What if the tip is not being connected for whatever reason? Wait forever? The current behavior of respecting the timeout looks safer. Would be an odd user experience to provide a timeout and the program to decide to wait longer for whatever reason.
There was a problem hiding this comment.
Just before the lock and return there was a period when both m_tip_block_mutex and cs_main were unlocked, so tip_hash may be stale here. So this could return an inconsistent hash+height - the hash of one block and the height of another. The previous code was ok because it retrieved both the hash and the height from chainman().ActiveChain().Tip() under cs_main. I think this will fix it:
- LOCK(::cs_main);
- return BlockRef{*Assume(tip_hash), Assume(chainman().ActiveChain().Tip())->nHeight};
+ return getTip();There was a problem hiding this comment.
In commit "rpc: handle shutdown during long poll and wait methods" (ad3af401c19b8d05ce69011a359db3090b7018e1)
Nice suggestion. If this suggestion is applied should also simplify the code above
- if (!tip_hash || chainman().m_interrupt) return {};
+ if (chainman().m_interrupt) return {};since tip_hash is no longer relevant. Can also reduce the scope of the tip_hash variable.
There was a problem hiding this comment.
Given that "no tip" is expected here (unlikely but is not a "programming logic error"), maybe avoid CHECK_NONFATAL() and use JSONRPCError:
std::optional<BlockRef> old_block{miner.getTip()};
std::optional<BlockRef> new_block;
if (IsRPCRunning()) {
const uint256 h{old_block.has_value() ? old_block->hash : uint256::ZERO};
new_block = timeout > 0 ? miner.waitTipChanged(h, std::chrono::milliseconds(timeout)) : miner.waitTipChanged(h);
}
if (!new_block.has_value()) {
throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "No tip within timeout or shutting down");
}There was a problem hiding this comment.
Oh, you mean old_block could be nullopt (assuming the RPC is loaded at this point).
But the suggested error here would change the current behavior - if there's a timeout we return the old block. I could do an early error return though if the first getTip() doesn't give us a tip.
There was a problem hiding this comment.
I mean to avoid CHECK_NONFATAL() if the tip is null and use JSONRPCError instead.
If the tip is null at the start, then wait for it to be set, if not set within the timeout, then JSONRPCError.
There was a problem hiding this comment.
I ended up with something similar...
There was a problem hiding this comment.
Maybe elaborate the message that we could be here if no tip was connected within the timeout: "No tip within timeout or shutting down".
There was a problem hiding this comment.
I'm introducing a new RPC_SHUTDOWN_ERROR which makes sense here. I don't think the message itself matters, since the user knows they shut down the node - and the exact details of where and why the RPC call fails aren't important.
There was a problem hiding this comment.
nit: I think it is redundant to check that maybe_tip has value with CHECK_NONFATAL() just after if (!maybe_tip). That's like:
if (A) {
throw ...
}
assert(!A);
tip = maybe_tip->hash; seems fine as well.
There was a problem hiding this comment.
Indeed, but on the other hand code changes over time, stuff appears between the if statement and the usage of tip, then someone changes the if statement, and now we have a crash...
commented
Feb 12, 2025
New behavior seems good, but I am confused by this description. It looks to me like previous behavior was to segfault in this case, because it would return |
left a comment
There was a problem hiding this comment.
Code review be15f2e769987b1df2cf1c8f327c5aafe064dbb3. Mostly looks good but I think vasild's suggestion #31785 (comment) should be used so waitTipChanged can't return an inconsistent hash and height
There was a problem hiding this comment.
In commit "rpc: handle shutdown during long poll and wait methods" (ad3af401c19b8d05ce69011a359db3090b7018e1)
Nice suggestion. If this suggestion is applied should also simplify the code above
- if (!tip_hash || chainman().m_interrupt) return {};
+ if (chainman().m_interrupt) return {};since tip_hash is no longer relevant. Can also reduce the scope of the tip_hash variable.
There was a problem hiding this comment.
re: #31785 (comment)
I don't think suggested text is accurate because if timeout is exceeded while waiting for a new tip, this returns information about the current tip. I don't think that behavior should be changed.
Returning null only when node is shutting down should be the simplest and best behavior.
be15f2e to
6321d59
Compare
|
I changed I introduced a new
Updated the description. On question about long polling I still need to investigate: #31785 (comment) |
6321d59 to
fd9c8b6
Compare
commented
Feb 13, 2025
|
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
b8dc8d5 to
af5f0cc
Compare
commented
Feb 13, 2025
|
I pushed a commit to clarify long poll behavior. |
af5f0cc to
a2d91b5
Compare
commented
Feb 13, 2025
|
I mixed up the commits in my last push, will fix... |
80e6e42 to
b4d98b8
Compare
left a comment
•
There was a problem hiding this comment.
Concept ACK
In 64a2795 "rpc: handle shutdown during long poll and wait methods"
is there a use for returning the last known tip?
If not, should we simplify the behavior by always returning nullopt when the tip has not changed or no better block is available?
Or It will be more consistent to return the last known tip if it is set, when this edge case happen handle it by returning nullopt.
commented
Mar 20, 2025
|
@ismaelsadeeq I think this just preserves existing RPC behavior. What to return during shutdown is a bit arbitrary anyway, because a connected client will have to handle shutdown more generally, potentially by also shutting down. I also haven't checked if any of the other tests rely on the existing behavior. So maybe better for a followup? |
left a comment
There was a problem hiding this comment.
Code review ACK cc1001f3bf17b31512c05fb359e09483a07fb2a3. Only change since last review was rebasing after #31283
re: #31785 (review)
is a use for returning the last known tip?
I think the main use for returning the last known tip when the tip hasn't changed is just to allow the caller distinguish between the case where the tip has not changed from the case where the node is shutting down. If it returned null in both cases we would need to have to add a separate isShuttingDown method to distinguish them and code calling waitTipChanged in a loop would be more complicated.
I don't think there is a use for returning the last known tip when the known is shutting down, just because caller can assume the tip at that point is the same value they passed as the current_tip argument.
| /** | ||
| * Waits for the connected tip to change. During node initialization, this will | ||
| * wait until the tip is connected. | ||
| * wait until the tip is connected (regardless of `timeout`). |
There was a problem hiding this comment.
In commit "Have createNewBlock() wait for a tip" (db14ca3556ca792546bf4343feb733271333690f)
Commit message is a little confusing because it doesn't mention the timeout change. Would be clearer if it said the commit was changing two things (1) returning null on shutdown instead of last tip (2) ignoring timeout value during startup instead of returning 0 if timeout elapsed before tip was connected
There was a problem hiding this comment.
re: #31785 (comment)
I improved the commit message.
Thanks for the update. In case you wind up updating again, maybe consider extending to make a little more readable:
-- return null on shutdown instead of the last tip
-- ignore timeout value node initialization
+Update createNewBlock to:
+
+- ignore timeout value during startup and wait for a tip to be connected instead of returning 0
+- return null on shutdown instead of the last tip to make shutdown easier to detect
This allows consumers of BlockTemplate to safely
assume that a tip is connected, instead of having
to account for startup and early shutdown scenarios.- return null on shutdown instead of the last tip - ignore timeout value node initialization This allows consumers of BlockTemplate to safely assume that a tip is connected, instead of having to account for startup and early shutdown scenarios.
Move the comparison to hashWatchedChain inside the while loop. Although this early return prevents the GetTransactionsUpdated() call in cases where the tip updates, it's only done to improve readability. The check itself is very cheap (although a more useful check might not be). Also add code comments.
cc1001f to
05117e6
Compare
left a comment
There was a problem hiding this comment.
Code review ACK 05117e6, just updated a commit message since last review
| /** | ||
| * Waits for the connected tip to change. During node initialization, this will | ||
| * wait until the tip is connected. | ||
| * wait until the tip is connected (regardless of `timeout`). |
There was a problem hiding this comment.
re: #31785 (comment)
I improved the commit message.
Thanks for the update. In case you wind up updating again, maybe consider extending to make a little more readable:
-- return null on shutdown instead of the last tip
-- ignore timeout value node initialization
+Update createNewBlock to:
+
+- ignore timeout value during startup and wait for a tip to be connected instead of returning 0
+- return null on shutdown instead of the last tip to make shutdown easier to detect
This allows consumers of BlockTemplate to safely
assume that a tip is connected, instead of having
to account for startup and early shutdown scenarios.|
|
||
| if (chainman().m_interrupt) return {}; | ||
|
|
||
| // Must release m_tip_block_mutex before getTip() locks cs_main, to |
There was a problem hiding this comment.
Nit: This comment seems a bit misplaced here now. Maybe place it further up before calling WaitLock?
There was a problem hiding this comment.
Will look into moving it if I need to retouch more.
commented
Apr 14, 2025
|
ACK 05117e6 |
This PR prevents Mining interface methods from sometimes crashing when called during startup before a tip is connected. It also makes other improvements like making more RPC methods usable from the GUI. Specifically this PR:
Assumecheck to disallow passing negative timeout values toMining::waitTipChangedwaitfornewblock,waitforblockandwaitforblockheightRPC methods usable from the GUI when-server=1is not set.Mining::waitTipChangedto returnoptional<BlockRef>instead ofBlockRefand returnnulloptinstead of crashing if there is a timeout or if the node is shut down before a tip is connected.Mining::waitTipChangedto not time out before a tip is connected, so it is convenient and safe to call during startup, and only returnsnullopton early shutdowns.Mining::createNewBlockto block and wait for a tip to be connected if it is called on startup instead of crashing. Also documents that it will return null on early shutdowns.This allows
waitNext()(added in #31283) to safely assumeTipBlock()isn'tnull, not even during a scenario of early shutdown.Finally this PR clarifies long poll behaviour, mostly by adding code comments, but also through an early
break.