Skip to content

3.x.x: flaky test: a_relaunched_frontend_recovers_one_row_at_the_same_statement_cost_at_both_cardinalities counts one extra candidate-poll transaction (65 vs 61 statements) #729

Closed

Description

@RobertClarke

candidate_storm_restart::a_relaunched_frontend_recovers_one_row_at_the_same_statement_cost_at_both_cardinalities failed once in CI. It counted one extra candidate-poll transaction in its baseline, so it's a timing flake in the measurement, not the server finding its message claims.

Seen: CI on #725's first head 69797f64 (the v3.0.0-rc.4 runtime, differing from the tag only in the rc.4 notes): run 37469847966, job 112290176938 (PostgreSQL shard 2). The binary's other 20 tests passed.

  • Error: recovering one row cost a relaunched frontend 65 statements with 23 rows behind it and 61 with 99. Per-row recovery cost is not independent of N; this is a finding about the server, not this test.

The two statement lists differ by exactly one transaction. The baseline (23 rows) ran one more of each of these; everything else is identical:

Statement Baseline (23 rows) Storm (99 rows)
BEGIN 7 6
SELECT c.fatal_error, EXISTS(… qbit_ledger_writer_lease …) … FROM qbit_prism_cluster c CROSS JOIN qbit_prism_submission_hold h … 6 5
WITH stepped AS (SELECT o.block_hash FROM qbit_block_candidate_outbox … 1 0
COMMIT 7 6

That's one candidate-poll cycle. The extra cost sits at the smaller N, so it isn't per-row scaling: a periodic poll iteration fell inside the baseline's counting window.

Intermittent: the same runtime passed it in CI on #725's final head 4a8e855a (run 37470368719).

Fix (test-only): count only the recovery's own statements. For example, exclude the poll loop's transactions, or compare the two cardinalities' statement multisets after removing whole poll cycles, so a poll that happens to land in one window can't fail the comparison.

🤖 Generated with Claude Code

Activity

  1. kiwidream commented on Oct 22, 2026

    @kiwidream
    Member

    Another occurrence on #777 (lockfile-only change), run 39806884637, shard 2: 65 vs 72 statements. This time the extra transaction is the metrics collector's (metrics/collectors.rs:56), not a candidate poll. The storm run's diff from the baseline is exactly these 7 statements:

    BEGIN
    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY
    SELECT set_config('statement_timeout','2000',true),set_config('lock_timeout','500',true)
    SELECT count(*), COALESCE(GREATEST(0,extract(epoch FROM transaction_timestamp()-min(created_at))),0)::double precis…
    SELECT max(upper_seq)-qbit_prism_share_next_seq() FROM qbit_prism_share_partitions WHERE state='attached'
    SELECT block_hash FROM qbit_block_candidate_outbox WHERE block_hash=ANY($1::text[]) AND state='orphaned'
    COMMIT
    

    So the comparison picks up any periodic background transaction that falls inside the recovery window, not just the candidate poll. A fix should exclude, or attribute, the background tasks' transactions (the metrics collector's ten-second tick included) rather than special-case one of them.

  2. kiwidream commented on Oct 27, 2026

    @kiwidream
    Member

    Four more occurrences since the comment above. Each has the metrics-collector signature: one cardinality counts 7 extra statements.

    Run What ran Statements (baseline vs storm)
    40359403717 #780 (no-op test PR), Oct 26 72 vs 65
    40391226379 3.x.x push at 59194d03, Oct 26 72 vs 65
    40523580929 #788, Oct 27 72 vs 65
    40535982684 #788 again (closed and reopened, same head), Oct 27 72 vs 65

    Since Oct 22, PostgreSQL shard 2 has failed in 5 of 23 completed CI runs. Every one of those failures was this test, and one of them was a 3.x.x push.

    Why the idle windows can never learn this transaction. The collector's snapshot (metrics/collectors.rs) runs its orphan-evidence lookup (… WHERE block_hash=ANY($1::text[]) AND state='orphaned') only when revision_work_terminal_probe() returns hashes. It returns hashes only once the process has registered a block in its landing metrics. coordinator.rs registers one on three events:

    • a definitive submitblock acceptance;
    • an active-chain candidate observation;
    • a reconciled landed block.

    A registered block closes when it is proven orphaned, or when the revision work for its landing reaches a miner's socket. No miner connects in this test, so after the first registration the lookup runs on every 10 s tick.

    In this test, the relaunched child registers nothing during its two idle windows, so the lookup's text never enters the background set. In the measured window, the child offers the siblings it claims ahead of the recovered row, and the node accepts them. From then on, any collector tick that overlaps the recovered row's marked span contains one statement that was never seen while idle, so foreground counts the whole 7-statement run.

    So this is not an idle window that settled too early. It is a background statement that only exists after an acceptance. How likely a measurement is to hit it depends on how long the recovered row's span lasts compared with the 10 s tick.

    Fix direction (test-only). Learn the background set in the state the measured window runs in. For example, let the relaunched child offer and land one sibling before the idle windows. Then every background statement that depends on an accepted or landed block, this lookup included, is seen while idle. That covers the class rather than special-casing the collector.

    🤖 Generated with Claude Code

  3. kiwidream commented on Oct 27, 2026

    @kiwidream
    Member

    Fix in #792. Two corrections to my comment above, from the deterministic reproductions made for it:

    • The 72 vs 65 lookup: it isn't switched on by siblings accepted ahead of the recovered row. Since 3.x.x: database clock steps stretch outbox retries and claim leases; a PostgreSQL outage holds only one alert rule #581, the first claim poll after the release takes the recovered row over, ahead of every sibling, and the recovered row's own active observation (Metrics::accepted_block) registers the block that switches on the collector's orphan lookup.
    • The original Oct 6 failure (61 vs 65): it wasn't an extra transaction. The 99-row run lost four statements of the recovered row's own claim transaction. That poll sent BEGIN and the fence before the measured window's mark, so runs() split the transaction.

    #792 also handles a third case found while testing it: a bare ROLLBACK that SQLx sends when the job/blob pruner's COMMIT outlasts its 5 s deadline on a slow disk.

    🤖 Generated with Claude Code

  4. kiwidream commented on Oct 27, 2026

    @kiwidream
    Member

    Fixed by #792, merged into 3.x.x as 2e11f1bf (its "Fixes #729" does not fire on a non-default branch). It fixes all three causes in candidate_storm_restart.rs. (1) The metrics collector's orphan lookup, which starts only after the recovered row's own accepted block and was counted as foreground: a sibling now lands before the idle windows, and the measured executions come from the verification window. (2) The claim transaction straddled the mark (the Oct 6 65-vs-61 variant): it is now kept whole. (3) The job/blob pruner's bare ROLLBACK after a slow COMMIT: transaction control is now neutral in the foreground count. Shard 2 passed on both CI heads of #792 (runs 40565379439 and 40569032223). A failure of this test on a tree that contains 2e11f1bf is a new finding.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions