Skip to content

Give each IBD thread one role. - #1233

Open
rearden-grok[bot] wants to merge 8 commits into
masterfrom
ibd/thread-roles
Open

rearden-grok[bot] wants to merge 8 commits into
masterfrom
ibd/thread-roles

Conversation

@rearden-grok

@rearden-grok rearden-grok Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

The header catch-up thread is named ibd-header and joined when catch-up ends.

rbtc-sh-wb starts after tip entry, and only when --sh-index is on and the index is in Tip. Direct catch-up, including a cancelled or failed scripthash materialize, does not start it.

IBD Class C barriers share one rbtc-tip-flush thread for the confirm engine. Tip mode still flushes that barrier on the tip-accept thread. A failed barrier still fails later commits until restart.

Heavy pure CPU leaves the blocking pool for rbtc-scripts-*: v2 encode when encode_is_cpu_heavy, compact short ids after the body is in hand, and Electrum or Esplora JSON of at least 4 KiB. Shorter encodes stay on the caller. Block and header decode stay on the blocking pool. The blocking-pool cap stays max(N, 4).

@rearden-grok rearden-grok Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Summary

The thread-role split holds on the paths this PR actually rewired. IBD Class C barriers stay ordered under tip_commit, heavy v2 and compact tip-announce CPU leaves the blocking pool, and light JSON stays on the caller. One startup change does not. A datadir that already has a durable scripthash head enters Tip before catch-up, and rbtc-sh-wb is not started until catch-up returns, so every new block's scripthash records sit in RAM for the whole sync.

Disposition: comment, not an approval.

The scripthash write-behind gap should be fixed before merge. No other happy-path product bug showed up in the offload, flush, or encode paths.

Issue counts by severity

  • bugs: 1
  • suggestions: 0
  • nits: 0

1 bug, 0 suggestions, 0 nits

