Skip to content

3.x.x: order the tips plan's scheduled block against its warm-up tips instead of racing them (#638) - #652

Merged
kiwidream merged 5 commits into
3.x.xfrom
kiwidream/638-real-node-smoke-race
Oct 2, 2026
Merged

kiwidream merged 5 commits into
3.x.xfrom
kiwidream/638-real-node-smoke-race

Conversation

@kiwidream

@kiwidream kiwidream commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Closes #638.

Problem

real_node::real_node_smoke_reconciles_against_postgres_and_the_chain failed in about 3–8% of CI runs with pool_blocks: 0: 3 external blocks above the ramp and no own block. The tips plan's warm-up placed its one scheduled block (15 s) and its three external tips (6, 12, 18 s) on the wall clock alone. Control::ScheduledBlock went out at 15 s, and the peer was asked for the third tip at 18 s, whatever had become of the block. The block is a full network-target search in an unoptimised debug build on a 2 vCPU runner, followed by the server's asynchronous offer. When that outlasted the 3 s gap, the third tip took height 12963 first, and the block was either refused or became a losing branch.

The block also raced the tip before it in the other direction. It was mined on the session's newest job whatever tip that job was on. A job that arrived late (the gate allows 5 s from a tip to the last session's notify, and one run measured 5.8 s) gave the block a parent the second tip had already replaced.

Change

