Repository navigation
Conversation
… sync and verify Adds `node apps/relay/dist/database-move-cli.js` to the relay image, so the step-4 move of orca_relay off the shared auth instance can run as a Cloud Run job with no restart and no flag change on that instance. - install-tracking / uninstall-tracking: an unlogged relay_move_dirty table with a sentinel row, and one statement-level trigger set per relay table (generated from the catalog, cross-checked against relayPostgresSchemaStatements()). The trigger marks old and new keys with ON CONFLICT DO UPDATE, skips the tool role, and is installed one table at a time under a 200 ms lock wait with retries. - prepare-target: the relay schema on the dedicated side, the authority and copy progress tables on both, grants for the tool role, and a schema/collation compare. - copy: primary-key keyset pages, parents first, rows as Postgres-made JSON text (no JavaScript number parsing), paced by bytes/s, resumable from a position saved with each page; each page also deletes target rows the source no longer has. - sync: claims dirty keys with DELETE ... FOR UPDATE SKIP LOCKED into a claimed journal (replayed after a crash), reads current rows in one snapshot, applies them FK-safe, and stops on a missing sentinel. One syncer per source (advisory lock), and never into the side the relay is serving. - verify: row counts and md5 over primary-key ranges, listing differing keys. Co-Authored-By: Claude <noreply@anthropic.com>
…ds, needs every trigger - copy and sync re-read both sides' tables every 30 s and before each copied table. A table whose columns or keys changed stops the run with schema_changed_during_run, and its copy position is reset, since rows already copied may lack the new column. - copy is paced on what the source reads from disk for the table (heap, index and TOAST block reads from pg_statio_user_tables x block size), or the JSON size if larger, plus a rows/s cap (--max-rows-per-second, default 2000). A bloated table such as relay_control_connection_reservations costs its dead pages too. - copy, sync and the cutover refuse while any relay table on the source lacks one of its tracking triggers, not only when the sentinel is gone. Co-Authored-By: Claude <noreply@anthropic.com>
…bility format #29405 now marks each process as ' mv1:<id>'. The tool's relay-role sessions carry the same mark with one id per tool run, so they never count as unmarked. Co-Authored-By: Claude <noreply@anthropic.com>
…t is missing (Postgres < 17) Co-Authored-By: Claude <noreply@anthropic.com>
After uninstall-tracking and install-tracking on a side, relay writes made in between were never recorded, yet the copy position still said every table was done, so copy reported 0 rows and the dedicated copy stayed stale. - Each arming of tracking puts a fresh id in the sentinel's key, and every copy position records the id it was copied under. - copy and sync refuse with copy_predates_tracking (run copy --restart) when any position on the target was written under another arming, including positions from before this change. - uninstall-tracking on a side a cutover fenced says so, and points at dropping the old database through the Cloud SQL API, which needs no login. Co-Authored-By: Claude <noreply@anthropic.com>
…rget; authority move columns - uninstall-tracking deletes the sentinel first, in its own commit, so an untrack that stops partway leaves no arming for a later install to keep. install-tracking also starts a new arming when it finds a table without its triggers. - sync (and, in part 2, the cutover) refuse with target_not_current unless the target has a finished copy under the source's current arming, or the source's arming is unchanged since it became the target itself (a move back). - prepare-target adds `superseded` and `tracking_arm` to relay_database_authority for the cutover to write. Co-Authored-By: Claude <noreply@anthropic.com>
…ed column - Creating relay_database_authority on primary starts the period in which a process serves only after a successful pointer read, so prepare-target now refuses unless database-target.json already exists and is readable. - The `superseded` column is no longer added; the relay moves to a stateless rule (#29405). An existing column is left alone. - The pointer store moves into this PR (prepare-target reads it) and gains a generation-matched delete for `finish`. Co-Authored-By: Claude <noreply@anthropic.com>
|
Closing this public-repository PR because its core is deployment operator tooling, which moved to private I reviewed the full body, production file scope, CLI entry point/import graph and regression intent. This PR provides operator-invoked change tracking, paced copy, synchronization and verification. It is not called by the relay serving path. The new production modules are confined to the database-move CLI graph; the shared secret change only extracts the existing text fetch into readSecretManagerText for that CLI, preserving readDedicatedDatabaseCredentials behavior. The tool's migration value survives, so the code and its Postgres race/cutover tests should be carried into the private operations repository if that move is still planned. I have not inspected private implementation or asserted that the database migration is complete. This is an ownership/scope closure, not an algorithm correctness review: I did not exhaustively audit the copy/cutover internals or rerun the operational tests. The separate #29405 changes the active public relay runtime's database-selection contract and is left open; private operator ownership does not invalidate that runtime feature. Relevant public ownership and runtime boundaries are unchanged in HEAD..origin/main. |
ELI5
The relay keeps its records in a database it shares with sign-in. We want to move those records to a database of their own without restarting anything. This PR adds the first half of the tool that does the move: it keeps a list of which records changed, copies everything across slowly, keeps the copy up to date, and checks that the two copies match. Nothing runs until an operator starts it.
What Changed
For users: nothing. This is a new command-line tool in the relay image (
node apps/relay/dist/database-move-cli.js <command>, meant to run as a Cloud Run job). No running relay process calls it.How it works:
install-tracking,uninstall-tracking). Inside the relay's own database, a small unlogged table (relay_move_dirty) records the primary key of every changed row. A trigger on each relay table adds the key, for inserts, updates (old and new key) and deletes.prepare-targetrefuses unless the pointer object already exists: once primary hasrelay_database_authority, processes serve only after reading the pointer (feat(relay): follow a database-target pointer with a hold-and-switch between two Postgres targets #29405), andfinishends that period.prepare-targetdoes. Creates the relay schema on the new database exactly as a director would. Adds the move's bookkeeping tables and the tool role's grants on both sides, then refuses if the two schemas differ (columns, types, keys, collation).copy. Copies each table in primary-key order, parents before children, in small pages, paced to about 1 GB/h of source disk reads (block reads frompg_statio_user_tables, so a bloated table's dead pages count) and a rows/s cap. Rows travel as JSON text that Postgres writes and reads itself, so large numbers keep every digit. The position is saved with each page, so a stopped copy resumes. Each page also removes rows on the target that the source no longer has.sync. Repeatedly takes a batch of changed keys off the list and copies their current rows (or deletes them on the target if they are gone).install-trackingthat creates the change list gives it a new id, and each copied table records the id it was copied under. After an uninstall and reinstall,copy,syncandcutoverrefuse (copy_predates_tracking) untilcopy --restartredoes the copy, because writes made while tracking was off were never recorded.verify. Compares row counts and checksums over key ranges, and lists the keys that differ.Why
cloudsql.logical_decodingflag on the source. Cloud SQL lists that flag as restart-required, so turning it on restarts the shared sign-in database. The staging restart on 10-11 produced 503s. Triggers need nothing from the instance: the relay's own role owns its tables and can add triggers to them.ON CONFLICT DO UPDATEin the trigger: it locks the key's row until the writer commits. The sync'sFOR UPDATE SKIP LOCKEDclaim then passes over a key whose change is not committed yet, so a change can never be lost between the sync's read and the writer's commit. The race test below showsDO NOTHINGloses changes.row_to_json→jsonb_populate_recordsetkeeps types and BIGINT precision with no per-column code.Part 2 (fence and cutover) is stacked on this PR. This PR is stacked on #29405.
Linked Issue
Fixes #
Visual Proof
N/A: an operator command-line tool, no UI.
Testing
Local Postgres 17 and 16 (two databases with separate roles stand in for the two instances):
Trigger race: 6 writers hold transactions open after the trigger marks a key, while the sync claims as fast as it can. Over 6 rounds (about 12,000 writes), the copies match exactly every time. With the trigger switched to
DO NOTHING, the copies diverge in the first round.The tool role's writes are not tracked; old and new keys are both marked on a key change.
A role other than the owner that writes relay rows keeps working once tracking is on. Without the grant this adds, its insert fails with permission denied.
BIGINTs of 2^63−1 and 2^53+1 survive copy and sync exactly.
Parents and children (the fence-attempt tables) sync one key per batch in any order, including deletes. A unique value swapped between two keys in one batch settles; the old query failed with a duplicate key.
A copy killed mid-table resumes and copies only the rest. A rerun removes rows deleted on the source while untracked.
An emptied change list stops the sync until
copy --restart.verifynames the exact differing keys.CLI: bad flags and missing credentials are refused before any connection.
pnpm typecheckpasses; the full relay suite (with part 2 on top) passes locally: 167 files, 1,430 tests.I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
Agent skill upstream boundary
Notes
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)