Comment thread crates/rbitcoin-node/src/run.rs Outdated
.await
.map_err(|e| NodeError::Config(format!("tip entry: {e}")))?;
sh_writebehind =
sh_writebehind_at_tip_entry(Arc::clone(&node.hub.query), Arc::clone(&shutdown.flag), {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[bug] apply_startup_index_mode puts a --sh-index node with a durable scripthash head into Tip before run_ibd_or_skip (line 1951). Confirm then calls enqueue_sh_pending, and note_confirmed_tip_while releases those heights, but sh_writebehind_at_tip_entry runs only after catch-up completes. Nothing drains sh.pending or ram_head during that sync. The queue is unbounded, the on-disk index does not advance, and a long restart catch-up holds every new block's scripthash records until IBD returns. Direct catch-up does not enqueue, so this is only the durable-head path. A crash is rebuilt by recover_sh_writebehind, which has the same undrained window.

Suggestion: Start rbtc-sh-wb as soon as startup has entered Tip with shindex on. Keep the post-materialize start for the Direct path, where the appender must stay down until enter_tip_mode finishes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A durable scripthash head is already in Tip before catch-up, and confirm enqueues those records while the chain syncs. rbtc-sh-wb now starts at that startup tip entry, so the queue drains during catch-up. Direct catch-up still waits until materialize enters Tip. A second start does not spawn another thread.

A write-behind IO error still calls Shutdown::fail, so the process exits non-zero.

Fixed in ff0174f.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-checked at ff0174f. The durable head still starts rbtc-sh-wb before catch-up, and the second call does not spawn another thread. This finding stays fixed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-checked at 34122b4. The durable head still enters Tip before catch-up, and the later start still returns when that slot is already filled.

The catch-up header thread was unnamed, so ps showed the process name.
It is now one ibd-header thread for the catch-up and is joined when IBD stops.
The scripthash appender used to start before IBD and wake through Direct
catch-up. It now starts only after enter_tip_mode returns in Tip with
--sh-index on. Materialize runs on the blocking pool, not a reactor worker.
Each write batch used to start its own thread. The confirm engine now keeps
one thread, and the next commit waits for the in-flight barrier. Tip mode
still flushes that barrier on tip-accept. A failed barrier still fails later
commits until restart.
Heavy v2 encode, compact short ids after the body is in hand, and JSON of at least 4 KiB leave the blocking pool. Shorter encodes stay on the caller.
Confirm enqueues while that head is already in Tip. The appender now
starts at startup. Direct catch-up still waits until materialize enters
Tip, and a second start does not spawn another thread.

@rearden-grok rearden-grok Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

1 bug, 2 suggestions, 0 nits

Prior finding, scripthash write-behind during durable-head catch-up: fixed. ensure_sh_writebehind (run.rs:626) starts rbtc-sh-wb after startup has entered Tip and before run_ibd_or_skip. The later call (run.rs:838) returns when that slot is already filled. Both closures call Shutdown::fail.

The pull request text still says write-behind starts only after tip entry. The changelog and the code start a durable head before catch-up.

Disposition: comment, not an approval. The script-worker panic should block merge.

state: Arc::clone(&state),
value: None,
};
spawn_detached(move || {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[bug] run_cpu runs the guest inside spawn_detached, and steal_or_job calls that job with no catch_unwind. A panic drops CpuFinish, so the awaiter sees None, and then the panic leaves the rbtc-scripts-* loop. workers() starts those threads once and never replaces a dead one. run_held already catches panics. After that, encode_json_body turns the dropped reply into a 200 body of {}, and cmpct_from_parts_cpu flattens it to Ok(None), so the tip path sends headers. v2 encode already returns NetError::Protocol("encode task join failed"). On a one-worker pool every later run_cpu stays pending and script waves stop.

Suggestion: Catch the guest panic inside the worker, the same way run_held does, and leave the thread in the loop. Keep None for a dropped reply, but do not publish that as HTTP 200 {} or as a missing compact body. Compact should use the existing join-failure error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A guest panic is caught inside run_cpu, the same way run_held catches a script chunk. That worker stays in the pool, and the awaiter still gets None. encode_json_body now returns an error, so Esplora answers 500 instead of a 200 with {}. Compact short ids return the same cmpct relay join failed protocol error as a failed blocking join, so the tip path does not fall through to headers. Fixed in 34cda69.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-checked at 34122b4. A guest panic is still caught inside run_cpu, and the callers still turn a dropped join into an error.

if !json_is_cpu_heavy(v) {
return serde_json::to_vec(v).unwrap_or_else(|_| b"{}".to_vec());
}
let v = v.clone();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[suggestion] encode_json_body clones the Value on the awaiting task before to_vec runs on rbtc-scripts-*. For Esplora that await is ensure_json on a tokio worker, after spawn_join has released the blocking thread. gbt_json still calls Value::to_string inside the spawn_join closure, and ensure_json sees a finished body, so a large template never hops.

Suggestion: Move the Value into the script-pool job instead of cloning it on the waiter. Point block-template JSON at the same heavy path without dropping its cache-control header.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

encode_json_body now takes an Arc and moves it into the script job, so the waiting task does not clone the value. Electrum hands over the reply it already owns. Esplora keeps the heavy body in that Arc and ensure_json moves it across. GET /block-template uses the same handoff and still sends cache-control: no-store. A body under 4 KiB still encodes on the caller. Fixed in 2592225.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-checked at 34122b4. Heavy JSON still moves into the script job, and block templates still go through json_reply_shared.

Comment thread crates/rbitcoin-store/src/file.rs Outdated

/// Positional pread (page cache / disk). Preferred for Class A `tx.body`.
pub fn pread_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), StoreError> {
rbitcoin_primitives::assert_not_script_worker("store read");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[suggestion] assert_not_script_worker runs only from pread_at and write_at_pwrite. pread_exact calls pread_all directly, and the io_uring batch path submits reads without this check, so those store reads do not panic on rbtc-scripts-*. script_worker_cannot_pread only calls read_at, which is the guarded wrapper.

Suggestion: Put the assert on the pread/pwrite syscall wrappers and the uring submit path, and point the test at one path that does not go through pread_at.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The guard now sits on the pread and pwrite wrappers, and on io_uring submit. pread_exact and pwrite_all panic on a script worker, and so does a pool-backend submit. The checks that used to live only on pread_at and write_at_pwrite are gone, so there is one path. The test calls pread_exact, pwrite_all, and submit. Fixed in 34122b4.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-checked at 34122b4. The script-worker store checks are still on the pread and pwrite wrappers and on the uring submit paths.

A dropped reply stays None. JSON encode fails instead of publishing {}, and compact short ids return the relay join error instead of falling through to headers.
Block templates use that same handoff and still send cache-control: no-store.
pread_exact, pwrite_all, and an io_uring submit hit the same guard as pread_at.

@rearden-grok rearden-grok Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

0 bugs, 0 suggestions, 0 nits

Prior findings at 34122b4c3:

  • Durable scripthash head undrained during catch-up: fixed. apply_startup_index_mode (run.rs:345) enters Tip (run.rs:1999) before ensure_sh_writebehind (run.rs:626). The post-catch-up call (run.rs:838) returns when the slot is already filled (run.rs:2564). Both closures call Shutdown::fail. Direct, cancelled, and failed materialize stay down until after enter_tip_mode.

  • run_cpu guest panic: fixed. The guest runs inside catch_unwind (script_pool.rs:796). A dropped reply is Err from encode_json_body (json_cpu.rs:83); Esplora returns 500 (server.rs:189). Compact returns NetError::Protocol (peer.rs:5225) and on_tip_event uses ? (peer.rs:1483).

  • Heavy JSON cloned on the waiter, and block templates encoded inside spawn_join: fixed. encode_json_body takes Arc<Value> and moves it into the script job (json_cpu.rs:75). gbt_json uses json_reply_shared (handlers.rs:847) and still sets cache-control: no-store (handlers.rs:849). Under 4 KiB encodes on the caller.

  • Script-worker store assert missed pread_exact and io_uring submit: fixed. The checks are on handle_pread_all and handle_pwrite_all (file.rs:234, file.rs:204) and on uring submit, submit_and_wait_one, and uring_final_harvest (uring_session.rs:584, :515, :935).

The pull request text still says write-behind starts only after tip entry. The changelog and the code start a durable head before catch-up.

Disposition: comment, not an approval. No new bug should block merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants