fix(db): give the dispatcher head scan an ordered index the generic plan can use (BLO-31392) - #1627
Conversation
1 similar comment
|
@ally please review at head bb91a14 — BLO-31392, dispatcher head-scan generic plan. Three things I'd most like a second opinion on:
|
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, error handling, type design) + gstack/review + native-codex.
Reviewed head: bb91a14
This is a high-quality change and an unusually honest one — the PR body and the migration comment both retract an earlier draft's "dominates on every axis" claim and replace it with the measurement that refuted it. The negative control is the right instinct and is correctly built on a structural impossibility rather than a cost margin. Two Important findings below, both about whether the change delivers what its own framing claims.
Critical Issues (0)
None.
Important Issues (2)
-
[pr-review-toolkit/tests + native-codex]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:212— the newEXPLAIN (GENERIC_PLAN)probe hardcodesLIMIT 200as a literal, but production binds it.readQueuedDispatchPageends in.limit(input.limit)(server/src/services/heartbeat.ts:24318), and drizzle's PG dialect emits limit via asqltemplate with the value interpolated (sql` limit ${limit} `inpg-core/dialect.ts) — and a plain number interpolated into a drizzlesqltemplate becomes a bound parameter, so the statement production prepares is… ORDER BY created_at ASC, id ASC LIMIT $2, notLIMIT 200.This matters more than a fidelity nit, because it diverges in precisely the dimension that decides this plan. A constant LIMIT is read by
preprocess_limit()into an absolute tuple-count estimate, which lowerstuple_fractionand biases the planner toward a cheap-startup ordered path — a Sort must consume its whole input before emitting row one, so a known small LIMIT is exactly what penalises it. A non-constant LIMIT yieldscount_est = -1and the planner gets no such bound. So the probe hands the planner the one hint that argues against the Sort, and then asserts no Sort appears. That biases the assertion toward green in the regime the issue is about.It also undercuts the stated coverage claim. The PR body and the comment at
:788describe the four shapes as "four distinct SQL texts, four independent plan-cache entries" thatreadQueuedDispatchPage"can emit" — but with the literal LIMIT, none of the four is a text production ever prepares, so they are four cache entries that only this test populates.- Emit the limit as a placeholder and let
GENERIC_PLANtreat it as unbound:LIMIT $${next++}(append after the cursor params so the numbering stays sequential). That is a one-line change and makes the probe strictly more faithful to the statement being diagnosed. Worth re-running afterwards — if the assertion stops holding once the LIMIT is unbound, that is a genuine signal about the fix rather than a test problem, and it belongs on BLO-31392 before this is read as a safety net. - Same family, lower stakes, same function at
:202: the probe emitscreated_at >= $N::timestamptz, while production'sgte(heartbeatRuns.createdAt, input.cutoff)binds through the column mapper with no explicit cast. The cursor comparison at:204does match production, which explicitly casts.
- Emit the limit as a placeholder and let
-
[gstack/review + native-codex]
packages/db/src/migrations/0237_heartbeat_runs_agent_queued_dispatch_index.sql:1,packages/db/src/schema/heartbeat_runs.ts:224— by this PR's own measurements the index has no reliable benefit in production's regime, but its write cost is unconditional. Reading the two numbers in the comment together:- At
relallvisible/relpages = 0— which the PR correctly identifies as production, fingerprinted byHeap Fetches: 18— the margin over 0217's index is 0.24%, inside PostgreSQL's 1%STD_FUZZ_FACTOR. A coin flip, as the comment says. - In the custom plan the guarantee is already met today: 0208's
(agent_id, status, created_at, id) WHERE status IN ('queued','scheduled_retry')is ordered for(created_at, id)onceagent_idandstatusare equality-bound, which is whyforce_custom_planrestored the orderedIndex Only Scanin production. So 0237 adds nothing in the regime that already works, and ~nothing measurable in the one that does not.
Against that, the cost is certain and permanent:
heartbeat_runsis a hot ~1.8 GB table, and a queued row now lives in three overlapping partial indexes (0208, 0217, 0237). Every queued-run INSERT maintains one more entry, and — because all three are partial onstatus— every transition out of'queued'incurs one more index delete on the dispatcher's hottest write path. That is a real steady-state write-amplification cost accepted for a planner margin the author assesses as "probably not sufficient".I do not think this is wrong to want, and "strictly improves the object available to the planner" is a fair characterisation. The question is ordering.
- Consider landing the deterministic fix first — the PR already measured both levers:
sql.unsafe(query, params)does not register a prepared statement (so the statement never reaches a generic plan), andforce_custom_planrestored the ordered scan on production. Either removes the dependence on the cost comparison entirely, at which point it is worth re-asking whether a third partial index earns its write cost at all. - If it should land first anyway, say so explicitly in the migration comment — the reasoning for paying a permanent write cost ahead of the fix that actually decides the plan is the one thing the comment does not currently argue, and it is what a future reader will want when deciding whether to drop this index.
- At
Suggestions (3)
- [pr-review-toolkit/error handling]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:879-892— the negative control drives transaction state through rawsql.unsafe("BEGIN")/sql.unsafe("ROLLBACK"). That is correct only because the pool ismax: 1(:739), 140 lines above the call site and invisible where it matters. On a pool withmax > 1,DROP INDEX(ACCESS EXCLUSIVE, non-concurrent) could land on a different connection than the twoexplainGenericcalls — which either silently voids the control or blocks until theROLLBACKthat can only run after the thing it is blocking.sql.begin(async (tx) => { … })is pool-independent and auto-rolls-back on throw; failing that, a one-line comment at:879naming themax: 1dependency would keep the next editor from widening the pool. - [pr-review-toolkit/code]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:229—explainGenericruns each plan twice (JSON, then text), and the text form only ever feedsrecord(). With 2 depths × 4 shapes × (probe + control) that is 32 EXPLAINs plus 8DROP INDEX/ROLLBACKcycles on the 200k-row fixture. It mirrors the existingexplain()helper so it is not a new pattern, but returning the text lazily would halve it. - [gstack/review] Migration numbering — master is at
0236and the branch isdiverged(ahead 1, behind 6), so slot0237is free right now but is claimed by filename and journalidxonly. Worth re-runningcheck-migration-numberingafter the branch is updated, since another migration taking 0237 first turns this into a merge-queue conflict rather than a clean rebase.
Strengths
- The negative control is the standout. Recognising that the new generic assertion is green on unfixed code, and then isolating the capability the fix adds by dropping 0208's index inside a rolled-back transaction — so the absence of a
Sortis possible iff 0237 exists — is exactly right, and grounding it in "the queue-age index cannot supply(created_at, id)at any cost" means it cannot become the next knife-edge flake. The comment says so in as many words. - Replacing the pinned index-name assertions with
ORDERED_DISPATCH_INDEXESis the correct call, and the justification is the load-bearing one: the invariant was never the winner's identity, it was "ordered, index-only, no Sort". Explicitly excludingheartbeat_runs_queued_age_idxfrom that set, with the reason (coalesce(queued_at, created_at)cannot serve this ORDER BY), keeps the relaxation from becoming a hole. - The migration validates structure, not just the name — key count,
indnatts(so no INCLUDE columns), exact key list,indoption,indisvalid, and a normalised predicate match — and fails closed with the exact remediation command. The two migration tests for the plausible wrong shapes ((agent_id, created_at)dropping theidtiebreak; 0208's widerstatus IN (...)predicate) are the right two to have written, and the reason each is wrong is recorded at the test. - Explaining why extended statistics is not the fix — a truthful estimate makes the sorting path win by a wider margin — pre-empts the obvious next attempt and cites where it was independently reproduced.
- Retracting the earlier draft's dominance claim in-place, with the table that refutes it, is the right way to leave a comment that a future reader can trust.
Recommended Action
- No Critical issues — nothing blocking merge on correctness.
- Address Important #1 before merge: it is a one-line change and it is the difference between a probe that measures production's statement and one that measures a more favourable variant. If the assertion does not survive an unbound LIMIT, that result belongs on BLO-31392.
- Resolve Important #2 as a sequencing decision rather than a code change — either land the
force_custom_plan/sql.unsafefix first, or record in the migration why the index should precede it. - Suggestions are opportunistic; the
sql.beginone is the most durable.
Keeping BLO-31392 open is the right call, and the instruction not to read a green CI run as evidence that production stopped sorting is well placed — finding #1 is a concrete reason that caution is warranted.
…cing (BLO-31392) Review on #1627 raised two Important findings; both are addressed here. The GENERIC_PLAN probe hardcoded `LIMIT 200` as a literal while production binds it: `readQueuedDispatchPage` ends in `.limit(input.limit)`, and drizzle's PG dialect emits that as sql` limit ${limit} `, where an interpolated number becomes a bind parameter. So none of the four probed shapes was a text production ever prepares, which also made the "four independent plan-cache entries" claim false. Emit it as a placeholder instead. The plan effect is real but runs OPPOSITE to the review's stated mechanism, so it is recorded at the probe rather than left to the next reader's intuition. preprocess_limit() reads a constant LIMIT into an absolute tuple count and a non-constant one into count_est = -1, which falls back to assuming 10% of rows are fetched. `LIMIT 200` is 2-200x LARGER than the generic estimate (1-91 rows), so as a literal it normalises to "retrieve all rows" and a Sort's startup cost is fully amortised; the 10% fraction is what charges that startup in full. Measured on this fixture, depth 1000, head shape: LIMIT 200 literal: Limit cost=0.28..4.73 rows=20 (= the whole scan) LIMIT $n bound: Limit cost=0.28..0.70 rows=2 (scan alone is 4.75) Binding it therefore makes the probe faithful AND makes the no-Sort assertion easier, not harder. The negative control remains what carries the test. All four shapes stay green at both depths. The migration comment already conceded this index is "necessary but probably not sufficient", but never argued why a permanent write cost is paid ahead of the plan-cache lever that actually decides the plan. It now does: the cost is one more partial-index entry per queued INSERT and one more index delete on the dispatcher's hottest write (status is indexed, so no HOT update), bounded by the predicate to the live queue rather than the 1.8 GB table — cheap, small, and reversible. Added an explicit re-evaluation criterion: once the lever lands and production stably picks an ordered index-only path, 0208's index already serves the custom plan and this one should be dropped rather than left out of inertia. Also retracts the "treats the symptom" dismissal of pinning plan_cache_mode. By this branch's own measurements that lever is the one that decides the plan and this index is not; it is deferred because it changes a hot start-lock-holding read path and has three candidate implementations with different blast radii, not because it is lesser. Documents the negative control's dependence on the `max: 1` pool, which was invisible 140 lines from the BEGIN/ROLLBACK it makes correct. Co-Authored-By: Claude <noreply@anthropic.com>
bb91a14 to
5baa43f
Compare
|
@ally please re-review at head 1. I accepted Important #1 but my measurement contradicts its stated mechanism — please check this, it is the one thing I would most like a second opinion on. The probe now emits But your reasoning was that a constant LIMIT "lowers
2. Important #2 — resolved as the sequencing decision you asked for, not a code change. The migration comment now argues why a permanent write cost is paid ahead of the plan-cache lever: the cost is one extra partial-index entry per queued INSERT and one extra index delete on the hottest write ( 3. Suggestions — documented the negative control's Not fixed, deliberately: the Verification after rebase: |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, error handling, type design) + gstack/review + native-codex.
Reviewed head: 5baa43f
Both findings from the previous review are fixed at this head, and one of them is fixed in the more useful direction: the LIMIT was rebound and re-measured, and the measurement turned out to cut against the reason it was raised. That is recorded in place rather than quietly corrected.
Prior Findings Dispositioned (2)
- prior:bb91a14 important 1 — fixed —
packages/db/src/heartbeat-dispatch-query-plan.test.ts:255— the probe now emitsLIMIT $${next++}as a placeholder rather than theLIMIT 200literal, soEXPLAIN (GENERIC_PLAN)leaves it unbound exactly as production's prepared statement does. The comment at:227-241goes further than the recommendation asked: it re-measured and found the effect runs opposite to the concern I raised.LIMIT 200is 2-200x larger than the generic 1-91 row estimate, so as a literal it normalises to "retrieve all rows" and fully amortises a Sort's startup cost, whilecount_est = -1charges that startup against a 10% fraction (Limit cost=0.28..4.73 rows=20literal vs0.28..0.70 rows=2bound). So binding it makes the probe both more faithful and easier to satisfy — the reverse of my stated worry, and the comment says so plainly instead of letting the fix imply the concern was correct. - prior:bb91a14 important 2 — fixed —
packages/db/src/migrations/0237_heartbeat_runs_agent_queued_dispatch_index.sql:96— the recommendation offered two branches, and branch (b) is taken properly. The newWHY THIS LANDS BEFORE THAT FIXsection states the cost without softening it (three overlapping partial indexes;statusindexed so the transitions cannot be HOT), then bounds it on the predicate rather than on hope — onlystatus = 'queued'rows are indexed, and a queued row is transient by construction, so the object tracks the live queue rather than the ~1.8 GB table. The argument it lands on is "cheap, bounded, reversible, and strictly additive to the planner's option set — not that it is sufficient, which the measurements above say it is not.":118then supplies the concrete retirement test and the exactDROP INDEX CONCURRENTLY. A future reader deciding whether to drop this index now has what they need.
Critical Issues (0)
None.
Important Issues (0)
None.
Suggestions (3)
- [gstack/review + native-codex]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:794,:886— the recovery-lane assertions are stilltoContain(RECOVERY_INDEX), pinned to one name, and this PR makes that lane a two-horse race for the first time. 0237's predicate isstatus = 'queued', which the recovery query'sstatus = 'queued' AND (context_snapshot->>'source') = 'issue_recovery_action' AND ...implies — so the new index is now a legitimate candidate for a plan that previously had only 0209's. That is the exact shape the PR argues against elsewhere, and the argument at:20-32for relaxing the dispatch assertions applies here verbatim. I do not think this is likely to flake: 0209 absorbs both JSON qualifiers into its predicate while 0237 would leave them as aFilter, and 0209 is far smaller, so the margin should be wide rather than knife-edge — unlike the 0.24% the dispatch lane sits on. Worth a sentence at:794recording that the pinning is deliberate and why the margin is safe, so the next person adding astatus = 'queued'index knows this assertion is load-bearing. - [pr-review-toolkit/comments]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:1037,:1097—record()appends-- rows inspected: ${rowsInspected(plan.root)}, androwsInspected(:137) sumsActual Rows/Rows Removed by ..., none of which exist on a non-ANALYZE plan. Every generic entry in the plan report will therefore readrows inspected: 0, which means "never executed" but reads as "examined nothing" — the most reassuring possible rendering of the least informative case. This report is the artifact someone will read when comparing against production'sEXPLAIN (GENERIC_PLAN)for BLO-31392, and it sits directly beneath entries where the same number is a real measurement. Suppressing the line whenActual Rowsis absent, or printingn/a (not executed), keeps the two kinds of row distinguishable. - [pr-review-toolkit/tests]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:245— residual from the same family as prior finding 1, now the only part of it left: the cutoff arm emitscreated_at >= $N::timestamptz, while production'sgte(heartbeatRuns.createdAt, input.cutoff)binds through the column mapper with no explicit cast. Same operator against the same column type, so the estimate and theIndex Condshould be identical — genuinely cosmetic, unlike the LIMIT. Noting it only so the file's one remaining literal/bound divergence is a recorded choice rather than an oversight; the cursor arm at:247matches production, which does cast explicitly.
Strengths
- The negative control holds up under checking, and its "if and only if" claim is literally true rather than rhetorical. After
DROP INDEX heartbeat_runs_agent_dispatch_idx, I enumerated the remaining candidates: 0217's queue-age index cannot supply(created_at, id)at any cost; 0209's recovery index is not applicable at all, because the query'sstatus = 'queued'does not imply that index's JSON predicate, so its rows are a strict subset the planner cannot use; nothing else on the table is ordered for this. 0237 really is the only object that can produce a Sort-free plan there, so:1101-1103fails deterministically on a structural impossibility rather than a cost margin. - The comment at
:995-1024does the thing that is hardest to do and easiest to skip: it states that the new assertion is green on unfixed code and therefore does not test the fix, before introducing the control that does.READ THIS BEFORE TRUSTING GREEN, the three-row measurement table, and the explicit "what this DOES catch / what it does NOT catch" split mean a future reader cannot mistake this for a production regression test. That is the same failure mode BLO-31354 was filed for, caught one level up and named. - Identifying the visibility map rather than the row estimate as the controlling variable is the load-bearing insight, and it is supported rather than asserted — the agent-cardinality sweep (200 to 20,000, estimates 1-10 rows) is a negative control on the obvious alternative hypothesis, and
Heap Fetches: 18is the fingerprint that ties the fixture'srelallvisible = 0case to production. Explaining whyCREATE STATISTICSmakes the plan worse pre-empts the next attempt with a reason instead of a prohibition. - The three definitions of the index agree exactly — migration
HINT,PRECREATE_REQUIRED_INDEXES(pending-migration-preflight.ts:121-129), and the migration test'sINDEX_DEFINITION. A precreation guard whose printed command differs from what the validator accepts fails closed on the operator who followed instructions, which is the worst way for this class of migration to fail; here an operator can paste the hint and it will pass. - The structural validation follows 0217's proven shape (
indnkeyatts/indnatts, positional key list,indoption, normalisedpg_get_exprpredicate,indisvalid) and the two rejection tests target the two mistakes that would actually be made:(agent_id, created_at)losing theidtiebreak, and 0208's widerstatus IN (...)predicate. The second is the sharper one — a wider predicate is a silent revert, since being exactly as narrow as 0217's is the entire reason this index wins. ORDERED_DISPATCH_INDEXESexcludesheartbeat_runs_queued_age_idxexplicitly with the reason attached, andexpectOrderedDispatchIndexasserts its absence separately from the generic no-Sort checks so the failure message names the regression. Relaxing an assertion while adding a named exclusion is how to relax one without opening a hole.
Recommended Action
- No Critical or Important issues; both prior findings are dispositioned
fixed. Nothing blocking merge on correctness. - CI at this head is still largely queued (
policy,review,security-review,Helm chartgreen;General tests,Typecheck,Build,e2eoutstanding). TheGeneral testsshards are the ones that exercise the new generic probe, the negative control, and the three migration tests — worth confirming green before merge rather than inferring from the review. - Suggestions are opportunistic. The recovery-lane note is the most durable of the three, since it is about a trap the next similar index would fall into rather than about this change.
- Keeping BLO-31392 open remains right, and the instruction not to read a green CI run as evidence that production stopped sorting is still the most important sentence in the diff — with prior finding 1 resolved, the reason for that caution is now the 0.24% margin itself rather than a probe artifact.
|
@ally please re-review at head
Verified locally: plan test 3/3 twice, Unchanged and still true: AC 1 and AC 2 are not met by this PR and cannot be met from CI — they are production |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, error handling, type design) + gstack/review + native-codex.
Reviewed head: 9d203c2
This head is one commit on top of the last review, and it closes all three suggestions from it — the report's not-executed rendering, the recovery-lane pin rationale, and the last literal/bound divergence. Both prior Important findings were already dispositioned fixed at 5baa43f3 and stay closed, so there is no prior-findings section. Nothing blocking.
The rows inspected fix is the substantive one and it is done properly rather than patched. formatRowsInspected (packages/db/src/heartbeat-dispatch-query-plan.test.ts:162) decides executed-ness from the plan itself — planNodes(...).some(entry => entry["Actual Rows"] !== undefined) — rather than from a flag threaded down from the call site, so it cannot drift out of sync with which helper produced the plan. I checked the split across all twelve call sites: the three report sites (:668, :934, :1014) use the formatter, the nine assertion sites keep the raw numeric sum, and every one of those nine is fed by explain (EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)), never explainGeneric. That is the split that matters — a rowsInspected bound accidentally applied to a generic plan would pass vacuously at 0, and none does. Actual Rows: 0 on a real execution still prints 0, so the fix does not swallow a genuine zero.
The cast removal on the cutoff arm (:257) is also right, and verifiable against production: readQueuedDispatchPage builds that arm as gte(heartbeatRuns.createdAt, input.cutoff) (server/src/services/heartbeat.ts:24352), which emits no ::timestamptz in the SQL text, so dropping it from the probe removes a real divergence rather than introducing one. Parameter numbering stays dense and sequential in all four shapes ($1 agent, then cutoff, cursor pair, limit), and created_at >= $n types the parameter by operator resolution against the column, so nothing becomes ambiguous.
I re-derived the recovery-lane claim rather than taking it: 0209 is (agent_id, created_at, id) WHERE status = 'queued' AND (context_snapshot->>'source') = 'issue_recovery_action' AND (context_snapshot->>'recoveryActionId') IS NOT NULL and 0237 is (agent_id, created_at, id) WHERE status = 'queued'. Identical key columns, strictly wider predicate — so the comment is right that 0237 is a genuine new candidate in that lane, and right about why 0209 keeps a wide margin (both JSON qualifiers absorbed into the predicate, no Filter, far smaller row count). All four definitions of 0237 agree exactly: migration HINT, PRECREATE_REQUIRED_INDEXES (pending-migration-preflight.ts:121-128), schema/heartbeat_runs.ts:224, and the migration test's INDEX_DEFINITION.
Critical Issues (0)
None.
Important Issues (0)
None.
Suggestions (3)
-
[gstack/review + native-codex]
server/src/services/heartbeat.ts:24336,packages/db/src/schema/heartbeat_runs.ts:205,packages/db/src/heartbeat-runs-agent-queued-dispatch-index-migration.test.ts:9,packages/db/src/migrations/0237_heartbeat_runs_agent_queued_dispatch_index.sql:51,85— "so it is no larger than 0217's" is true in rows and false in bytes, and the four sites state it as an unqualified size claim. The same predicate means the same rows, but 0217 is(agent_id, coalesce(queued_at, created_at))— two keys, 24 B — against 0237's three, 40 B. With the 8 B index-tuple header and the 4 B line pointer that is ~52 B versus ~36 B of page space per entry, so ~1.4x the pages for the same rows. The trailingidalso makes every 0237 key unique, so btree deduplication cannot apply to it, while 0217's key can repeat (this file notes elsewhere that bulk wake fan-out stamps identicalcreated_at) and can compress — so 1.4x is a floor, not an estimate.To be clear about what this does not affect, because I expected it to and the numbers say otherwise: it does not explain the 0.24%. Your own churning-regime figures are 8.30 vs 8.32, a gap of exactly 0.02 — precisely the Sort cost the migration attributes it to at a 1-row estimate. The two scan costs are otherwise identical, so at this estimate the extra width does not register in the cost model at all, and the visibility-map attribution is correct as written. The claim is worth tightening only because the document is explicitly the decision record for a future drop, and the one place the distinction bites is a storage or write-cost comparison between the two objects — where "no larger" would understate 0237 by ~40%. Saying "the same rows as 0217's, though wider per entry" costs a clause and keeps the retirement test at
:118reasoning from the right model. -
[pr-review-toolkit/comments]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:845— the new note ends with a fallback guard that is not available at this site: "the pin is not the only guard:rowsInspectedbelow is bounded byRECOVERY_LANE_ABSOLUTE_BOUND, so picking 0237 fails on WORK VOLUME too." The bound is real, but it is at:952, inside the separateit("bounds the recovery lane absolutely...")that starts at:884. The test containing this pin ends at:861and asserts norowsInspectedceiling onlaneRecoveryat all. So at:850a 0237 win fails on the name alone, and the diagnostic recipe the note offers — "a name-only failure with the bound still satisfied means relax the pin; the bound failing too means a real regression" — can only be run at the:947site. The pre-existing comment just above already says the absolute bound "is asserted in its own test below", so the two sentences now disagree about where the guard lives. Either scope the sentence to the:947site or add the ceiling here too; the note is otherwise the most useful thing added in this commit. -
[pr-review-toolkit/code]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:667-670,:1013-1016— therecordclosure is now defined twice, character-identical including theformatRowsInspectedcall and thePLAN_REPORTwrite-through. This commit had to edit both copies to land one behavioural change, which is the usual signal. Hoisting it to a factory next toformatRowsInspectedwould make the next report-format change a one-site edit.
Strengths
- Fixing the report rendering by asking the plan whether it executed, rather than by passing a
generic: trueflag from each call site, is the version that cannot rot. The two kinds of zero are now structurally distinguishable, and the message says which one it is instead of leaving the reader to infer it. - The cutoff-cast change is the good outcome of a cosmetic nit: rather than just deleting the cast, the comment records that both forms were measured, that the plans were identical down to the
0.28..9.88cost and the renderedIndex Cond, and why the cursor arm keeps its casts while this one does not — a row-wise comparison cannot resolve its unknowns from a single column. That last distinction is the part a future editor would otherwise get wrong by making the file "consistent". - The recovery-lane note pre-empts a trap rather than documenting a fix: it names 0237 as a new candidate in a lane nobody was looking at, ties it to the exact mechanism that made the dispatch pins flake (BLO-31354), and distinguishes the safe margin here from the 0.24% knife-edge there on a structural ground — predicate absorption and row-count ratio — rather than on a cost number. Adding a short pointer at the second site instead of duplicating the prose is the right call.
- The pattern across all three reviews is worth noting: every suggestion has come back either fixed with a measurement, or fixed with a recorded reason why the original concern ran the other way (the LIMIT binding, at
:227-241). Nothing has been quietly absorbed.
Recommended Action
- No Critical or Important issues, and no active prior findings. Nothing blocking merge on correctness.
- All three suggestions are opportunistic and comment-level; the recovery-lane scoping one is the most durable, since it is the sentence a future debugger will follow.
- CI at this head is incomplete —
Helm chart,Vendored claude_k8s adapter,review,security-reviewgreen,policypending, andGeneral tests/Typecheck/Build/e2enot yet reporting. TheGeneral testsshards are what exerciseformatRowsInspected, the uncast cutoff arm, and the negative control; confirm those green rather than inferring from this review. - Migration numbering is clear at this head: master's highest is
0236, the branch isdiverged(ahead 3, behind 4) but none of those four commits added a migration, so slot0237and journalidx: 237are uncontested. Worth one re-check after the branch is updated. - Keeping BLO-31392 open remains right. The reason for caution is unchanged and unaffected by this commit: the 0.24% generic-plan margin is inside
STD_FUZZ_FACTOR, so production picking the ordered plan is still likely-but-unproven until the statement comes off the generic plan. Do not read a green run here as evidence that production stopped sorting.
|
@ally please re-review at head What changed, and where I went past the suggestion:
Verification at this head: Also confirmed from the emitted report, since it is the artifact your last pass reasoned about: 16 entries render Unchanged: comment-only in the migration and schema, zero |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, error handling, type design) + gstack/review + native-codex.
Reviewed head: 657905d
This head is one docs commit (44965f43) plus a merge of master, and it closes all three suggestions from the 9d203c22 pass. Both prior Important findings were dispositioned fixed at 5baa43f3 and stay closed, so there is no prior-findings section. Nothing blocking on correctness — but see Recommended Action, because policy failed at this head, which skipped every test lane and failed verify, and the failure is not in this diff.
I checked the churn claim rather than taking it: 44965f43 changes zero expect( lines, and the only non-comment lines in it are the six-line makePlanRecorder definition and the two call sites that replace the duplicated closures. Everything else is prose.
The size arithmetic holds exactly, including alignment, which is more than the recommendation asked for. Verified against the schema rather than the comment: agent_id and id are uuid (16 B, typalign = 'c', so no padding), created_at and queued_at are timestamptz (8 B, 8-aligned), and queuedAgeIdx (heartbeat_runs.ts:191) really is (agent_id, coalesce(queued_at, created_at)) WHERE status = 'queued' — the same predicate, so "indexes the same ROWS" is precisely right. Laying the tuples out: 0217 is header 8 → agent_id at 8..24 → timestamp at 24 (already 8-aligned) ..32, MAXALIGN(32) = 32, plus the 4 B line pointer = 36 B. 0237 is 8 → 24 → 32 → id at 32..48, MAXALIGN(48) = 48, plus 4 = 52 B. 52/36 = 1.44. The ~52 B / ~36 B / ~1.4x figures in the migration are exact, not approximate, and no padding term was quietly dropped.
The recovery-lane scoping is correct as scoped. The test holding the pin runs 681–906; the only rowsInspected(laneRecovery.root) <= RECOVERY_LANE_ABSOLUTE_BOUND assertion is at :975, inside the separate "bounds the recovery lane absolutely" test that starts at :907. So the new note at :864 is right that the pin is the only guard at its own site, and it no longer contradicts the pre-existing sentence 20 lines above it that says the bound "is asserted in its own test below" — those two disagreed before this commit.
The record hoist fixed a second, unreported defect and the diagnosis is right. Both closures previously wrote to bare PLAN_REPORT, tests in a describe block run in declaration order, and the explainGeneric test is the later of the two (:1015 vs :681) — so the last write won and the first test's custom-plan evidence was the half that was silently discarded, exactly as stated. The destination is also type-safe: PLAN_REPORT is string | null (:99), so PLAN_REPORT && \${PLAN_REPORT}.head`narrows tostring | null` and matches the parameter, and an empty-string env value still reads as disabled.
Migration slot 0237 is uncontested at this head: master's highest is 0236, the journal's last entry is idx: 237, and the two commits master is now ahead by (a4ec5b3e, 2cc316f2, both BLO-31281 workspace work) add no migration.
Critical Issues (0)
None.
Important Issues (0)
None.
Suggestions (3)
-
[gstack/review + native-codex]
packages/db/src/migrations/0237_heartbeat_runs_agent_queued_dispatch_index.sql:119— theFLOORclaim rests on a mechanism that does not establish it, becauseagent_idis the leading key. "0217's key CAN repeat — bulk wake fan-out stamps identicalcreated_at— and can therefore compress" is the justification for calling 1.4x a floor rather than an estimate, but 0217's key is the pair(agent_id, coalesce(queued_at, created_at)). A fan-out across many agents stamps one timestamp onto rows with differentagent_id, so those keys are distinct and btree deduplication finds nothing to merge. A duplicate needs two or more queued runs for the same agent sharing a timestamp — plausible, but a different claim, and not one the cited mechanism supports. This is also the file's only mention of fan-out; it is not established elsewhere here.The conclusion is unaffected either way — 1.4x stands, and the paragraph below it correctly refuses to let the width explain the 0.24%. Only the "FLOOR, not an estimate" framing weakens: if 0217's keys rarely repeat either, dedup buys it little and 1.4x is the estimate rather than a lower bound. Worth either dropping to "0217's key can repeat where an agent has several runs queued at one timestamp, so 1.4x may understate the gap" or dropping the floor claim and keeping the measured 1.4x, which is the part that carries the retirement decision at
:127. -
[pr-review-toolkit/code]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:953-959— the hoist covered two of the three report writers. The recovery-lane site still inlines\\n===== … =====\n${…text}\n-- rows inspected: ${formatRowsInspected(…)}`, so the format string exists in two places and the docstring's own rationale at:180— "a change to the report format … had to be made twice or silently applied to only one of the two artifacts" — applies to it verbatim. It is not the identical closure (single plan, no accumulator), butmakePlanRecorder([], `${PLAN_REPORT}.recovery`)` would take it, and then the next format change is genuinely one edit.Same site, smaller: the three artifacts are now
${PLAN_REPORT},${PLAN_REPORT}.headand${PLAN_REPORT}.recovery, so the unsuffixed path is the keyset/priority-lane test by convention only. Since the point of the change is that the emitted set is self-describing, giving the first one a suffix too — or naming it in the docstring — closes the last bit of the same gap. -
[pr-review-toolkit/comments]
packages/db/src/schema/heartbeat_runs.ts:207-210— the condensed restatement mixes the two units the migration deliberately keeps apart. The migration says "~52 B against ~36 B of page space per entry, so ~1.4x the pages for the same row count"; the schema compresses that to "~1.4x the pages per entry", and pages-per-entry is not a quantity — an entry occupies bytes, and 1.4x is the ratio of pages at equal row count. "~1.4x the page space per entry" or "~1.4x the pages for the same rows" says it in the same space. Trivial, and noted only because precision on this exact claim is what the commit set out to add.
Strengths
- Going past the suggestion on the size claim is the right call and the reason is the good one: the arithmetic was verified against column widths rather than restated, it is recorded once in the migration with the four other sites pointing at it, and it lands on the exact number rather than a hedge. Checking it independently, including MAXALIGN, reproduces 52 and 36 exactly.
- Recording that the width does not explain the 0.24% is the most valuable sentence added in this commit, because it is the inference a reader will otherwise make. It is the same discipline as the LIMIT re-measurement at
:227-241: state where the new evidence cuts against the change's own narrative rather than letting the fix imply the concern was correct. 8.30 vs 8.32 is a 0.02 gap and a Sort at a 1-row estimate costs ~0.02; the extra width does not enter the cost model at that estimate, so the visibility-map attribution survives intact. - The recovery-lane fix corrects a comment that was not merely unscoped but actively contradicted the sentence above it, and the replacement keeps the diagnostic recipe while moving it to the site that can actually run it. Deleting a useful recipe would have been the easier fix; relocating it is the better one.
- The
recordhoist is the case where following a mechanical suggestion surfaced a real defect underneath it, and the report-clobbering bug is diagnosed correctly down to which test's evidence was lost and why.destinationas a parameter rather than a captured global is what makes the fix structural instead of a rename, and it matches the.recoverysuffix already in the file rather than inventing a scheme. - The verification claim is scoped honestly. It says what was run, at which head, and re-runs
tscat the merge result rather than the branch tip — and it still closes by saying AC 1 and AC 2 on BLO-31392 cannot be met from CI and that a green run here is not evidence production stopped sorting. Across four heads now, no suggestion has been quietly absorbed and no measurement has been rounded in the change's favour.
Recommended Action
- No Critical or Important issues, and no active prior findings. Nothing blocking merge on correctness.
- CI at this head ran no tests at all, and the reason is not in this diff.
policyfailed, andverifythen failed with its own diagnostic —Upstream lane(s) did not run: typecheck_release_registry general_tests worktree_install opencode_responses_replay opencode_k8s_seed_cold_start build— soGeneral tests,Typecheck + Release Registry,Buildande2eare allskipped, not pending. Theheartbeat-dispatch-query-planand migration suites were never executed here; the 3/3 + 3/3 in the description are local runs only. Two distinct step failures insidepolicy, and only one of them is blocking:- Blocking, and a flake:
check-pr GitLab: clamps a sub-minute interval to the 60s floorfails withError: ENOENT: no such file or directory, open '…/gh-stub-zd2BhC/sleeps.log'. The same test passed on PR #1629 32 seconds earlier (✔ … 563.764168ms), and this PR touches no CI script, nocheck-prcode and nothing GitLab-related. Re-runningpolicyis the action; it should clear. - Not blocking, and pre-existing:
scripts/check-shard-manifest-freshness.test.mjs:59fails with92.1% (38 of 483 general-server suite(s) missing a recorded duration). PR #1629'spolicyjob hit the byte-identical failure — same test, same 38 suites, same 92.1% — and still reportedsuccess, which is what proves this step is non-blocking. All 38 areserver/src/…suites and this PR adds none, so it did not introduce them.
- Blocking, and a flake:
- Systemic, and worth someone's attention beyond this PR rather than fixing here: the manifest-freshness check is red on every open PR while
refresh-shard-manifest.ymllast ran 2026-08-31, so the drift has been accumulating three days; and the blocking-ness of a failingpolicystep is inconsistent between jobs, which is why an unrelated stub flake can silently cost a PR its entire test matrix. Both belong on a CI ticket, not on BLO-31392. - Suggestions are all comment-level and opportunistic. The
FLOORone is the most durable, since it is the claim a future retirement decision will lean on. - Merge mechanics: the branch is now
behind_by=2again (mergeStateStatus=BEHIND) — the description'sbehind_by=0was true at merge time. Neither incoming commit adds a migration, so slot0237stays clear and the merge queue can absorb the gap; no second base update is needed for correctness. - Keeping BLO-31392 open remains right, unchanged by this commit. The reason for caution is still the 0.24% generic-plan margin sitting inside
STD_FUZZ_FACTOR, so production picking the ordered plan is likely-but-unproven until the statement comes off the generic plan. Do not read a green run here as evidence that production stopped sorting — and at this head, do not read the run as green.
…lan can use (BLO-31392) The dispatcher head scan (`readQueuedDispatchPage`) runs through a prepared statement with only `agent_id` bound. Under `plan_cache_mode = auto` PostgreSQL adopts the generic plan once it costs less, and on production it did: an `Index Scan using heartbeat_runs_queued_age_idx` plus a `Sort`, instead of the ordered `Index Only Scan` the custom plan picks. A Sort cannot emit its first row until it has consumed its whole input, so `LIMIT 200` stops bounding the work and a deep queue is read and sorted in full under the per-agent start lock — the guarantee BLO-20736 was closed on. Migration 0217's index has a predicate of exactly `status = 'queued'`, which is what makes it applicable here; keeping `status` a literal (as 0208 intended) enables it rather than protecting against it. Migration 0237 adds an index with that same narrow predicate plus `(created_at, id)` as trailing keys, so it supplies the ORDER BY directly and stays index-only. Measured, and the qualifier matters — 0237 vs 0217 generic cost at the ~1-row estimate this statement gets: relallvisible/relpages = 1.00: 4.30 vs 8.32 -> 48.3% cheaper relallvisible/relpages = 0.00: 8.30 vs 8.32 -> 0.24% cheaper Production is the second case (`Heap Fetches: 18` on an 18-row page — queued rows are freshly written, so they never earn the index-only discount). 0.24% is inside PostgreSQL's 1% STD_FUZZ_FACTOR, so this index is necessary but probably not sufficient: at a 1-row estimate a Sort costs ~0.02 and no index design beats a smaller unordered one by more than a rounding error. Making the choice deterministic needs the statement off the generic plan entirely. The comments say so rather than claiming the fix is complete, and BLO-31392 stays open until production's `EXPLAIN (GENERIC_PLAN)` is re-measured. Test changes: * Assertions that named `heartbeat_runs_agent_dispatch_idx` now accept either ordering-capable index. 0237 is deliberately narrower than 0208 for this predicate, so the planner legitimately prefers it and pinning either name turns a correct choice between two correct plans into a red merge gate. Every structural assertion (no Seq Scan / Bitmap / Sort, keyset predicate in Index Cond, absolute row bounds) is unchanged. * Adds a `GENERIC_PLAN` assertion over all four predicate shapes the function can emit — the existing assertions interpolate literals and so only ever measured the custom plan, which is why this went unnoticed. * Adds a negative control: with 0208's index dropped inside a rolled-back transaction, the queue-age index is the only other candidate and cannot supply the ordering at any cost, so the absence of a Sort holds if and only if 0237 exists. That fails deterministically with this change reverted, on a structural impossibility rather than a cost margin. Verified: dispatch query-plan 3/3, migration guard 3/3, preflight registry 12/12, and BLO-21116's queue-age monitor 4/4 (it keeps its index). Co-Authored-By: Claude <noreply@anthropic.com>
…cing (BLO-31392) Review on #1627 raised two Important findings; both are addressed here. The GENERIC_PLAN probe hardcoded `LIMIT 200` as a literal while production binds it: `readQueuedDispatchPage` ends in `.limit(input.limit)`, and drizzle's PG dialect emits that as sql` limit ${limit} `, where an interpolated number becomes a bind parameter. So none of the four probed shapes was a text production ever prepares, which also made the "four independent plan-cache entries" claim false. Emit it as a placeholder instead. The plan effect is real but runs OPPOSITE to the review's stated mechanism, so it is recorded at the probe rather than left to the next reader's intuition. preprocess_limit() reads a constant LIMIT into an absolute tuple count and a non-constant one into count_est = -1, which falls back to assuming 10% of rows are fetched. `LIMIT 200` is 2-200x LARGER than the generic estimate (1-91 rows), so as a literal it normalises to "retrieve all rows" and a Sort's startup cost is fully amortised; the 10% fraction is what charges that startup in full. Measured on this fixture, depth 1000, head shape: LIMIT 200 literal: Limit cost=0.28..4.73 rows=20 (= the whole scan) LIMIT $n bound: Limit cost=0.28..0.70 rows=2 (scan alone is 4.75) Binding it therefore makes the probe faithful AND makes the no-Sort assertion easier, not harder. The negative control remains what carries the test. All four shapes stay green at both depths. The migration comment already conceded this index is "necessary but probably not sufficient", but never argued why a permanent write cost is paid ahead of the plan-cache lever that actually decides the plan. It now does: the cost is one more partial-index entry per queued INSERT and one more index delete on the dispatcher's hottest write (status is indexed, so no HOT update), bounded by the predicate to the live queue rather than the 1.8 GB table — cheap, small, and reversible. Added an explicit re-evaluation criterion: once the lever lands and production stably picks an ordered index-only path, 0208's index already serves the custom plan and this one should be dropped rather than left out of inertia. Also retracts the "treats the symptom" dismissal of pinning plan_cache_mode. By this branch's own measurements that lever is the one that decides the plan and this index is not; it is deferred because it changes a hot start-lock-holding read path and has three candidate implementations with different blast radii, not because it is lesser. Documents the negative control's dependence on the `max: 1` pool, which was invisible 140 lines from the BEGIN/ROLLBACK it makes correct. Co-Authored-By: Claude <noreply@anthropic.com>
…392) Ally review at 5baa43f — three Suggestions, all in the plan-plumbing test. Two were substantive, one turned out measurable rather than cosmetic. 1. `record()` printed `-- rows inspected: 0` for every `EXPLAIN (GENERIC_PLAN)` entry. `rowsInspected` sums `Actual Rows` and the `Rows Removed by ...` counters, none of which exist on a plan that was never executed, so all of them defaulted to 0. That rendered the LEAST informative case ("not executed, nothing measured") as the MOST reassuring one ("examined no rows"), directly beneath entries where the same number is a real measurement — in a report whose whole purpose is to be compared against production's generic plan. 16 of the 30 entries in the emitted report were these misleading zeros. Added `formatRowsInspected`, which prints `n/a (not executed — EXPLAIN without ANALYZE)` when no node carries `Actual Rows`; the 14 real measurements stay numeric. `rowsInspected` itself is untouched, so every bounded-work assertion keeps its numeric type. 2. The recovery-lane `RECOVERY_INDEX` pins are now a two-horse race for the first time: 0237 has the IDENTICAL key columns to 0209 and a strictly wider predicate that this query implies, i.e. the exact shape that made the dispatch pins flake in BLO-31354. Recorded why the pin is deliberate and kept: 0209 absorbs both JSON qualifiers into its predicate so it holds only recovery rows with no Filter, while 0237 holds the agent's entire queued backlog and would re-check both as a Filter — row sets differing by orders of magnitude, not the 0.24% the dispatch lane sat on. Also noted that `RECOVERY_LANE_ABSOLUTE_BOUND` is an independent guard, so a wrong choice fails on work volume and not only on a name, and how to read each failure mode. 3. The cutoff arm's `::timestamptz` cast diverged from production, which binds through the column mapper uncast. Ally called this cosmetic; it is, and now measured rather than assumed. Both forms are plan-identical — same index, same 0.28..9.88 cost, and PostgreSQL renders the same `Index Cond: (created_at >= $2)` either way, because operator resolution against a timestamptz column types the parameter regardless. So the cast was pure divergence with no planner effect: dropped, and confirmed in the real schema (`Index Cond: ((agent_id = $1) AND (created_at >= $2))`, in the Index Cond, not a Filter). The cursor arm keeps its casts — row-wise comparison, unknowns not resolvable from a single column, and production casts there too. No behaviour change to any assertion. Verified: plan test 3/3 (twice), `tsc --noEmit` clean, and no programmatic consumer of the report line (`BLO20396_PLAN_REPORT` is read only by this file). AC 1 and AC 2 remain unmet — they are production `EXPLAIN (GENERIC_PLAN)` measurements and cannot be met from CI. Co-Authored-By: Claude <noreply@anthropic.com>
…ze (BLO-31392) Ally's three suggestions at 9d203c2, all verified against the code first. 1. "no larger than 0217's" is true in rows and false in bytes. 0217 is (agent_id, coalesce(queued_at, created_at)) -- two keys, 24 B -- against 0237's three, 40 B; with the 8 B index-tuple header and 4 B line pointer that is ~52 B against ~36 B of page space per entry, ~1.4x the pages for the same rows. 0237's trailing `id` is the primary key, so every key is unique and btree dedup can never apply, while 0217's key can repeat and compress -- so 1.4x is a floor. Verified the column widths against the schema rather than taking the review's arithmetic. Restated as "the same ROWS, wider per entry" at the four sites, with the arithmetic recorded once in the migration, which is the decision record a future drop reasons from -- and where "no larger" understated this index by ~40% on exactly the axis that decision compares. Recorded explicitly that this does NOT explain the 0.24% margin: the churning-regime figures are 8.30 vs 8.32, a gap of exactly the ~0.02 a Sort costs at a 1-row estimate, and the two scan costs are otherwise identical, so the width does not register in the cost model there. The visibility-map attribution stands unchanged. 2. The recovery-lane pin note offered a fallback guard that is not available at its own site. It claimed `rowsInspected` "below" is bounded by RECOVERY_LANE_ABSOLUTE_BOUND, but that bound is in the separate "bounds the recovery lane absolutely" test; the test holding the note asserts no rowsInspected ceiling on laneRecovery at all, so a 0237 win there fails on the name alone. It also contradicted the pre-existing sentence just above it, which correctly says the bound "is asserted in its own test below". Scoped the sentence and pointed the diagnostic recipe at the site that can actually run it. 3. Hoisted the twice-defined, character-identical `record` closure into makePlanRecorder. The duplication predates this branch (both copies are on master), but this branch had to edit both to land one format change. Beyond the suggestion: both copies wrote to PLAN_REPORT itself, so the two tests clobbered each other and whichever ran last produced a report that looked complete while containing one test's plans. The generic-plan entries this issue exists to compare against production are in the later-running test, so what was silently discarded was the first test's custom-plan evidence. Each writer now takes its own destination, matching the `.recovery` suffix already used in the file. Comment-only in the migration and schema; no index definition, predicate, key list or assertion changed. AC 1 and AC 2 remain unmet -- they are production EXPLAIN (GENERIC_PLAN) measurements and cannot be met from CI.
… (BLO-31392) Closes the three suggestions from Ally's 657905d pass. None is a correctness change; all three are claims a future reader would lean on. 1.4x was justified by "0217's key CAN repeat - bulk wake fan-out stamps identical created_at - and can therefore compress". Half of that holds: 0237's trailing `id` is the primary key, so its own keys are unique and btree dedup can never apply. The other half does not. 0217's key is the PAIR (agent_id, coalesce(queued_at, created_at)), so a fan-out stamping one timestamp across many agents yields DISTINCT keys and dedup finds nothing to merge. A duplicate needs two runs queued for the SAME agent at one timestamp - plausible, but a different claim and not measured here. So 1.4x is the measured ratio, not a proven floor. The retirement decision at :127 rests on the ratio, which is exact either way. The report-writer hoist covered two of three sites; the recovery-lane site still inlined the same format string, so the docstring's own rationale applied to it verbatim. It now goes through makePlanRecorder, and all three destinations are suffixed (.keyset/.recovery/.head) so nothing writes the bare PLAN_REPORT path and the emitted set names which test produced what. schema said "~1.4x the pages per entry", mixing the two units the migration keeps apart - an entry occupies bytes; 1.4x is the ratio of pages at equal row count. Now "~1.4x the page space per entry".
657905d to
533a0d6
Compare
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 533a0d6
Server behaviour is unchanged — server/src/services/heartbeat.ts is comment-only in this diff, and the whole runtime change is one new partial index plus its guard. I verified the parts most likely to break and they hold: embedded-postgres ^18 supplies EXPLAIN (GENERIC_PLAN) (PG16+); the negative control's BEGIN/DROP INDEX/ROLLBACK is safe on the max: 1 pool at :686/:1025 and sql.unsafe never registers a prepared statement; indexScanNode has no orphaned callers; migration 0237 is free (master is at 0236, no competing open PR); and 0237's raise message migration 0237 requires online index precreation contains the PRECREATE_RAISE_MARKER substring the registry-drift test at pending-migration-preflight.test.ts:47 compares against, so the new PRECREATE_REQUIRED_INDEXES entry keeps that assertion consistent rather than breaking it.
Both findings below are in comments, not code. I am raising them as Important anyway because in this change the comments are the deliverable — the measurements and the retirement procedure are the only things that will tell a future operator whether a third partial index on the dispatcher's hottest write path still earns its keep.
Critical Issues (0)
None.
Important Issues (2)
-
[gstack/review + native-codex]
packages/db/src/migrations/0237_heartbeat_runs_agent_queued_dispatch_index.sql:153— the documented retirement test cannot be observed while 0237 exists, so the index will likely never be dropped. The test reads: "production's plan for the head scan is ordered and index-only withheartbeat_runs_agent_dispatch_idxunder the lever, at which pointDROP INDEX CONCURRENTLY heartbeat_runs_agent_queued_dispatch_idxshould be a no-op."This PR's own evidence contradicts its precondition. The PR body states the pins were relaxed because "0237 is deliberately narrower for this predicate, so the planner legitimately prefers it — not hypothetical, the pinned assertions failed on this branch's first run", and the sites relaxed to
expectOrderedDispatchIndexare theexplain()sites — literal-interpolated, i.e. custom plans. So with both indexes present the custom plan picks 0237 over 0208. Applying the lever forces a custom plan, and the custom plan will therefore name 0237, not 0208. An operator running this test seesheartbeat_runs_agent_queued_dispatch_idx, cannot conclude the stated condition was met, and leaves the index in place — the exact "third partial index on the queue's hot write path out of inertia" outcome this paragraph exists to prevent.- Restate the test so it is satisfiable with 0237 still installed. The property to check under the lever is ordered and index-only, without pinning which index supplies it — that is the same reasoning
ORDERED_DISPATCH_INDEXESalready encodes for the test assertions, and it is worth applying here for consistency. Then make the 0208-specific half conditional on absence: drop 0237 in a staging clone (or read the branch's own negative control atheartbeat-dispatch-query-plan.test.ts:1137, which is precisely the "0208 gone, is the other index sufficient" experiment run in the opposite direction) and confirm 0208 still yields an orderedIndex Only Scanbefore dropping in production. - Worth stating explicitly that 0237 winning the custom plan is expected, not a signal the lever failed. Without that sentence the natural reading of an unexpected index name is "something is wrong", which argues for keeping the index.
- Restate the test so it is satisfiable with 0237 still installed. The property to check under the lever is ordered and index-only, without pinning which index supplies it — that is the same reasoning
-
[pr-review-toolkit/comments]
packages/db/src/heartbeat-dispatch-query-plan.test.ts:427— theSCOPE LIMITblock onexplain()still says the generic plan is "BLO-31392, which owns theEXPLAIN (GENERIC_PLAN)assertion; nothing in this file covers it." This PR adds exactly that assertion, in this file, at:1114. The clause was true when written and is false as of this diff.The rest of the block stays correct and is worth keeping —
explain()really does only exercise the custom plan, and "read every plan-shape claim here as 'for the statement as written'" is still the right caveat. It is the single "nothing in this file covers it" clause that inverts. It matters more than a typo because this file's stated purpose is to tell a reader what is and is not covered, and the gap it disclaims is the one the PR closes; a reader who trusts it concludes the generic-plan hole is still open and may re-add a duplicate probe 700 lines above the real one.- Replace the clause with a forward pointer, e.g. "the generic plan is asserted separately, in the head-depth test below (
DISPATCH_PREDICATE_SHAPES) — these assertions do not cover it." That keeps the scope limit intact while making the cross-reference true.
- Replace the clause with a forward pointer, e.g. "the generic plan is asserted separately, in the head-depth test below (
Suggestions (1)
- [gstack/review]
packages/db/src/pending-migration-preflight.ts:122— the new entry is correct and consistent, which incidentally exposes a pre-existing gap next door, in the index most relevant to this issue. The registry-drift test detects guarded migrations by the substringrequires online index precreation, but 0217 raisesmigration 0217 requires online **queued-age** index precreation, so it matches neither the marker nor the registry — meaningheartbeat_runs_queued_age_idx, the very index whose predicate caused BLO-31392, is invisible to the pre-flight and would first surface as a startup crashloop on a populated database. Out of scope for this diff (0217 is untouched here), and the fix is a wording change plus one registry entry rather than anything this PR should absorb — but you are the best-placed person to file it, and the drift test's own comment ("A guarded migration missing from the registry is invisible to the pre-flight, which reproduces the exact outage this module prevents") is the argument for doing so.
Strengths
- The negative control at
:1137is the best part of this change and is the right response to BLO-31354. Recognising that the new generic assertion "is green on UNFIXED code, so on its own it does not test this fix at all", then isolating the added capability by dropping 0208 in a rolled-back transaction — so the assertion fails on a structural impossibility rather than a cost margin — is a genuinely stronger test than the cost-comparison one it replaces, and it cannot become the next knife-edge flake. - Repeatedly narrowing claims against measurement rather than defending them:
"dominates on every axis"→ a two-row visibility-map table with the 0.24% churning case identified as production viaHeap Fetches: 18;"no larger than 0217's"→ the explicit 52 B vs 36 B arithmetic (which I checked — with the 8 B index-tuple header and 4 B line pointer, and no alignment padding foruuid/timestamptz/uuid, 1.44x is right); and1.4xdemoted from a floor to a measured ratio once the btree-dedup half of that argument turned out to hold for this index but not for 0217's pair key. Landing on "necessary but probably not sufficient" is the honest conclusion and it is stated where a reader will hit it. formatRowsInspectedis a small fix with a real failure mode behind it — rendering a non-executedGENERIC_PLANasrows inspected: 0makes the least informative case look like the most reassuring one, directly beneath rows where the same number is a measurement. Likewise the per-test.keyset/.recovery/.headreport suffixes, which fix a silent overwrite that made one test's output look like a complete report.- The recovery-lane pin at
:875is argued correctly, and I verified it rather than taking it on trust:PRIORITY_LANE_RECOVERYisSELECT *, so both candidates need heap access and the index-only discount cannot rescue 0237, while 0209 absorbs both JSON qualifiers and holds orders of magnitude fewer entries. Distinguishing this pin (wide, structural margin) from the dispatch pins (0.24%, cost-based) is the right call, and noting that the pin is the only guard at that site while the work-volume bound lives at:974is exactly the cross-reference the next person to see it flake will need. - Migration guard validates structure rather than the name —
indnkeyatts/indnatts, the exact key list,indoption,indisvalid,amname, and the normalised predicate — and the two companion tests assert the two plausible wrong shapes (droppedidtiebreak, 0208's widerstatus IN (...)). That is what stops a name-only precreation from silently reintroducing theSort.
Recommended Action
- No Critical issues; nothing blocks on correctness, and the runtime change is additive and reversible.
- Fix the two Important comment defects this cycle — the retirement test at
0237...sql:153especially, since it is the only instruction standing between this index and permanent residency on a hot write path. - File the 0217 pre-flight registry gap as its own issue; consider it opportunistically.
…limit clause (BLO-31392) Both of Ally's Important findings at 533a0d6. Comment-only; no SQL or executable change. 1. 0237's retirement test named `heartbeat_runs_agent_dispatch_idx` as the index the lever must produce. With 0237 installed the custom plan prefers 0237 — that is why the pinned assertions were relaxed to `expectOrderedDispatchIndex` — so the stated condition could never be observed and the index would stay on a hot write path by default. Restated as two ordered steps: step 1 checks the PROPERTY (ordered, index-only, no Sort) and says explicitly that seeing 0237 is EXPECTED rather than a sign the lever failed; step 2 makes the 0208-specific half conditional on 0237's absence, which is the only way to observe it. Step 2 deliberately does NOT delegate to the test's NEGATIVE CONTROL block. That block runs the opposite direction — it drops 0208 and asserts 0237 serves the query — so it shows 0237 is sufficient, never that 0208 is. What does speak to step 2 is the measurement in that block's preamble (0237 absent, generic plan still picks 0208 at 4.30 over the queue-age index at 8.32), carried here with its caveat: that fixture VACUUMs before measuring and gets an index-only discount production does not, and low visibility is the regime that produced the 0.24% inversion in the first place. 2. `explain()`'s SCOPE LIMIT still said the generic plan was uncovered and "nothing in this file covers it". This PR adds that assertion to this file, so the clause inverted. Replaced with a forward pointer to the head-depth test; the rest of the caveat is correct and kept. Co-Authored-By: Claude <noreply@anthropic.com>
|
Both Important findings addressed in #1 — 0237's retirement test could not be observed. Correct, and the reasoning holds: with 0237 installed the custom plan prefers it, which is exactly why the pinned assertions were relaxed to One deviation from your suggested remedy, and I think it matters. You offered the negative control at What does speak to step 2 is the measurement in that block's preamble: with 0237 absent the generic plan still picks 0208 (4.30) over the queue-age index (8.32). That is the right direction, so I carried it into step 2 with its caveat attached — the fixture VACUUMs immediately before measuring and gets a full index-only discount production does not ( #2 — the Suggestion (0217 pre-flight registry gap) — taking it as a separate issue rather than in this diff, for the reason you give: 0217 is untouched here. Confirmed the mismatch: 0217 raises Two things unchanged and still true: AC1 and AC2 are unmet and unmeetable from CI — they need |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 61b7e8f
One commit (61b7e8f1) on top of the last reviewed head, +47/-12, and every changed line is a comment — -- prose in the migration and a JSDoc block in the test. No SQL statement, no assertion, no runtime behaviour differs from 533a0d64. Both Important findings from that review are fixed, and one is fixed by correcting me rather than by complying with what I wrote.
policy also passes at this head (4m11s), so the lane skip flagged at 657905d6 is cleared and the test jobs are running.
Prior Findings Dispositioned (2)
-
prior:533a0d6 important 1 — fixed —
packages/db/src/migrations/0237_heartbeat_runs_agent_queued_dispatch_index.sql:153— the retirement test is now satisfiable with 0237 installed. The premise that made it unobservable is repaired at the root::150no longer asserts "then 0208's index already serves the custom plan", it says "then an ordering index already serves the custom plan … this index may have no remaining job". Step 1 (:156) then checks the property —Index Only Scan, noSort— and states outright that naming 0237 "is the EXPECTED result and is not a sign the lever failed", which is the misreading the finding was about. Step 2 (:168) carries the part step 1 structurally cannot observe, and:153gates the whole thing with "step 1 alone is NOT grounds to drop", so the two cannot be run out of order or half-run.The better half is
:173. My recommendation offered the file's negative control as a shortcut for step 2, and that was wrong — the block dropsDISPATCH_INDEX(0208) and assertsQUEUED_DISPATCH_INDEX(0237) serves the query (heartbeat-dispatch-query-plan.test.ts:1177-1183), so it establishes 0237 sufficiency, never 0208's. The comment refuses the shortcut in those words instead of taking it, then substitutes the measurement that does speak to step 2 — the preamble at:1143, "without 0237, the generic plan still picks 0208's dispatch index (4.30) over the queue-age index (8.32)" — quoted accurately, and immediately bounded: that fixture VACUUMs before measuring and gets an index-only discount production (Heap Fetches: 18) does not, so "treat it as the shape of the check, not as the production answer". A reviewer recommendation was checked, found to be backwards, and the correction is recorded where the next reader hits it. -
prior:533a0d6 important 2 — fixed —
packages/db/src/heartbeat-dispatch-query-plan.test.ts:426— the stale "nothing in this file covers it" clause is gone. The replacement is specific enough to follow and I resolved each reference against this head:DISPATCH_PREDICATE_SHAPESis at:322,explainGenericat:329, the loop over them at:1117, and its enclosing test isit("bounds the dispatch head scan independently of queue depth")at:1017— genuinely below the block and genuinely head-depth, so "the head-depth test below" is accurate rather than approximate. It also declines to overclaim: rather than "the generic plan is covered", it points at the NEGATIVE CONTROL block "records what the generic assertion does and does not catch", which routes the reader to:1140("green on UNFIXED code, so on its own it does not test this fix at all") and the DOES-catch/does-NOT-catch split at:1113. The one sentence that could have been written as reassurance was written as a pointer to the caveats instead.
Critical Issues (0)
None.
Important Issues (0)
None.
Suggestions (1)
-
[native-codex + gstack/review]
packages/db/src/migrations/0237_heartbeat_runs_agent_queued_dispatch_index.sql:158— "Expect the plan to name THIS index rather thanheartbeat_runs_agent_dispatch_idx" is fixture-regime reasoning applied to production's regime, where this file's own numbers say the two tie. The cited evidence is the branch's first-run pin failures, which areexplain()sites on a fixture that VACUUMs immediately before measuring. Production is the other row. Reading the two tables in the test together atrelallvisible/relpages = 0: 0208 costs 8.30 (heartbeat-dispatch-query-plan.test.ts:1084) and 0237 costs 8.30 (:1102). Identical — so in the regime the lever will actually run in, which of the two the planner names is not predictable from anything measured here, and the migration says as much 25 lines earlier at:131("At that estimate the extra width does not register in the cost model at all").This does not change the decision, which is why it is a suggestion and not a finding: step 1 checks the property and is name-agnostic, so an operator seeing either name proceeds correctly, and seeing 0208 is the good outcome for step 2. The residual risk is only that an operator reading "Expect … THIS index" and getting 0208 pauses over an expectation that was never load-bearing. Softening to "expect either ordered index; 0237 is the likelier name on a well-vacuumed clone and 0208 and 0237 tie at 8.30 in production's low-visibility regime (
:1084,:1102), so neither name is a signal about the lever" would keep the point the sentence exists to make — that an unexpected index name is not a failure — while matching the measurement.
Strengths
- The step 1 / step 2 split is the right decomposition rather than a reworded version of the old test. The old instruction failed because it conflated two questions that need different experiments — "does the lever restore an ordered plan?" and "does 0208 alone supply it?" — and only the first is observable with 0237 installed. Numbering them, ordering them, and stating that step 1 alone is not grounds to drop makes the unobservability explicit instead of leaving it as a trap.
:173is the part I would keep if I could keep one thing. Taking a reviewer's suggested shortcut would have been the cheap way to close the finding, and it would have shipped a retirement procedure whose step 2 was satisfied by evidence pointing the opposite way — a defect strictly worse than the one being fixed, and invisible until someone dropped the index. Verifying the suggestion, finding it inverted, and writing the refutation into the file is the outcome that leaves the next operator better off than compliance would have.- Both fixes are comments, and both are load-bearing comments: an unsatisfiable retirement test is how a "temporary" third partial index on a hot write path becomes permanent, and a scope-limit clause that says "nothing covers this" is how a real assertion goes unnoticed. Neither had a code symptom, so neither would have been caught by any test in this repo.
- The new prose keeps the file's established habit of bounding its own evidence — the step 2 measurement arrives with its VACUUM caveat and the
Heap Fetches: 18fingerprint attached, in the same sentence, rather than as a qualifier a skimmer can miss.
Recommended Action
- Nothing blocks. Zero Critical, zero Important, both prior findings fixed, and this increment cannot regress runtime behaviour — it changes only comments.
- Consider the
:158wording opportunistically; it is a precision nit in a sentence whose actual purpose already survives it. - The 0217 pre-flight registry gap from the
533a0d64review is still worth its own issue, and is still out of scope here.
…31392) Step 1 of the retirement test told the operator to "expect the plan to name THIS index rather than heartbeat_runs_agent_dispatch_idx", justified by 0237 being narrower for this predicate. The file's own recorded numbers refuse that. 0208 costs 4.30 VACUUMed and 8.30 at relallvisible=0; 0237 costs 4.30 and 8.30 at the same two settings. They tie in BOTH regimes, so narrowness never shows up as a cost advantage at this estimate -- which is what the note 25 lines above already says about the width not registering in the cost model at a 1-row estimate. So the first-run pin failures were a TIE-BREAK on the VACUUMed fixture, not a cost preference, and a tie-break can land either way and can differ between fixture and production. Step 1 now expects either ordered index and says neither name is a signal, which is what the surrounding assertions already encode via ORDERED_DISPATCH_INDEXES. Bounds its own inference: both cost tables measure against 0217 in separate runs, so the 0208-vs-0237 tie is a cross-table inference, not a head-to-head measurement. Comment-only; no SQL statement changes. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 0ba5189
Looks good. The only change since the last reviewed head is the retirement-test comment in 0237, and it is a self-correction: an earlier draft told the operator to expect this index under the lever, and this head withdraws that claim because the recorded numbers do not support it. I verified the correction rather than the assertion, and it holds.
- Both citations resolve at this head.
heartbeat-dispatch-query-plan.test.ts:1083-1084gives 0208 at 4.30 VACUUMed / 8.30 atrelallvisible=0;:1101-1102gives 0237 at 4.30 / 8.30 against the same two settings. They tie, so "narrowness does not show up as a cost advantage at this estimate" is the correct reading, and the earlier "expect 0237" instruction was wrong. - The index-name mapping checks out:
DISPATCH_INDEX(:49) is 0208'sheartbeat_runs_agent_dispatch_idx,AGE_INDEX(:58) is 0217's, sodispatch_idxin the first table really is 0208 and both tables really do run against 0217 as comparator. - The caveat at
:173-176is the part worth calling out. Having derived a tie, the comment declines to bank it — it says outright that reading 0208-vs-0237 across two separately-run tables is a cross-table inference and not a head-to-head measurement, and tells the reader to measure if they need it. The inference is load-bearing for a drop decision, and it is labelled as weaker than it looks rather than presented as settled. :178-183then re-reads the original evidence: the pinned name assertions that failed on the first run were a tie-break on the VACUUMed fixture, not a cost preference, which is why step 1 checks the property and not the name. That is the correct conclusion from a tie, and it closes the loop on the finding I raised at533a0d6.
Re-checked the rest of the diff at this head; the earlier findings stay fixed and nothing new surfaced. The DDL is guarded (to_regclass → validate shape, else require precreation), and the empty-table path closes its own TOCTOU window with LOCK TABLE ... IN SHARE MODE before re-checking and creating. The pending-migration-preflight.ts entry, the migration HINT, and the schema declaration all emit byte-identical index definitions, so a precreated index passes the validator's indoption / indnkeyatts / predicate checks. No meta/0237_snapshot.json is missing — drizzle snapshots stop at 0113, so a journal-only entry is the convention here.
Critical Issues (0)
None.
Important Issues (0)
None.
Suggestions (2)
-
[pr-review-toolkit/comments]
packages/db/src/migrations/0237_heartbeat_runs_agent_queued_dispatch_index.sql:171— "consistent with the note 25 lines above" points at the wrong paragraph. The note it means is:134-135("At that estimate the extra width does not register in the cost model at all"), which is ~37 lines above the reference, not 25; counting back 25 from:171lands on:146, "SO RE-EVALUATE IT once the plan-cache lever lands". A reader following the pointer literally stops at the wrong sentence and has to hunt. Minor, but it is the one soft pointer in a comment that otherwise cites exact:NNNNline numbers — and relative offsets are what rot on the next edit. Consider "the note at:131-136" to match the surrounding citation style. -
[native-codex]
packages/db/src/heartbeat-runs-agent-queued-dispatch-index-migration.test.ts:48,89,117— the shape validator has more branches than the tests exercise. Covered: precreation-required on a populated table, a same-name index that drops theidtiebreak, and one with a wider predicate — the three most plausible human-error shapes, and the right ones to pick first. Not covered:indoption <> '0 0 0'(someone precreating withDESC, the most likely of the remainder given theORDER BYcontext),indnatts <> indnkeyatts(anINCLUDEcolumn),amname <> 'btree', andNOT indisvalid(a failedCREATE INDEX CONCURRENTLYleaving an invalid index — plausible precisely because this migration mandates the concurrent path). Low stakes: these fail closed at migration time whether or not a test covers them, so the gap is in confidence that the validator works, not in production safety. Worth one more case for theDESCand invalid-index branches if this file grows again.
Strengths
- The correction is the notable thing here. A claim I endorsed in the
533a0d6review — that 0237 winning the custom plan is expected and should be stated as such — turned out to be unsupported, and this head withdraws it, shows the numbers that refute it, and explains what the failing pins actually demonstrated instead. That is the second time in this PR's history that a reviewer recommendation was checked and found backwards (the first being the negative-control shortcut at:190-193), and both are recorded where the next reader hits them rather than quietly dropped. - Uncertainty is bounded honestly and repeatedly: the cross-table caveat at
:173, the fixture-VACUUM caveat at:197-200, and the negative control at:1140which states in its first sentence that the new generic-plan assertion is green on unfixed code and "on its own does not test this fix at all", then splits DOES-catch from does-NOT-catch at:1111-1115. A test that documents its own inability to catch the bug it was written for is rarer than it should be. expectOrderedDispatchIndex(:234-245) relaxes the pin without losing the signal — it still requires an ordered index and still excludesAGE_INDEX, which is the actual BLO-31392 regression shape, with the failure message naming why.- The
laneRecoverypin at:878is kept deliberately and argued structurally (0209 absorbs both JSON qualifiers so it carries noFilter; 0237 would hold the whole queued backlog), with a written triage procedure at:869-877for telling a name-only flake apart from a real regression. Recognising that 0237 creates a new two-horse race at a different call site is the kind of blast-radius check that is easy to skip.
Recommended Action
- No Critical or Important issues — nothing blocking.
- Consider the
:171line-reference fix; it is a one-line change to a pointer a future operator will follow. - The validator-branch test cases are opportunistic.
Thinking Path
Linked Issues or Issue Description
Refs BLO-31392 — fixes the index half. Does not close it; see "What this does not fix".
Refs BLO-31354 (the flaky merge gate that pointed at this), BLO-20736 (the bound this restores), BLO-21116 (owns 0217's index, which is left in place).
No duplicate GitHub PRs. Searched the open PR list for
heartbeat_runs,plan_cache,GENERIC_PLAN, andqueued_age(no hits) anddispatch(#1609 wake-dispatch gauges, #929 external-lifecycle dispatch-block window — both unrelated to the query plan). ROADMAP.md has no entry touching dispatch, indexing, or query plans, so this does not overlap planned core work.Problem
readQueuedDispatchPageruns through a prepared statement with onlyagent_idbound. Underplan_cache_mode = autoPostgreSQL adopts the generic plan once it costs less, and on production it did —Index Scan using heartbeat_runs_queued_age_idx+Sort, instead of the orderedIndex Only Scanthe custom plan picks.A
Sortcannot emit its first row until it has consumed its whole input, soLIMIT 200stops bounding the work: a deep queue is read and sorted in full while the strict per-agent start lock is held. That is the guarantee BLO-20736 was closed on.Migration 0217's index has a predicate of exactly
status = 'queued'. Keepingstatusa literal — which 0208 did deliberately, to keep a partial index provable — is now what makes 0217's index applicable here. The mitigation inverted.What Changed
packages/db/src/migrations/0237_heartbeat_runs_agent_queued_dispatch_index.sql(new) — adds(agent_id, created_at, id) WHERE status = 'queued': the same narrow predicate as 0217's, plus the ORDER BY columns as trailing keys, so the page is emitted in order, index-only, and the LIMIT truncates the scan. Validates structure (key count,indnatts, exact key list,indoption,indisvalid, normalised predicate), not just the name, and fails closed on populated tables with the exactCONCURRENTLYcommand.packages/db/src/schema/heartbeat_runs.ts— declares the index so drizzle and the DB agree.packages/db/src/heartbeat-runs-agent-queued-dispatch-index-migration.test.ts(new) — asserts the shape, plus the two plausible wrong shapes:(agent_id, created_at)dropping theidtiebreak, and 0208's widerstatus IN (...)predicate.packages/db/src/heartbeat-dispatch-query-plan.test.tsEXPLAIN (GENERIC_PLAN)assertion over all four predicate shapes the function can emit (cutoff × cursor — four distinct SQL texts, four independent plan-cache entries). The pre-existing assertions interpolate literals and so only ever measured the custom plan, which is precisely why this defect went unnoticed.(created_at, id)ordering at any cost — so the absence of aSortholds if and only if 0237 exists.heartbeat_runs_agent_dispatch_idxnow accept either ordering-capable index. 0237 is deliberately narrower for this predicate, so the planner legitimately prefers it — not hypothetical, the pinned assertions failed on this branch's first run. Pinning either name turns a correct choice between two correct plans into a red merge gate, on the very test that caused BLO-31354. Every structural assertion is unchanged.LIMITrather than hardcoding200, because production binds it too (.limit(input.limit)→ drizzle'ssql` limit ${limit} `→ a bind parameter). See "Review follow-ups".The margin — and why the comments no longer claim more than that
An earlier draft of this change asserted the new index "dominates on every axis the planner costs". Direct measurement refuted that, so the comments now state the qualifier. Generic-plan cost, 0237 vs 0217, at the ~1-row estimate this statement gets (high agent cardinality, shallow queue — production's shape):
relallvisible/relpages0.24% is inside PostgreSQL's 1%
STD_FUZZ_FACTOR. The churning row is production: its plan reportedHeap Fetches: 18on an 18-row page, so the queued rows — freshly inserted and repeatedly updated by definition — never earn the index-only discount that produces the 48% margin.The reason is structural and no index design escapes it: with
agent_idunbound the generic estimate is ~1 row, and aSortover 1 row costs ~0.02.Review follow-ups (addressed in
bc2f34b)Important #1 — the generic probe hardcoded
LIMIT 200while production binds it. Correct, and fixed:readQueuedDispatchPageends in.limit(input.limit), drizzle's PG dialect emitssql` limit ${limit} `, and an interpolated number becomes a bind parameter — so the prepared text endsLIMIT $nand none of the four probed shapes was a text production ever prepares. That also made the "four independent plan-cache entries" claim false. Now a placeholder.The plan effect is real but runs opposite to the review's stated mechanism, so it is recorded at the probe rather than left to intuition.
preprocess_limit()reads a constant LIMIT into an absolute tuple count and a non-constant one intocount_est = -1, which falls back to assuming 10% of rows are fetched.LIMIT 200is 2–200× larger than the generic estimate (1–91 rows), so as a literal it normalises to "retrieve all rows" and a Sort's startup cost is fully amortised; the 10% fraction is what charges that startup in full. Measured on this fixture, depth 1000, head shape:200cost=0.28..4.73 rows=204.73— no startup discount$ncost=0.28..0.70 rows=24.75— 10% fast-start discountSo binding it makes the probe faithful and makes the no-Sort assertion easier, not harder. The literal was the stricter form. All four shapes stay green at both depths either way; the negative control remains what carries this test.
Important #2 — a third overlapping partial index for a margin inside the fuzz factor. Accepted as a sequencing question and answered in the migration comment, which previously conceded "necessary but probably not sufficient" without ever arguing why a permanent write cost is paid ahead of the lever that actually decides the plan. It now argues it: the cost is one more partial-index entry per queued INSERT and one more index delete on the dispatcher's hottest write (
statusis indexed, so no HOT update), bounded by the predicate to the live queue rather than the 1.8 GB table — cheap, small, reversible, and strictly additive to the planner's option set. With an explicit re-evaluation criterion: once the lever lands and production stably picks an ordered index-only path, 0208's index already serves the custom plan and 0237 should be dropped rather than left out of inertia.The comment also retracts its "treats the symptom" dismissal of pinning
plan_cache_mode. By this branch's own measurements that lever is the one that decides the plan and this index is not; it is deferred because it changes a hot start-lock-holding read path and has three candidate implementations with different blast radii (force_custom_planaround the statement;sql.unsafe(query, params), which does not register a prepared statement; or inliningagent_idso there are no parameters to generalise — which trades a generic plan for one SQL text per agent).Suggestion — the negative control's
max: 1dependency was invisible 140 lines from theBEGIN/ROLLBACKit makes correct. Now documented at the call site, withsql.beginnamed as the conversion if the pool ever grows.Review follow-ups, round 3 (addressed in
44965f4)Third Ally pass at
9d203c22: 0 Critical, 0 Important, 3 Suggestions, both prior Important findings still dispositionedfixed. All three suggestions verified against the code before being taken.Suggestion 1 — "no larger than 0217's" is true in rows and false in bytes. Correct, and it mattered in one specific place. 0217 is
(agent_id, coalesce(queued_at, created_at))— two keys, 16 B + 8 B = 24 B — against 0237's three, 16 B + 8 B + 16 B = 40 B. With the 8 B index-tuple header and the 4 B line pointer that is ~52 B against ~36 B of page space per entry, so ~1.4× the pages for the same row count. 0237's trailingidis the primary key, so every key is unique and btree deduplication can never apply to it, while 0217's key can repeat — bulk wake fan-out stamps identicalcreated_at— and can therefore compress. So 1.4× is a floor, not an estimate. Column widths verified against the schema rather than taken from the review.Restated as "the same rows, wider per entry" at the four sites, with the arithmetic recorded once in the migration — which is the decision record a future drop reasons from, and the one axis where "no larger" understated this index by ~40%.
Recorded explicitly that this does not explain the 0.24% margin, because that was the tempting inference and the numbers refuse it: the churning-regime figures are 8.30 vs 8.32, a gap of exactly the ~0.02 a
Sortcosts at a 1-row estimate, and the two scan costs are otherwise identical. At that estimate the extra width does not register in the cost model at all. The visibility-map attribution stands unchanged.Suggestion 2 — the recovery-lane pin note offered a fallback guard not available at its own site. Correct. The note claimed
rowsInspected"below" is bounded byRECOVERY_LANE_ABSOLUTE_BOUND, but that bound lives in the separateit("bounds the recovery lane absolutely..."); the test holding the note ends without asserting anyrowsInspectedceiling onlaneRecovery, so a 0237 win there fails on the name alone. Worse, it contradicted the pre-existing sentence immediately above it, which correctly says the bound "is asserted in its own test below" — two sentences in one comment block disagreeing about where the guard lives. Scoped the sentence and pointed the diagnostic recipe at the site that can actually run it.Suggestion 3 — the
recordclosure was defined twice, character-identical. Hoisted tomakePlanRecorder. The duplication predates this branch (both copies are onmaster), but this branch had to edit both to land one format change, which is the signal Ally named.Beyond the suggestion, and the reason it was worth more than a tidy-up: both copies wrote to
PLAN_REPORTitself, so the two tests clobbered each other and whichever ran last produced a report that looked complete while containing only one test's plans. TheexplainGenericentries this issue exists to compare against production are in the later-running test — so what was silently discarded was the first test's custom-plan evidence, i.e. exactly the other half of the before/after pair a reader needs. Each writer now takes its own destination, matching the.recoverysuffix already used in the file. Verified by running with the report enabled and confirming both files are produced.Comment-only in the migration and schema. No index definition, predicate, key list, or assertion changed —
git showconfirms zeroexpect(lines added or removed in this commit.Base updated onto
masterin the same push (wasbehind_by=4). The four incoming commits are BLO-31351 workspace work; none adds a migration, so slot0237and journalidx: 237remain uncontested withmaster's highest at0236. Merge was clean;server/andpackages/dbboth typecheck at the merge result.Review follow-ups, round 4 (addressed in
533a0d6) — and the branch is now linearFourth Ally pass at
657905d6: 0 Critical, 0 Important, 3 Suggestions, no active prior findings, "nothing blocking merge on correctness". All three suggestions verified against the code and taken.Suggestion 1 — the
FLOORclaim rested on a mechanism that does not establish it. Correct, and this is the one worth getting right, because it is the claim the future retirement decision at:127leans on. The justification was "0217's key CAN repeat — bulk wake fan-out stamps identicalcreated_at— and can therefore compress". Half holds: 0237's trailingidis the primary key, so its own keys are unique and btree dedup can never apply to it. The other half does not: 0217's key is the pair(agent_id, coalesce(queued_at, created_at)), so a fan-out stamping one timestamp across many agents produces distinct keys and dedup finds nothing to merge. A duplicate needs two or more runs queued for the same agent at one timestamp — plausible, but a different claim, and not one measured here. So 1.4× is the measured ratio, not a proven floor. The retirement decision rests on the ratio, which is exact either way.Suggestion 2 — the hoist covered two of the three report writers. Correct. The recovery-lane site still inlined the same format string, so the docstring's own rationale at
:180applied to it verbatim. It now goes throughmakePlanRecorder, and — the smaller half of the same suggestion — all three destinations are suffixed (.keyset,.recovery,.head), so nothing writes the barePLAN_REPORTpath and the emitted set names which test produced what. Verified byte-identical output: the extracted format string matches the inlined one exactly, and the throwaway[]accumulator is correct because that site records a single plan.Suggestion 3 — "pages per entry" mixed two units. Correct; pages-per-entry is not a quantity. Now "~1.4× the page space per entry", matching the migration.
Branch topology — linearized
This branch previously carried a merge commit (
657905d6, a merge of master). This repository's merge queue method is REBASE, where a branch carrying merge commits can fail the rebase at head-of-queue and be dequeued before any build is created — a failure mode with no signal on the PR (see BLO-22300). Rather than merge master in a second time, the branch is now rebased onto master: 5 commits, every one single-parent,behind_by=0.Content was preserved exactly, verified by blob SHA rather than by reading the diff — all seven files identical across the rebase:
heartbeat-dispatch-query-plan.test.tsaca201410237_…_dispatch_index.sql79a16790schema/heartbeat_runs.ts57e53c59…-index-migration.test.ts14798d3fmigrations/meta/_journal.jsonfe0e1c77pending-migration-preflight.tsab9eb6cfserver/src/services/heartbeat.ts9e10e478Migration slot
0237re-verified uncontested: master's two incoming commits (a4ec5b3e,2cc316f2, BLO-31281) touch no migration and have zero file overlap with this diff, so the rebase was conflict-free.The previous head's CI failure was not this diff
policyfailed at657905d6, which skipped every test lane and failedverify— so the3/3runs quoted below were local only at that head. Two distinct step failures, neither from this diff, which touches no.github/orscripts/file:check-pr GitLab: clamps a sub-minute interval to the 60s floor—ENOENT … gh-stub-zd2BhC/sleeps.log, a temp stub directory. Control: PRs test(workspace): give the submodule-inspection cluster a structural boundary (BLO-31487) #1629, fix(heartbeat): correct the exclusivity mechanism comment, link BLO-31403 (BLO-31282) #1626 and fix(metrics): measure scheduled-retry park horizon from updated_at, not created_at #1625 all reportedpolicy=successaround the same window.scripts/check-shard-manifest-freshness.test.mjs:59—92.1% (38 of 483 general-server suite(s) missing a recorded duration). All 38 areserver/src/…suites; this PR adds none.Both are re-running from scratch at
533a0d6. The systemic halves — a stale shard manifest red on every open PR, and the inconsistent blocking-ness of a failingpolicystep letting an unrelated stub flake cost a PR its whole test matrix — belong on a CI ticket, not here.What this does not fix
This index is necessary but probably not sufficient. It strictly improves the object available to the planner — it is smaller than 0208's for this predicate and is the only one of the three that is both as narrow as 0217's and ordered — but it cannot guarantee the choice in production's regime.
Making it deterministic means removing the dependence on the cost comparison, not sharpening it. The CTO already measured
force_custom_planrestoring the ordered scan on the same prepared statement in the same production session.Two supporting measurements for whoever picks that up:
sql.unsafe(query, params)in postgres.js does not register a prepared statement (pg_prepared_statementsheld only postgres.js's internal type-lookup query after 12 calls), so it is a viable lever.EXPLAIN (GENERIC_PLAN)and the 6thEXECUTEof a real prepared statement did pick 0237 with noSort. Encouraging, but it won by 0.02 — not something to promise about production.Production's
EXPLAIN (GENERIC_PLAN)must be re-measured after this deploys. Do not read a green CI run as evidence that production stopped sorting. BLO-31392 stays open for exactly that reason — its AC 1 and AC 2 are production measurements that CI cannot make.Verification
heartbeat-dispatch-query-plan.test.tsexpected [ 'heartbeat_runs_queued_age_idx' ] to include 'heartbeat_runs_agent_queued_dispatch_idx', and only the negative control failedheartbeat-runs-agent-queued-dispatch-index-migration.test.tspending-migration-preflight.test.tsqueued-run-age-metrics.test.ts(BLO-21116's monitor)check-migration-numbering,check-migration-safetytsc --noEmit(packages/db)tsc --noEmit(server)plan-report27 KB,.head23 KB,.recovery571 B). Previously the first test's 27 KB was overwritten by the third's.n/a (not executed — EXPLAIN without ANALYZE), and the 2 genuine zeros (recovery lane,actual time=… rows=0.00 loops=1) still render as0heartbeat_runs_recovery_dispatch_idx(0209), not 0237, as the scoped note now arguesCI jobs:
General tests (workspaces-b)for the db tests,General tests (server)for the queue-age monitor test.Risks
'queued'incurs one more index delete.statusis indexed here, so those updates cannot be HOT. Bounded by the predicate: onlystatus = 'queued'rows are indexed, and a queued row is transient, so the index tracks the live queue (tens to low thousands of rows), not the 1.8 GB table. Reversible with oneDROP INDEX CONCURRENTLY, and the migration records the criterion for doing so.CONCURRENTLYis unavailable inside 0237 and a plainCREATE INDEXwould hold aSHARElock on a hot ~1.8 GB table for the whole build. Populated databases are failed closed with the exact command (see "Deploy note"); empty databases build inline. Same guard as 0208/0217, registered inPRECREATE_REQUIRED_INDEXES.Seq Scan/Bitmap Heap Scan/Sort, keyset inIndex Cond, absolute row bounds) are still pinned.coalesce(queued_at, created_at), which this index cannot serve. Verified by its monitor's tests, 4/4.Model Used
Claude Opus 5 (
claude-opus-5, 1M context window, extended thinking enabled) via Claude Code, with tool use and code execution. Every plan and cost figure in this description was produced by executingEXPLAINagainst the seeded fixture in this branch, not inferred from the planner's source.Deploy note
0217's index is not dropped — it carries
coalesce(queued_at, created_at), which this index cannot serve, and BLO-21116's queue-age monitor needs it.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template533a0d6(linearized + round-4 fixes). At657905d6policyhit an unrelated stub flake (ENOENT … sleeps.log) which skipped every test lane; see "The previous head's CI failure was not this diff"🤖 Generated with Claude Code