feat(server): in-app online-DDL lane for non-transactional/locking DDL (JEF-580) - #151
Merged
thejefflarson merged 1 commit intoJul 28, 2026
Conversation
…L (JEF-580) Implements ADR 0021: CREATE/DROP INDEX CONCURRENTLY moves out of the transactional sqlx::migrate! (which can't run CONCURRENTLY at all, and holds an advisory lock for the whole run) into a new server/src/online_ddl.rs lane, spawned after the HTTP listener binds so boot/readiness never wait on a build and a failure only logs (fail-soft; reads fall back to whatever index already covers the query). - Declarative desired-index list, seeded with the existing covering index (metric_series_rollups_name_bucket_covering_idx) copied verbatim from migration 0017 -- against prod, where it's already valid, the lane's first run is a no-op that proves the mechanism. - Reconciles via pg_index.indisvalid rather than IF NOT EXISTS: absent -> build, valid -> no-op, INVALID (left by an interrupted build) -> drop + rebuild. - Runs on a dedicated connection (db::online_ddl_connect_options: statement_timeout=0, lock_timeout=3s, bounded maintenance_work_mem), outside any transaction, guarded by its own pg_try_advisory_lock key (distinct from sqlx's) so exactly one replica builds during a rollout. Tests (against a real Postgres) cover all four reconcile states plus the no-op seeding case: absent->build, valid->no-op, invalid->drop+rebuild, lock contention skips cleanly, and a run against the already-valid seeded covering index changes nothing. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
thejefflarson
deleted the
thejefflarson/jef-580-in-app-online-ddl-lane-for-non-transactional-locking-ddl-adr
branch
July 28, 2026 04:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements ADR 0021: builds the in-app online-DDL lane that runs
CREATE/DROP INDEX CONCURRENTLYoutside the transactionalsqlx::migrate!migrator.server/src/online_ddl.rs(new): a declarativeDESIRED_INDEXESlist plus a runner. Seeded with the existing covering index (metric_series_rollups_name_bucket_covering_idx), copied verbatim from migration 0017 — since it already exists and is valid in prod, the lane's first run there is a no-op that proves the mechanism without changing anything (JEF-591 will later narrow this entry; out of scope here).pg_index.indisvalid, notIF NOT EXISTS: absent → build, valid → no-op, INVALID (left by an interruptedCONCURRENTLYbuild) → drop + rebuild.PgConnection(db::online_ddl_connect_options:statement_timeout=0,lock_timeout=3s, boundedmaintenance_work_mem=64MBfor the Pi's limited RAM), outside any transaction, guarded by its ownpg_try_advisory_lockkey (0x004A_4546_5F35_3830, distinct from sqlx's own hash-derived migration lock) so exactly one replica builds during a rollout and others skip cleanly.main.rs: the lane istokio::spawned only after the HTTP listener binds, so an index build never blocks boot or/healthz. Fire-and-forget — a failure is logged (tracing::error!), not fatal; reads fall back to whatever index already covers the query.Test plan
Added 5 tests in
server/src/online_ddl.rsagainst a real Postgres (DATABASE_URL), covering every reconcile state plus the seeded no-op case:absent_index_is_built— no index → reconcile builds it, ends Valid.valid_index_is_a_noop— pre-built valid index → reconcile leaves itspg_classoid unchanged (no drop/rebuild).invalid_index_is_dropped_and_rebuilt— force an INVALID index via aCREATE UNIQUE INDEX CONCURRENTLYthat fails on a duplicate-key violation, then reconcile drops and rebuilds it to Valid.advisory_lock_contention_skips_cleanly— hold the lock on a separate connection first; a contended run returnsOk(())and leaves the index Absent (never attempted).seeded_covering_index_run_is_a_noop— against the real seeded index (built by migration 0017), a fullrun()leaves its oid unchanged.Commands run:
cargo fmt --check— cleancargo check— cleancargo clippy --all-targets -- -D warnings— cleanDATABASE_URL=postgres://watcher:watcher@localhost:5432/watcher cargo test --locked— 133 passed, 0 failed (67 lib unit tests incl. the 5 new ones, 66 integration tests)Also ran
/soundcheck:pr-review(no Critical/High findings — all SQL string interpolation is over hardcoded constants, never user input) and a/simplifypass (factored a sharedbuild_indexhelper out of the absent/invalid reconcile branches, deduped a repeated test oid-fetch intorelation_oid).Closes JEF-580