executor: native CREATE INDEX CONCURRENTLY with fail-closed invalid-index recovery - #15
executor: native CREATE INDEX CONCURRENTLY with fail-closed invalid-index recovery#15Kiran01bm wants to merge 1 commit into
Conversation
…covery First Phase 3 native-path operation (PLAT-38441). A failed concurrent build leaves an invalid catalog entry; the executor proves the build backend stopped, reads one atomic catalog snapshot, and reports the leftover as a typed outcome naming the operator's explicit DROP INDEX CONCURRENTLY — it never drops an index itself, because a name-based drop cannot prove whose index it destroys. Proof queries are pg_catalog-qualified so a hostile search_path cannot shadow them into a false clean; every indeterminate state fails closed.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Review requested by Armand and performed by his agent — same two lenses used across this stack (#14, #13, #9, #8, #7, #6, #2): pg-sprite as an OSS-first, best-in-class Postgres DDL tool, and pg-sprite as a clean integration target for an orchestrator. Reviewed at head This is the best PR in either stack, and the reasoning is better than the code — which is the right way round for a safety-critical package. Four things are worth naming because they are not obvious and someone will eventually want to "simplify" them:
The findings below are about the seam between this executor and the humans and systems that will use it — not the catalog logic, which holds up. OSS lens
Integration lens
Verified solidThe catalog reasoning holds under attack. The single-statement snapshot genuinely does keep the identity proof and the index inspection from straddling a concurrent replacement, and the deliberate absence of This review was generated by Claude Code (claude-fable-5). |
|
🤖 Adversarial correctness review requested by Armand and performed by his agent — separate from the two-lens pass. Method: take the executor's own safety claim ("it never drops an index, because a name-based drop cannot prove whose index it destroys") and ask whether the advice it gives an operator is held to the same standard, then verify against a real PostgreSQL 16 at head The catalog logic held up under everything I threw at it. What did not hold is the layer above it: the executor refuses to drop by name and then instructs a human to do exactly that, in states where it has not established that the named index is debris. Findings, most severe first1. return fmt.Sprintf("index %s.%s may be invalid and needs explicit recovery; verify pg_index.indisvalid and recover with DROP INDEX CONCURRENTLY %s: %v", …)Two live reproductions where following it destroys a good index: A concurrent healthy build. A second actor — another pod, a retry, a re-driven job — asks for an index another session is still building. During the build the entry is legitimately The first build then completed normally — An indeterminate verdict. With no free pool connection the verdict can't run, and the executor fails closed by naming a drop over an index it never inspected: Here the build failed in phase 1 on a name collision and created nothing at all; The distinction the message needs is already in the type — 2. The verdict's deadline scales with the build budget, so a build that fails in milliseconds can block the caller for hours. The detached verdict context is The verdict's work is two short catalog reads plus a 50ms poll loop; it has no reason to inherit the build's budget. A short independent bound (seconds, not the build budget) would reach the same fail-closed answer immediately instead of after the full budget. Note the pool doesn't have to be misconfigured for this: 3. An operator's
4. The success path is the only path that trusts the absence of an error. 5. Two small things in the same file. Probed and heldThe Reproduction tests
|
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving on Armand's behalf. My two-lens review and adversarial correctness pass are posted above — the findings there are for follow-up, not fix-before-merge blockers.
This approval was submitted by Claude Code (claude-fable-5) at Armand's direction.
Summary
First Phase 3 native-path operation (PLAT-38441):
pkg/executor.BuildIndexConcurrentlyruns one named, schema-qualifiedCREATE INDEX CONCURRENTLYon a dedicated non-transactional session and owns the failure mode the statement is famous for — the invalid catalog entry a failed build leaves behind. Every invalid index is surfaced as a typed, fail-closed outcome naming the operator's explicit recovery; the executor never drops an index itself.What
BuildIndexConcurrentlywith its own wait policy:lock_timeout = 0(a per-lock timeout would cancel the build's snapshot waits and create the very invalid index this executor exists to prevent), one finitestatement_timeoutbudget bounded to PostgreSQL's 32-bit millisecond ceiling.CREATE INDEX ... CONCURRENTLY, named index, schema-qualified table, noIF NOT EXISTS(a name-only no-op can't prove the existing index is valid or even the requested one).pkg/statement.OpgainsIfNotExists.SELECTsharing one MVCC snapshot — and report*InvalidIndexErrorcarrying the original build failure and the recovery statement. A provably clean catalog returns the build failure alone.pg_catalog-qualified (relations, functions,OPERATOR(pg_catalog.=)): a search_path listing a user schema beforepg_catalogcannot shadow the catalogs into a false clean. A regression test builds against a hostile search_path with impostor catalogs and was verified to fail without the qualification.Why
A failed
CREATE INDEX CONCURRENTLYleaves an invalid index that every write still maintains but no query uses. PostgreSQL drops indexes by name, not identity, so automatic cleanup could destroy another actor's same-name index registered in the same window — the engine's one-migration-per-table lease (planned invariant LK-1) would close that, and until it exists recovery stays with the operator. Recovery SQL is returned only as data in the typed error.Failure flow