fix(helm): validate postgres dump before dropping the staging database - #3601
fix(helm): validate postgres dump before dropping the staging database#3601yodem wants to merge 2 commits into
Conversation
The staging sync dropped sefariastaging and then aborted the restore, leaving the environment empty. Two faults, both detectable in the dump file before anything destructive runs: 1. The dump carries an explicit "CREATE SCHEMA public" (TOC entry 3), because its source server owns public as a user schema. A database created from template0 already has that schema, so the entry collides -- and --exit-on-error --single-transaction turns one collision into a full rollback. --clean previously tolerated this and logged "errors ignored on restore: 2", which is why the table data still landed and the fault read as a mere job failure. 2. Production's dumps still predate the --section=post-data fix, so a restore that did succeed would produce an unindexed database. Inspect the archive with pg_restore --list first: refuse a dump with no post-data section, and filter the schema entry out via --use-list rather than pre-dropping the schema, so dumps that omit it (the PG15+ default) keep working. The database is now only dropped once the dump is known to be restorable, which also makes the backoffLimit retry harmless -- it previously re-ran the drop rather than retrying against surviving data. Refs sc-46516. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VaP5RZqJXTxVrhzfnToAdZ
📊 Code Quality Score: 8/100
Was this score accurate? 👍 Yes · 👎 No Scored by GitVelocity · How are scores calculated? |
Verified the patterns against the real production archive with pg_restore 18.3 (the version the CronJob runs). The dump carries two public-schema TOC entries, not one: 3; 2615 2200 SCHEMA - public postgres 3167; 0 0 COMMENT - SCHEMA public postgres The first commit filtered only the CREATE SCHEMA entry. The COMMENT survived it and needs ownership of the schema to apply -- it succeeds today only because the job connects as a superuser, which is a property of the current credentials rather than of this code. Widen the pattern to cover both, and log entry counts. A filter that silently matches nothing is exactly the failure being guarded against here, so it should be visible in the job log rather than inferred. Confirmed pg_restore 14 and 18 emit byte-identical --list output for this archive, so the pattern is not tied to the client version. Refs sc-46516. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VaP5RZqJXTxVrhzfnToAdZ
Verified end-to-end against the real production archiveRan the real dump ( Control (current master's logic, no Reproduces the production failure exactly, empty database included. With this PR's 265,345 rows — matching the count the runbook recorded for staging before the wipe. The The archive carries two public-schema entries, not oneThe first commit filtered only the first. The second commit widened the pattern: The Coverage
The third row is the one that matters going forward: once the backup fix ships a dump with post-data, this proceeds and restores. Not exercised: a live drop/create/restore against staging itself. That can only happen once a post-data dump exists, and it will be this code's first production run. |
Correction to my "Note for reviewers"I claimed this job connects to the instance holding production's Production runs in a separate cluster ( The guard still matters, just for a different reason: the dev cluster's Nothing in the code changes as a result — the guard and the pre-flight validation are unaffected. Correcting the record so the risk isn't misfiled. Related, and the reason this PR can't go green yetProduction's backup CronJob still carries the hardcoded override #3594 removes: - name: DATABASES_HOST
value: postgresThe 2026-08-09 run logged |
Follow-up to #3594, which is merged and running. The new drop-and-recreate sync failed four times today and emptied staging's Postgres in the process.
What happened
The dump carries an explicit
CREATE SCHEMA public— visible in the pre-#3594 logs asTOC entry 3; 2615 2200 SCHEMA public postgres— because its source server ownspublicas a user schema. A database createdTEMPLATE template0already has that schema, so the entry collides on entry 3, and--exit-on-error --single-transactionrolls the whole restore back.#3594 read those two
--cleanerrors as artifacts of--clean. Only theDROP SCHEMAhalf was; theCREATE SCHEMA publichalf lives in the archive unconditionally and survived the switch to drop-and-recreate.--cleanhad tolerated it (errors ignored on restore: 2) and the table data landed anyway — which is exactly why this read as "the job fails but data is fine".Impact
Staging is empty. Verified on
postgres-18:auth_userrowssefariastagingsizesefaria_auth: 547 MB)django_migrationsapplied2026-08-11 12:08:44Every migration bears one timestamp three minutes after the 12:05:39 failure, and the only populated tables are Django's own bookkeeping (
auth_permission,django_content_type,django_site). The app reconnected to the empty database and rebuilt the schema — the indexes are migration output, not restored data.The fix
Inspect the archive with
pg_restore --listbefore dropping anything:--section=post-datafix, so a restore that did succeed would produce an unindexed database. This moves fix(helm): repair staging production-data sync #3594's post-restore index assertion to where it can still prevent harm; the post-restore check stays, now confirming post-data landed rather than discovering it was absent.--use-listrather than pre-droppingpublic, so dumps that omit the entry (the PG15+ default) keep working.Ordering matters beyond this one bug:
backoffLimit: 1gives two pods per job, and both ran the drop today. A retry re-dropped rather than retrying against surviving data. With the drop gated behind a validated dump, the retry is harmless.Verification
helm templaterenders;bash -non the extracted container script passes.Note for reviewers
This job authenticates to
postgres-18— the instance that also holds production's 547 MBsefaria_auth— assefaria, which is a superuser. The exact-string guard onpostgres.db != sefaria_authis the only barrier, and it covers one name;pre-mdl(203 MB) andsefaria_audit(134 MB) sit on the same instance unguarded. Out of scope here, worth a follow-up.The job will stay red until production ships #3594's backup fix and produces a dump with post-data — correctly so, and now without cost. Repopulating staging is a separate decision: the 08-09 dump would restore ~12-week-old unindexed data.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VaP5RZqJXTxVrhzfnToAdZ