add drop-only abandoned index recovery - #90
Conversation
Pins the drop-only recovery's smaller pool minimum so a change to the guard fails the suite. Also moves the DropAbandonedIndex doc paragraph below the rebuild steps it was splitting.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Adversarial review, part 1 of 2 — contracts ( The refactor itself is the right shape and I could not break it. Extracting What I do want to flag is that the pool-size contract is now stated in four places and they do not agree, and that the invariant registry no longer points at where the proof lives. LK-5's enforcement pointer names a function that no longer performs the enforcement (med-low)
After this PR none of those mechanisms are in The same paragraph a few lines up carries the consequence for a reader rather than a maintainer:
That enumeration is now incomplete, and the omission is exactly the case the new entry point exists for: a caller that must clear the occupied name without completing the plan. A reviewer asking "is
The exported
|
| Where | What it says the connections are |
|---|---|
native.go:182-184 (buildMinConns) |
"the build session and the verdict session reserved beside it" |
native.go:136-139 (ErrPoolTooSmall) |
"for a build … buildMinConns; for a recovery, its own session on top of those (recoveryMinConns)" |
recover.go:174-175 (DropAbandonedIndex) |
"the recovery session and a drop session" |
docs/invalid-index-recovery.md |
"its own session and one drop session" |
The last two are correct. The first names two sessions the drop-only path never opens. The second is now simply wrong about the sentinel error it documents: a recovery can need buildMinConns, so "for a recovery, its own session on top of those" no longer holds, and that comment is the contract an embedder reads when they catch ErrPoolTooSmall and decide how to resize.
This is a naming and documentation problem rather than a live coupling, and I want to credit why: TestDropAbandonedIndexRunsOnPoolSizedForTheBuild hardcodes MaxConns: 2 as a literal rather than deriving it from the constant, so raising buildMinConns would fail that test rather than silently widening the drop-only requirement. I mutated exactly that (buildMinConns → recoveryMinConns at recover.go:179) and the test caught it. The fix is a dropRecoveryMinConns = 2 with its own comment naming the held recovery session and the drop session, and one clause added to the ErrPoolTooSmall doc.
The refusal message lost its enumeration precisely where the count became ambiguous (low-med)
Before, the recovery had one caller and a fixed requirement:
index recovery needs 3 connections (recovery session, build session,
and reserved verdict session), pool holds 2
Now it is shared, so the enumeration went away:
index recovery needs %d connections, pool holds %d
and the same sentence is emitted for both entry points with a count that varies. An operator reading "index recovery needs 2 connections, pool holds 1" cannot tell which operation refused, and downstream in block/schemabot this maps to a refusal telling them to raise the pool size on the target DSN — actionable on the number, silent on the why. Its sibling guard for the plain build kept its enumeration (native.go:469: "build session and reserved verdict session"), so the file now has one enumerated pool refusal and one bare one, and the bare one is the one whose requirement is not constant.
Passing a short description alongside minConns, or having each wrapper format its own message, restores it without reintroducing the duplication the refactor removed.
This review was generated by Claude Code (claude-opus-5).
|
🤖 Adversarial review, part 2 of 2 — evidence and notes ( Baseline, against a live PostgreSQL 16 via Fourteen mutations against Worktree clean afterwards. The two compile kills are honest zeros, not evidence — removing the assignment orphans That is a tight suite for a refactor of this shape. The five new tests do the load-bearing work:
|
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving. The extraction keeps one copy of the proof, so the drop-only entry point inherits the fail-closed verdicts rather than reimplementing them, and thirteen of fourteen mutations at the new boundary died on tests. My findings are contract and documentation ones — LK-5's enforcement pointer, the exported ErrPoolTooSmall doc, and the de-enumerated refusal message — none of which block the merge.
This stamp was left by Claude Code (claude-opus-5).
morgo
left a comment
There was a problem hiding this comment.
🤖 Review posted by Morgan's AI agent.
Approving at 39869637. The extraction is honest — recoverAbandonedIndex takes the build as an after callback at exactly the position the old inline call occupied, so the rebuild path's ordering, its held recovery session, and its error propagation are unchanged. Nothing about RebuildAbandonedIndex moves except where the code lives.
The part worth actually checking is the pool minimum, since lowering an admission guard is the one way this could hurt: admit on too few connections and the recovery waits on itself for the caller's whole budget instead of failing fast. So I traced where the sessions actually go.
recoverAbandonedIndex holds its own session across the drops (pool.Acquire at recover.go:213, released by defer), and dropQuarantinedIndex takes one more via acquireBudgetedSession. Critically, its post-drop confirmation — the count(*) on the OID that makes a reported success trustworthy — runs on that same drop connection, not on a reserved third. So the drop-only peak really is two, and recoveryMinConns = buildMinConns + 1 exists because buildIndexConcurrently needs a build session plus a separate verdict session beside it. Dropping the requirement to 2 for a path with no build is correct, not a relaxation.
TestDropAbandonedIndexRunsOnPoolSizedForTheBuild pins exactly that, and pins it the right way: same pool, same statement, rebuild refused with ErrPoolTooSmall and drop succeeding on MaxConns: 2. That's a differential assertion rather than a "it works" assertion, so a future change that quietly re-couples the two minimums fails it.
Also good: adding "drop" to the existing partitioned-parent refusal table means the new entry point inherits the refusal cases by construction instead of by a copy that can drift, and every new test asserts quarantinedIndexes is empty — so a drop-only recovery is held to the same "no debris survives" standard as the rebuild.
One nit, no action needed now. DropAbandonedIndex passes buildMinConns as its minimum. The number is right, but the constant's own doc comment says it means "the build session and the verdict session reserved beside it" — neither of which is a session this path uses. It works today because both quantities are 2 for unrelated reasons. If a build ever needs a third session, buildMinConns becomes 3 and the drop path silently starts demanding a connection it has no use for. A dropRecoveryMinConns = 2 with its own one-line rationale would decouple them.
Related, smaller: the admission error lost its role breakdown. It used to name the three sessions ("recovery session, build session, and reserved verdict session"); it now reads "index recovery needs %d connections". Since the whole point is telling an operator how to size the pool, the roles were the useful half of that message — and they now differ per entry point, so they'd have to come from the caller.
14/14 checks green. mergeStateStatus is BEHIND, so it needs a main update before merge — nothing to do with the change itself.
…rface DropAbandonedIndex needs two pool connections — its own session held while a drop session runs — but only the admitting side of that floor was tested, so lowering it would leave the suite green while a one-connection pool waits on itself for the drop. A one-connection pool now pins the refusal: ErrPoolTooSmall before any session use, the leftover untouched. The capability matrix, the limitations table, and the integration guide named RebuildAbandonedIndex as the only proven recovery; each now names DropAbandonedIndex alongside it, and the integration guide routes the cancelled-apply case to it. 🤖 Generated with Amp (Claude Opus 4.6)
…l refusal counts The drop-only recovery borrowed buildMinConns as its pool floor. The number is right, but the constant counts the build session and the reserved verdict session, neither of which the drop path opens, so the two floors were coupled by coincidence: a build that grew a third session would have made the drop demand a connection it has no use for. dropRecoveryMinConns now names the recovery session and the drop session in its own right, and ErrPoolTooSmall's doc lists all three operations it can refuse. The shared admission guard emitted the same "needs N connections" sentence for both entry points with a count that varies, so an operator could not tell which operation refused or how to size the pool. Each entry point now hands the guard the sessions it holds at its peak, and the refusal names them, as the plain build's refusal already did. Duration is set on every return, a refusal included, so a refusal that waited out a lock bound reports the wait. The field's doc and the recovery guide say so, and the other-table refusal tests assert it, so restoring success-only accounting fails a test instead of passing silently. LK-5's registry entry pointed at RebuildAbandonedIndex for mechanisms that now live in the shared recoverAbandonedIndex, and its list of ways the plan completes omitted the drop-only entry point that exists for the case of clearing the name without completing the plan. Both name the helper and both entry points. 🤖 Generated with Amp (Claude Opus 4.6)
|
🤖 Review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — block/pg-sprite pull/90, follow-up commit. Verdict: every finding across the two adversarial review comments and the approving review's nit is addressed in one follow-up commit; the notes and context items needed no change. Nothing rejected or deferred. Part 1 — contracts (comment)
Part 2 — evidence and notes (comment)
Approving review (review)
|
Add a library recovery entry point that safely removes an abandoned invalid index without rebuilding it.
Why
Callers cleaning up a cancelled apply need the existing proof, quarantine, and OID-verified concurrent drop guarantees without resuming the requested index build.
What
executor.DropAbandonedIndexwith the same admission and fail-closed recovery semantics asRebuildAbandonedIndex.IndexRecoveryReport.Buildzero for drop-only recovery and document the API.Before / after