feat: add neon-drill, loading restored backups into Neon branches - #37
Open
leochong wants to merge 2 commits into
Open
feat: add neon-drill, loading restored backups into Neon branches#37leochong wants to merge 2 commits into
leochong wants to merge 2 commits into
Conversation
restore-test proves a backup restores, then deletes the cluster it timed. Nobody can query the result, so "the bytes came back" is as far as the evidence goes. neon-drill runs the same rehearsal and leaves the recovered data in a throwaway Neon branch, which is cheap, isolated, and reachable by an ordinary connection string. It is two-stage by necessity. A wal-g physical backup cannot be restored into Neon: Neon keeps pages in its own pageserver, so there is no data directory to write and no replication protocol to stream into. Neither backup-fetch nor backup-push can address it. The drill restores physically into a scratch directory, starts the cluster, and dumps it logically into the branch. The physical restore remains the thing under test; the branch is what it leaves behind. The Neon load is deliberately outside the RTO verdict. It is a transfer bounded by dump-and-load throughput, and judging it against a recovery budget would fail the drill for reasons unrelated to backup health. Safety, since a branch is a billable compute endpoint: - neon-auth runs first, so bad credentials fail in seconds rather than after an hour of restoring a backup that has nowhere to go - the branch is deleted on every exit path, interrupts included - only branches carrying the walg-drill- prefix are ever deleted, so a bug in cleanup cannot destroy a branch somebody created by hand - the branch password reaches psql through the environment, never argv - the API key is scrubbed from control-plane error text, which a test pins: an endpoint echoing the Authorization header would otherwise put it straight into a CI log pkg/neon sits outside pkg/storages and is not registered in StorageAdapters. Neon cannot hold backups, and registering it as a storage backend would imply it can. runReplayPhase gains a leaveRunning parameter: a plain drill stops the restored cluster immediately, while the Neon drill dumps from it next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--source-database defaulted to "postgres", which is empty on almost every cluster. A drill using the default would dump nothing, load nothing, and report a pass - the exact false assurance the command exists to prevent. The restored cluster is already running by the time the dump starts, so its catalog is the authoritative answer. The drill now queries pg_database and picks the single connectable, non-template database that is not the maintenance one. The ambiguous cases are explicit rather than guessed: - several user databases: stop and list them. A Neon branch holds one database, so choosing here could load the wrong data under a green verdict. - nothing but postgres: proceed, but warn instead of passing, because the drill has almost certainly moved an empty database. An explicit --source-database or WALG_NEON_SOURCE_DATABASE is never second-guessed and skips the query, so the drill still works when the catalog cannot be read. The catalog query builds an explicit DSN rather than going through Connect(), which falls back to localhost:5432 and would otherwise be able to inspect the live cluster instead of the restored one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Why
restore-testproves a backup restores, then deletes the cluster it timed. Nobody can query the result, so "the bytes came back" is as far as the evidence goes.neon-drillruns the same rehearsal and leaves the recovered data in a throwaway Neon branch — cheap, isolated, and reachable by an ordinary connection string, so a person or a test suite can go and look.The constraint that shaped the design
A wal-g physical backup cannot be restored into Neon. Neon keeps pages in its own pageserver, so there is no data directory to write and no replication protocol to stream into. Neither
backup-fetchnorbackup-pushcan address it — localbackup-pushasserts wal-g runs on the same host and callspg_backup_start; remote mode needsREPLICATIONforBASE_BACKUP; and both first callGetPgServerInfo, which needspg_control_system(). Neon grants none of it.So the drill is two-stage by necessity:
The physical restore remains the thing under test. The branch is what it leaves behind.
The Neon load is deliberately outside the RTO verdict. It is a transfer bounded by dump-and-load throughput; judging it against a recovery budget would fail the drill for reasons unrelated to backup health.
rtocoversfetchandreplay; the load is reported asneon_load_seconds.What's here
pkg/neon— control-plane client: branch create/list/delete, connection URI, async operation polling. Sits outsidepkg/storagesand is not registered inStorageAdapters— Neon cannot hold backups, and registering it as a backend would imply it can.internal/databases/postgres/neon_drill.go— reuses the existing drill machinery (ValidateDrillTarget,spacePhase,runFetchPhase,writeRecoveryConfig,stopDrillCluster, theDrillPhase/DoctorStatusvocabulary) rather than duplicating it.NeonDrillOptions/NeonDrillReportembed their restore-drill counterparts, so the restore half reports identically to a plain drill.cmd/pg/neon_drill.go,cmd/pg/neon_branches.go— the latter carriesNoStorage, since it never reads backup storage.WALG_NEON_*config keys; the API key joinssecretSettings.PostgreSQL.md,BACKUP-RECOVERY.md,README.md,COPYRIGHT.md.Which database gets dumped
The drill does not guess. Once the restored cluster is running it queries
pg_databaseand picks the single connectable, non-template database that is notpostgres. The ambiguous cases are explicit:neon-loaddetail;postgres— proceeds, but warns rather than passing, since it has almost certainly moved an empty database.An explicit
--source-database/WALG_NEON_SOURCE_DATABASEis never second-guessed and skips the query, so the drill still works when the catalog cannot be read.The catalog query builds an explicit DSN rather than going through
Connect(), which falls back tolocalhost:5432and would otherwise be able to inspect the live cluster instead of the restored one.Safety
A branch is a billable compute endpoint, so:
neon-authruns first — bad credentials fail in seconds, not after an hour of restoring a backup that has nowhere to goSIGINT/SIGTERMincludedwalg-drill-prefix are ever deleted, so a bug in cleanup cannot destroy a hand-made branchpsqlthrough the environment, neverargvAuthorizationheader would have put the key straight into a CI logShared-code change
runReplayPhasegains aleaveRunningbool. A plain drill stops the restored cluster immediately; the Neon drill dumps from it next. One call site updated.Verification
pkg/neontests usehttptest(operation polling, list filtering, delete refusals, 404-as-success, rate limiting, key-never-leaks).--keep-branchretains, failed delete reportsfail, plus the source-database selection cases../cmd/... ./internal/... ./pkg/... ./utility/...is green exceptTestInterpretTypeSymlinkandTestHandlePITRWindow_MinWindowGate, both confirmed pre-existing by running them ata7e17e9bin a scratch worktree.misspellhits are suppressed with//nolint:cancelling/cancelledare Neon's wire values, and Americanising them would stop them matching.A test also caught a real parser bug: packaged builds print
pg_dump (PostgreSQL) 15.6 (Debian 15.6-1.pgdg120+2), and taking the last field yielded(Debian).Not covered by tests: the
pg_databasequery and the dump/load pipe both need a live cluster, so they are exercised only by the integration path — same as the existingqueryRecoveryPoint.Scope / follow-ups
Per the agreed scope this is the create-and-load side plus listing. Deferred: data-correctness verification (row counts/checksums) and wiring into
doctor/backup-verify/retention-validate/pitr-window.🤖 Generated with Claude Code