In a phase that holds both scheduled blocks and external tips (only the tips plan's warm-up does), the landing is now ordered against the tips (run::drive_phase_with_population):

  • The block waits for settled work. It is sent only when every mint asked of the node is on the node's tip, and session 0's newest job is on that tip. It also waits for any earlier block in the phase to settle.

  • The tips wait for the block's verdict. No tip is minted from the moment a block is due until that block has settled. Settled means one of three things:

    • the node answered a submitblock for that block's hash (and, on the fake node, an accepted one is on the chain);
    • the session reported that it could not build or send it. A failed write is excluded: the line may have reached the server, so the node's verdict decides it (review round 2). A control channel that refused the block also settles it (review round 1).

    The block's identity is the hash of the header the session submitted. Only that session's records from after the send count (EP-STATE). The server's answer never settles it (EP-ERRORS). A block candidate's append is never refused, so an answer of ledger-outcome-unknown or ledger-confirmation-failed can still be followed by an offer. A block on block-only work is answered stale-job and is still captured and offered (3.x.x: An own block found 9 s after the previous landing was refused unknown-job and never reached the node #478; review round 4). A refused block the node never sees is reported unsettled, and the tips held behind it as unminted.

  • New node readings. ExternalMint gains settled_tip and block_answered. On qbitd these come from the mint log, A's watcher and the relay's submitblock log. The fake node reads its own chain. mint_one already waits for B to hold A's tip, so a tip minted after the verdict builds on the pool block.

  • Nothing is dropped silently (EP-OBSERVABILITY). The phase report adds scheduled_blocks_ordered_against_tips. It has one row per block: due, sent and settled offsets, the block hash and how it settled. null means the phase ended first. It also adds external_tips_unminted. The phase's deadline is the only bound on either wait, so a block that never settles costs the remaining tips, and the report counts them.

  • real_node.rs. When pool_blocks != 1, the test prints node.relay.submissions, node.submissions, node.mints, each phase's ordering rows, client_failures_by_kind and client_failures_sample, and the post-ramp node logs. It also asserts that the warm-up's block was settled by the node's answer, that its hash is the accepted submission, and that no tip went unminted. The pool_blocks == 1 assertion is unchanged.

  • README: the --scheduled-blocks row describes the ordering and the two report fields.

  • Keepalives are held over the same span (review round 2). qbitd's keepalive mints are held while a block is due or outstanding. The hold is set under the keepalive's own stop_minting lock before the settled tip is read. A keepalive is therefore either already counted as pending, or is not asked for until the verdict. A block still outstanding at the phase's deadline is seen through at the boundary, within the drain limit, with the keepalives still held. The hold is released only after it settles (review round 3).

Other plans' phases are unchanged: no other phase holds both blocks and tips.

Tests

Commands were run one binary at a time, with CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 CARGO_INCREMENTAL=0.

  • cargo test -p qbit-prism-load --test harness: 150 passed. Eight tests are new: the four below, plus one for each review case (a session whose task stopped, a failed write, a block still outstanding at the deadline, and a refused block the node never sees). Each drives the real run::drive_phase over the fake node, for a 6 s warm-up with tips at 1.2, 2.4 and 3.6 s and the block at 3 s. A stand-in session gets its delays injected: when each tip's job reaches it, how long the search takes, what the server answers, and when the block is offered. No host load is involved.

    • the_next_warm_up_tip_waits_for_the_scheduled_block_the_node_has_not_answered: search 1.5 s plus offer 0.3 s, past the third tip's slot. The chain must be external, external, pool, external.
    • the_scheduled_block_waits_until_its_session_holds_work_on_the_settled_tip: every job reaches the session 1 s after its tip. The block must be sent after the second tip's job arrives and land on it.
    • a_scheduled_block_refused_with_its_outcome_unknown_still_holds_the_next_tip: the server answers ledger-outcome-unknown and offers 1.5 s later. The tip must wait for the node's answer.
    • a_scheduled_block_captured_after_a_stale_job_refusal_still_holds_the_next_tip: the server answers stale-job and offers the block 1.5 s later, as a 3.x.x: An own block found 9 s after the previous landing was refused unknown-job and never reached the node #478 capture does. The tip must wait for the node's answer.

    The new tests fail without the fix. With the ordering switched off, so the scheduler behaves as on 3.x.x, all four fail. The first three end with the block refused prev-blk-not-found, which is 3.x.x: flaky real_node_smoke: the one scheduled own block does not land (pool_blocks 0) because it races the third external tip #638's symptom. The fourth (then a stale-job test that expected a release) failed because no ordering row was recorded. All four pass with the fix. Each review-round test was also checked by reverting only its fix: it fails, and passes again with the fix restored.

  • PRISM_TEST_PG_BIN_DIR=<PostgreSQL 16 bindir> cargo test -p qbit-prism-load --test ctv_settlement: passed (36 s). This is a fake-node tips-plan run with a real client and server, 1 tip and 1 scheduled block. It now also asserts that the warm-up's block was settled by the node's answer and that no tip went unminted.

  • cargo test -p qbit-prism-load --lib (44, including the new qbitd::tests::a_held_keepalive_is_asked_for_only_after_its_release), --test realism (21), --test churn (6) and --test real_node (fake_node_mode_is_unchanged): all passed.

  • cargo fmt --all -- --check and cargo clippy -p qbit-prism-load --all-targets -- -D warnings: clean.

Not run here and why

  • real_node_smoke_reconciles_against_postgres_and_the_chain itself. The build host has no qbitd, so the test skips itself here, and the qbitd readings (Qbitd::settled_tip, Qbitd::block_answered) were checked by reading only. This PR's prism-native-postgres shard runs it. It should pass, and its new assertions should show the block settled by the node answered its submitblock. One CI pass does not measure a 3–8% flake, so the evidence that the race is gone is the deterministic harness tests above. If it fails again, the new output says where the block went.

Merge notes

  • None. Only qbit-prism-load changes, and the two report fields are additive.

🤖 Generated with Claude Code

… instead of racing them (#638)

The real-node smoke landed no own block in 3-8% of CI runs. The
scheduled block went out at 15 s and the third external tip was minted
at 18 s on the wall clock alone. When the block's search and the
server's asynchronous offer outlasted the gap, the tip took the block's
height. The block could also be mined on a job from before the
previous tip, when that tip's notify was slow.

In a phase that holds both scheduled blocks and external tips (the
tips plan's warm-up), the block is now sent once every mint asked of
the node is on its tip and session 0 holds work on that tip. No tip is
minted from the moment a block is due until it has settled. Settled
means one of three things: the node answered a submitblock for that
block's hash; the server refused it stale-job or low-difficulty, which
prove nothing entered the ledger; or the session reported that it
could not send it. A ledger-outcome-unknown answer does not settle it,
because a block candidate's append can still be offered.

ExternalMint gains settled_tip and block_answered for both nodes. The
phase report records each ordered block and any tips left unminted.
real_node.rs prints the relay and node submissions, the mints, the
client failures and the node logs when pool_blocks is not 1.

Four harness tests drive the real scheduler with injected stand-in
delays. All four fail with the ordering switched off, three of them
with prev-blk-not-found, and pass with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fbf4e28a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/qbit-prism-load/src/run.rs Outdated
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T16:13:45.097440Z 2d02b47 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

… the next tip is not held to the phase's end (#638)

A session whose task has already stopped has a closed control channel.
The ScheduledBlock send then fails and no ClientFailure is ever
reported for it, so the landing stayed outstanding and every later
warm-up tip was held until the deadline. The refused send is now the
landing's verdict: the block never left the harness. This was raised
in the Codex review of #652.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kiwidream

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 796b5db1f8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/qbit-prism-load/src/run.rs
Comment thread crates/qbit-prism-load/src/qbitd.rs
… the node's verdict (#638)

Two holes in the ordering, raised in the second Codex review of #652.

A scheduled block whose socket write failed is reported both as a
no-response submit and as a recorded ClientFailure. The write can fail
after the line reached the server, so only an unrecorded failure proves
the block was never sent. A recorded one now waits for the node's
verdict on the block's hash, like any other unanswered submit.

qbitd's keepalive task asked for its own mints regardless of the
ordering. A keepalive requested between the settled-tip read and the
block's send, or while the block was outstanding, could take the block's
height. The scheduler now holds keepalives over the same span as the
tips. The flag is set under the keepalive's stop_minting lock before the
settled tip is read, so a keepalive is either already counted as
pending or waits for the verdict. The hold is released at the phase's
end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kiwidream

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d32f4e371

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/qbit-prism-load/src/run.rs
…re releasing the keepalives (#638)

The keepalive hold was dropped at the phase's end, even when the
scheduled block was still searching or waiting for the node. A
keepalive due during teardown, or the next phase's tips under --plan
tips, could then take the block's height. This was raised in the third
Codex review of #652.

The phase now sees an outstanding landing through at the boundary,
within the drain limit, with the keepalives still held. The wait is
boundary time, like the drained restart's wait, and nothing is offered
meanwhile. The hold is released only after the landing settles or the
limit runs out. An aborted phase does not wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kiwidream

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2ffe574f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/qbit-prism-load/src/run.rs Outdated
…ince a stale-job block can still be offered (#638)

A block on block-only work, retired by a same-parent payout
replacement, is answered stale-job and still captured and offered
(#478). Scheduled blocks bump the payout revision, so this is reachable
here. Treating stale-job (or low-difficulty) as the block's verdict
released the next tip before the node had answered. This was raised in
the fourth Codex review of #652.

A block the session sent is now settled only by the node's answer. A
refused block the node never sees holds the tips to the boundary and is
reported unsettled, with the held tips as unminted; that run has
already failed the smoke. The harness tests pin the stale-job capture
and the unsettled report. The stand-in run's commit timeout is 1 s, so
the boundary's wait is short.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kiwidream

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 2d02b47d71

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kiwidream
kiwidream merged commit b8aeb29 into 3.x.x Oct 2, 2026
63 of 67 checks passed
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.

1 participant