Repository navigation
Conversation
`extenddb backup sync --move-store <old root>` re-points the catalog rows of a store root whose tree was copied under the configured root, so the backups keep their ARNs. The old root is given as the catalog stores it and matches byte for byte; the configured root and a root with no rows are refused, the latter printing the stores the catalog holds rows for. The move needs the configured store healthy, takes the maintenance claim on every backend (on SQLite under the serve lock as well) and waits for the running jobs of the configured store and of every store id the catalog records for the old root. It then checks each row of the old root against the configured store. A CREATING row whose owner is dead is resolved by the reconciler's per-row resolution, now one body shared with the dead-owner duty; one with a live owner stops the move. A DELETING row whose copied prefix holds no verified manifest has its deletion finished in the configured store behind the deletion fence. A DELETING, DELETED or FAILED row whose copied prefix holds a verified manifest stops the move, naming `--forget-backup`. An AVAILABLE row needs its prefix complete under the configured root (`probe_manifest`: the manifest, the sidecar, the digest, the row's ARN, every data file at its recorded size) and the manifest's size and item total equal to the row's. A row that would give an account two rows of one backup id stops it. Every stopping row is named in one pass, and none moves while any does (exit 1). Otherwise the fence re-reads the identity file once, `rewrite_store` moves the rows, the old root is listed again (a row still there exits 1), and a plain sync of the configured root follows. Each row moved, resolved or finished is logged under `extenddb::audit`; no object is copied and nothing under the old root is touched. The peak memory is the backends' read of the old root's rows: 569 MiB at 20,000 rows of a five-GSI table on a SQLite debug build and 500 MiB on a PostgreSQL one (51 and 49 MiB at 50 rows), 37 s each. `--forget-store <root>` removes every row of a root other than the configured one outright, and `--forget-backup <arn>` (repeatable, in order, up to the first refusal) one backup's row, of any status: no tombstone is left and no object is deleted, so no deletion duty ever acts on those bytes. Both run on a store in any state and with none configured, under the claim on every backend and after the wait. Each status uses a removal that exists: AVAILABLE `forget_available_backup_row` with its in-use rule, DELETED and FAILED `remove_backup_row`, CREATING `remove_dead_creating_backup_row`, DELETING `set_backup_deleted` then `remove_backup_row`. They leave a row a restore is reading, a CREATING row with a live owner and a FAILED row whose heartbeat is within the owner window, and exit 1 naming each. `--forget-backup` names a row under the ARN's other spelling without removing it, reports an in-catalog backup as one and changes nothing, and then runs the configured store's check once and prints it, naming the next backup whose manifest is missing. That is the way out of the unhealthy store a restored catalog is left with when its newest AVAILABLE backup was deleted after the catalog's backup; `--prune-missing` keeps refusing there, and plain sync's refusal names `--forget-backup`. `--forget-store` reports the store ids the catalog still records for the root with no row behind them (Cassandra's entries whose removal failed). The three options conflict with each other, with `--adopt-store` and with `--prune-missing`. Every sync that listed the root to its end now prints the rows of the other stores: per store (kind, root, store id) the rows per status, each CREATING and DELETING row by ARN, and the two options that act on them. A failed read of them exits 1. `BackupCatalog` gains `list_backup_rows_by_root(kind, root, status)`, `rewrite_store(from_kind, from_root, keys, to)`, which rewrites only rows still of the old kind and root and not CREATING and returns the count, and `summarize_store_roots()`, the rows per store and status. PostgreSQL runs one `UPDATE` over `unnest` of the keys and one `GROUP BY`; SQLite the same under the write lock and `BEGIN IMMEDIATE`; MongoDB `update_many` in chunks of 1,000 keys that `$inc` `lock_seq`, and a `$group`; Cassandra, per key, a serial read, the new root's status entry, the lightweight transaction conditioned on the old kind, root and status, then the old entry deleted (the module doc says why this write puts the entry first), an unknown outcome settled by a `LOCAL_SERIAL` read, and the summary a count of status entries. The engine's `Fake` and the server's `StubCatalog` implement all three. `crates/engine/src/backup/store_maintenance.rs` holds `move_store`, `forget_store`, `forget_backups`, `locate_root` and `other_stores`. `maintenance::wait_for_quiet` takes a slice of store identities. The reconciler's `Duty::fenced` re-reads the row after the fence and skips one that is gone or whose status or store changed, so a cycle that listed a row before a forget or a move acts on nothing. `OTHER_STORE` names the move. Tests: per-backend cells for the three methods (Cassandra's direct tests on a node of their own, with stop and unknown-outcome hooks); 13 engine cells on `Fake` and `MemStore`; 6 CLI unit tests; wire cells on SQLite, PostgreSQL and MongoDB: a copied tree moved, a FAILED row over a complete copy and a missing data file each stopping the move, `--forget-backup` letting it finish, and a server on the new root restoring a moved backup; another root's rows left alone by a reconciler, reported, removed by `--forget-store` and adopted again when the root returns; a store left unhealthy by its newest backups' missing manifests cleared by `--forget-backup`; `--forget-backup`'s refusals, with no store configured too; on PostgreSQL a CreateBackup refused during the move's wait, and the three options refused while another command holds the claim; on SQLite the three refused beside a server (exit 6); `--move-store` refused on each unhealthy store shape. 49 mutants of the new tests, all killed. Docs: the backup manual's restored-catalog paragraph (the `--forget-backup` procedure) and a new section, "Moving a store to a new root"; the admin guide's synopsis, sample output, the full report of other stores in place of "reported in part", and two paragraphs on the three options; the deployment guide's shared-root rule; the differences doc's "Where backups live" row. Design revision item 36, with the PR 10 section's split line, interfaces block and sync contract and section 7's memory paragraph edited with it. Not here: `migrate-legacy` (#821); a sweep of Cassandra's orphan status entries (reported, not removed); detecting a server left configured with the old root (the manual says to reconfigure every instance first). Co-authored-by: Lee <leeroyhannigan@yahoo.ie> Signed-off-by: Scott Robinson <robinnsc@amazon.com>
This branch has not been deployed
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.
Backup/restore series (design: #396): #384 restore fidelity (merged) · #393 store · #394 format · #395 SnapshotEngine · #666 catalog migration framework (merged) · PR 6 the catalog index · PR 7 configuration, the store identity file and
backup purge· #700 the CreateBackup job · #740 the restore job, the cross-backend matrix andbackup remove-target· #825backup sync,--prune-missing,--adopt-store· #855 the CI filters, the open measurement cells and the backup and restore manual · this PR:backup sync --move-store,--forget-store,--forget-backupand the report of the rows of other stores · next: #821migrate-legacy, #748 export and import through the store.Closes #820 (part of #624).
Stacked on #855 (
feat/backup-docs, head88d445da), itself on #825 (feat/backup-sync,358ff8bd) and #740 (9803ab7f); opened as a draft againstfeat/backup-docs. One commit. No migration, no schema change, no new dependency;Cargo.lockis unchanged. The design's PR 10 ships as three PRs (revision item 34): #825 is the catalog-loss recovery, this PR the options that rewrite or delete rows of other stores, #821migrate-legacy. Every binary below is a debug build of this tree (88d445daplus this diff).What
Storage trait (
crates/storage). ThreeBackupCatalogmethods, with no default body.list_backup_rows_by_root(kind, root, status): every row of the kind and root whatever its store id, of one status or all, inlist_backup_rows' order, the root matched byte for byte (an S3 root's?endpoint=suffix included).rewrite_store(from_kind, from_root, keys, to) -> u64: each row ofkeysthat is still of the old kind and root and not CREATING getsto's kind, root and id; the count of rows rewritten. The design's signature had no "from" arguments; with them a row another writer moved, or the CREATING row of a server still configured with the old root, is left rather than moved without its bytes. A partial rewrite (between MongoDB chunks, between Cassandra keys) leaves rows under both roots, which the trait doc states: every step is idempotent and the command names what is left.summarize_store_roots() -> Vec<StoreRootCount>: the rows per store (kind, root, id) and status, in one order on every backend (StoreRootCount::sort), for sync's report; the design had no method that lists the roots.BackupStatus::ALL. The docs offorget_available_backup_row,remove_backup_row,set_backup_deletedandremove_dead_creating_backup_rowname their new callers.PostgreSQL, SQLite, MongoDB, Cassandra. PostgreSQL: the by-root listing is
list_backup_rows' statement without the id (the(store_kind, store_root)prefix ofidx_backup_index_store_status);rewrite_storeoneUPDATE ... WHERE (account_id, backup_id) IN (SELECT * FROM unnest($4, $5)) AND store_kind = $6 AND store_root = $7 AND status <> 'CREATING', whose row lock orders it against a restore'sFOR UPDATEand DeleteBackup's transaction; the summary oneGROUP BY. SQLite: the same statements,rewrite_storeoneBEGIN IMMEDIATEtransaction under the write lock, all rows or none. MongoDB:findon kind, root and status;update_manyin chunks ofREWRITE_CHUNK_KEYS= 1,000 keys, filtered on the old kind and root andstatus: {$ne: "CREATING"}, that also$inclock_seq, so a concurrentclaim_restore_txorstart_backup_delete_txon a moved row conflicts, re-runs and reads the new store; a$groupfor the summary. Cassandra: the by-root listing reads the five(root, status)partitions ofbackup_index_by_status, filters entries on the kind, and keeps a row only when its re-readbackups_by_arnrow has that status, kind and root (root_index_arns, refactored fromstatus_index_arns).rewrite_store, per key: aLOCAL_SERIALread, skipped unless of the old kind and root and not CREATING; the new root's status entry written; the lightweight transactionUPDATE backups_by_arn SET store_kind, store_root, store_id ... IF store_kind = ? AND store_root = ? AND backup_status = <read>; the old root's entry deleted (a failure logged). An unknown outcome is settled by a serial read (moved when the row now carriesto's store);NotMatchedremoves the new entry and reads again, up to three attempts, after which the key is left uncounted. The module doc gives the one exception this makes to its row-first rule: a stop after the new entry leaves only an entry every reader skips, while the row first would leave a moved row that no listing of the configured store returns. The summary counts the status entries, unverified, as the trait doc says. Test hooksfail_rewrite_writesandstop_rewrite(RewriteStop::{AfterNewEntry, AfterRow}).Engine (
crates/engine/src/backup/). Newstore_maintenance.rs, written againstBackupCatalogand the configured store:locate_root(storage, root, configured_root): the kind of the rows under a root (both kinds are tried throughread_store_ids; an S3 label starts withs3://and a filesystem label is an absolute path) and the store ids the catalog records for it;RootRefusal::ConfiguredRoot, andNoRowswith the summary, so the command can print the stores the catalog knows.move_store: reads the old root's rows (each row'ssource_table_details_jsondropped as it is read), then checks each against the configured store and names every row that stops the move in one pass. A CREATING row goes throughreconciler::resolve_dead_creating_row, split out ofresolve_dead_creating_rowsso both callers share one body (CreatingOutcome), with the configured store's calls; a row not handed over has a live owner. A DELETING row with a verified manifest under its copied prefix stops the move; otherwisefinish_backup_deletedeletes what the copy brought behind the fence and sets DELETED. A DELETED or FAILED row with a verified manifest there stops the move, naming--forget-backup. An AVAILABLE row is probed withprobe_manifest(nowpub(super)) and its manifest's size and item total must be the row's. A row that would give an account a second row of one backup id (against the configured root's rows, and among the moved rows, which only Cassandra can hold) stops the move. With none stopping,StoreFence::check_catalogre-reads the identity file once,rewrite_storeruns, and the old root is listed again: a row still there is reported as left (a CreateBackup of a server still on the old root, a rowrewrite_storedid not count); when that re-list fails after a rewrite that counted every key, every key is taken as moved. The resolutions and finished deletions are kept when the move stops. Audit lines (extenddb::audit,backup sync --move-store) for each row moved, resolved or finished.forget_storeandforget_backups: rows removed outright with the removals that exist (forget_row): AVAILABLEforget_available_backup_row, DELETED and FAILEDremove_backup_row, CREATINGremove_dead_creating_backup_rowon the catalog's clock, DELETINGset_backup_deletedthenremove_backup_row(the DELETED state between them is safe: the DELETED-prefix and tombstone duties do not run under the claim). A removal that meets another status re-reads once and applies that status's removal. A FAILED row whose heartbeat is within the owner window is left as having a live owner (FailedOwners, onelist_backup_rows_older_thanper store).--forget-backupreads the ARN as given, then under its other spelling (BackupArnSpellings), then throughLegacyBackupReader::describe, and stops at the first ARN it does not remove. After--forget-storethe root's store ids are read again and those with no row behind them reported.other_storesandgroup_counts: the summary grouped per store other than the configured one, and each CREATING and DELETING row named throughlist_backup_rows(Some(status), <that store>).maintenance::wait_for_quietandbusy_jobstake&[StoreIdentity].reconciler.rs:Duty::fencedre-reads the row after the fence passes and skips one that is gone or whose status or store changed (logged), for the FAILED-prefix, DELETED-prefix and tombstone duties; the module doc says so and words theOTHER_STOREline for the move.restore.rs:OTHER_STOREreads "...; configure that store in [backup.store], or move the backup's rows to the configured store withextenddb backup sync --move-store <root>, and restore again", as design 5.3 step 7 says.sync.rs: the module doc says that sync reads and changes no row of another store and that the command reports them.App (
crates/app/src/cmd_backup.rs).backup sync --move-store <old root>,--forget-store <root>,--forget-backup <arn>(repeatable), each with clap help, conflicting with each other, with--adopt-storeand with--prune-missing(exit 2).--move-storeneedsIdentity::Known, locates the root before the claim, takes the claim on every backend (on SQLite too, beside the serve lock, as the other options do), waits over the configured identity and the old root's ids, runs the move, prints its sections (Resolved, Finished, Stopping the move, Moved, Left under the old root) and the summary line, exits 1 unless every row moved, and only then syncs the configured root. The forget options keep the catalog open when no store is configured (openreturnsOpening::CatalogOnlyfor them), print the store's state in the header, locate the root (--forget-store), take the claim on every backend, wait (over the configured identity when known, and the root's ids), remove, and print each row's outcome and the summary line; after--forget-backup, when a store is configured,check_configured_storeruns once andrecord_checkstores its outcome, printed as "The configured store's check passes (store id ...)" or the problem, which forManifestMissingnames the next ARN and the command for it. Plain sync'sManifestMissingrefusal names--forget-backup. Every sync that listed the root to its end prints "Rows of other stores": per store the counts per status, the CREATING and DELETING rows, and the line naming--move-storeand--forget-store; a failed read of them is printed and exits 1. The module doc gains the three options.take_claim,adopt_wait_identityandroot_refusalare factored out; the line and exit decisions are pure functions with unit tests.Harness.
tests/test_backup_job.py: five new cells and three extended (below); helpers_set_root,_set_store_disabled,_copy_root,_section,_age_row.tests/test_backup_store_config.py: the serve-lock cell runs the three options beside a SQLite server.Docs and texts. The backup manual: the restored-catalog paragraph now gives the
--forget-backup <arn>procedure in place of "This release has no command that clears it"; the claim paragraph names the three options among those that wait; the configuration section points a root that has to change at the move; a new section, "Moving a store to a new root" (reconfigure every server first, copy whole with the modes, finish the copy, the identity file, the root label byte for byte, the checks per status, the stop and its way out, the summary line, the re-list, what is left untouched,OTHER_STORE, memory, and--forget-storeto drop a root's rows). The admin guide: the identity paragraph (the move), the synopsis, the exit-1 list, the sample output with the other stores' section, the newest-manifest sentence (--forget-backup), the full report in place of "reported in part", and two paragraphs on the three options with the measured memory and a sample output. The deployment guide's shared-root bullet says how to change the root later; the differences doc's "Where backups live" row says a moved root keeps its ARNs. Design: revision item 36; the PR 10 section's split line, interfaces block (rewrite_store's arguments,summarize_store_roots) and sync contract, and section 7's memory paragraph, edited with it.Why
Design 4(c), 4(g) and the PR 10 section. #825 left three gaps that this PR closes. A store root could not change without losing its rows: a tree copied to a new root has no rows there, and sync skips every prefix whose backup id a row of the old root holds, so the backups were reachable only by adopting them again under the new root's store, and a failed restore of another store had no remedy but configuring that store. Rows of a root that is gone could not be dropped without a tombstone, and a tombstone is what a deletion duty acts on. And a catalog restored from a database backup whose newest AVAILABLE backup was deleted after the catalog's backup stays unhealthy ("manifest probe failed"), which
--prune-missingrefuses, with the prefix put back as the only way out. Plain sync also reported the rows of other roots only in part.Choices to weigh
summarize_store_roots. The design's interfaces gavelist_backup_rows_by_root, which takes a root; nothing listed the roots the report has to name. The alternative was onelist_account_backup_rowsper account, which reads every row of the catalog whole (about 22.5 KiB per row of a five-GSI table) to count them. The method is one grouping query on three backends; on Cassandra it counts status entries without re-reading the rows, so a hint left by a write that stopped part-way is counted, which the report and the trait doc say. The CREATING and DELETING rows it names are read throughlist_backup_rowsof each store, which re-reads every row.rewrite_storecarries the old kind and root and excludes CREATING. Between the move's checks and its rewrite a server still on the old root can write (its CreateBackup after the wait, its DeleteBackup, which the claim does not exclude). Moving a CREATING row whose job writes to the old root would give an AVAILABLE row of the new root with no bytes there. A row another writer moved to DELETING may still move; the configured store's reconciler then finishes that deletion under the new root, which is what the DeleteBackup asked for. The rows not counted are found by the re-list and reported.read_store_idsof the new root then counts that entry; its id is the configured id, so no identity mismatch follows. One race stays at the hint level the module doc already names: a DeleteBackup that read a row before the rewrite and writes DELETING after it moves the status entry under the old root.Duty::fencedre-reads the row: one catalog read per deletion. A reconciler cycle reads the maintenance claim once, at its start, and its FAILED-prefix, DELETED-prefix and tombstone duties act on rows listed when each began. A cycle that started just before a forget's claim could delete the prefix of a FAILED row the operator forgot precisely to keep it, and with no running jobs the wait returns at once, so the window is real. The re-read also covers arewrite_storelanding mid-cycle.finish_deleting_rowsis unchanged: a DELETING row's deletion is the one an operator asked for.a_duty_whose_row_is_forgotten_or_moved_after_its_listing_acts_on_nothingpins it with aFakehook between the listing and the deletion.--forget-backupis the way out ofManifestMissing, so it cannot require a healthy store, and a deployment that turnedkind = "disabled"must still be able to drop an old root's rows; neither option deletes an object or reads the store.--prune-missingkeeps refusing onManifestMissing, which is also the host-local-root detector (4(c)): pruning while it holds would remove rows whose bytes are on another host. The store's check after--forget-backupis printed and does not change the exit: the rows asked for were removed, and the check names the next ARN to forget when another newest manifest is gone.set_backup_deletedthenremove_backup_row. The DELETED state between them is never acted on: the DELETED-prefix and tombstone duties do not run while the claim is held. On a DELETING row of the configured storefinish_deleting_rows, which does run under the claim, may finish it first; the forget then finds DELETED and removes it.--forget-storeand to--forget-backupalike.backup list. Measured at 20,000 rows (below), within 12 % ofbackup listover the same rows. Streaming the listings out of the backends is not done here; the texts state the figure.Testing done
Measured on this tree, debug builds, a 16-core Xeon Platinum 8124M host (30 GiB, Linux 5.10), the services local.
cargo fmt --all -- --checkclean;cargo +1.97.0 clippy --locked --workspace --all-targets -- -D warningsclean, and with--features extenddb-storage-cassandra/test-hooks,extenddb-storage-mongodb/test-hooks, with--features extenddb-engine/backup-test-hooks, and on-p extenddb-storage-cassandra --all-targets --features test-hooks;cargo +1.88.0 check --locked --workspace --all-targetsclean, no warning.--lib: 479 passed, 13 of them new (move_store_resolves_checks_and_moves_each_status_of_the_old_root,move_store_names_every_row_that_stops_it_and_moves_none,rows_the_rewrite_does_not_move_are_listed_as_left,move_store_on_a_root_another_catalog_adopted_moves_nothing,move_store_refuses_a_second_row_of_one_backup_id,the_wait_covers_every_store_it_is_given,locate_root_finds_the_kind_and_ids_and_refuses_the_rest,forget_store_removes_the_roots_rows_outright_and_reports_what_it_left,forget_backup_removes_each_row_in_order_and_stops_at_the_first_refusal,a_stop_before_the_first_row_forgets_nothing,forget_backup_of_the_newest_rows_deleted_backup_heals_the_store,a_duty_whose_row_is_forgotten_or_moved_after_its_listing_acts_on_nothing,the_report_of_other_stores_counts_and_names_their_rows_and_nothing_acts_on_them); every existing cell unchanged by the reconciler split and theDuty::fencedre-read. App--lib55 passed (6 new: the conflicts and the repeat, the other-stores lines, and the exit and line functions of the move, the forgets and the store check). Server--lib backup19 passed (StubCatalog). Storage--lib233 passed (the_root_tally_counts_each_store_and_status_apart_in_one_order).--lib backup67 passed, PostgreSQL--test backup_restore61 passed (--test-threads=2), each withlist_backup_rows_by_root_returns_every_id_and_status_of_the_root_and_nothing_else,rewrite_store_moves_only_the_old_roots_rows_that_are_not_creatingandsummarize_store_roots_counts_the_rows_of_each_store_and_status; MongoDB--lib -- --ignored --test-threads=1 live_71 passed, the same three aslive_cells pluslive_rewrite_store_moves_every_chunk_of_keys(1,001 keys); Cassandra, on a single 4.1 node of its own, the three plustest_a_stopped_rewrite_leaves_the_row_listed_under_one_root(eachRewriteStop) andtest_an_unknown_rewrite_outcome_is_settled_by_a_serial_read, withtest_read_store_ids_lists_the_distinct_ids_of_a_root: 6 passed (--features test-hooks --test direct_integration -- --exact).tests/test_backup_store_config.py tests/test_backup_job.pyon the SQLite build 39 passed, 9 skipped (the PostgreSQL-only cells;-n 4), on the PostgreSQL build 32 passed, 16 skipped (the SQLite-only cells;-n 2); the four new cells that run on MongoDB passed on the MongoDB build. No PostgreSQL database left (the list equal to the one taken before).--forget-backupand the move finishing, a server on the new root describing and restoring a moved backup,--forget-storeand the root's return, and the forget options withkind = "disabled".--forget-backupgoing past a refusal, the left rows not reported,locate_rootaccepting the configured root, the configured store in the report); 4 in the CLI's pure functions; 5 in the CLI's wiring against the wire cells, each with the binary rebuilt (the other stores' section always empty, no check after--forget-backup, a stopped move's exit ignored,--forget-storenot knowing the configured root, and on PostgreSQL the move's wait over the configured store only). 49 of 49 killed, each on its test's assertion.--move-store(design section 7 and the admin guide state it): a root of 20,000 AVAILABLE rows of a five-GSI table, each prefix complete under the copy (one CreateBackup and 19,999 copies with their manifests rewritten to their own ARN, adopted bybackup sync; the copy made with hard links): 569 MiB peak RSS and 37.1 s on the SQLite build, 500 MiB and 36.9 s on the PostgreSQL build (51 and 49 MiB at 50 rows);backup listover the same rows 508 and 472 MiB, and the plain sync of the new root after the move 554 and 495 MiB. Filesystem roots on a local NVMe disk, PostgreSQL on localhost: an S3 store pays a request per manifest, sidecar and prefix listing, and a network database the read's round trips, neither measured.Acceptance lines of #820 (the design's PR 10 acceptance list) and where each is tested:
the_report_of_other_stores_...; wiretest_another_roots_rows_are_left_alone_until_forget_store_drops_them(a FAILED and a DELETED row dated 2000 and a DELETING row of another root keep their rows and bytes through three cycles of a server on the configured root, and the report counts and names them) and the move cell's report--move-storere-points its rows and Describe and Restore work against the new rootmove_store_resolves_...; wiretest_move_store_moves_a_copied_tree_and_stops_on_an_incomplete_copy(SQLite, PostgreSQL, MongoDB)--move-storeresolves the old root's CREATING and DELETING rows, stops on a row whose data files were not all copied, and refuses a DELETING, DELETED or FAILED row whose copied prefix holds a verified manifestmove_store_resolves_...andmove_store_names_every_row_...(each status and each incomplete shape); wire, the move cell (a FAILED row over a complete copy, a missing data file)--forget-backuprefuses a CREATING row with a live owner and a backup from which a restore target is CREATING, and says so and does nothing for a legacy ARNforget_backup_removes_each_row_...; the backends' existingforget_available_backup_rowin-use cells (PostgreSQL, SQLite, MongoDB, Cassandra), which the option calls; wiretest_forget_backup_leaves_a_live_creating_row_and_an_in_catalog_backup(the live CREATING row on PostgreSQL; on SQLite the serve lock makes every owner dead)test_a_second_maintenance_command_fails_at_oncewith the three options (PostgreSQL)test_backup_list_and_purge_on_an_empty_store_and_the_serve_lockwith the three options (exit 6)test_prune_missing_and_the_unhealthy_store_refusalswith--move-storeon each shape; the forget options run on an unhealthy store by design (the clears cell)--forget-storeand the store's return, sync adopts its prefixesforget_store_removes_...; wire, the another-roots cell--move-storeeither finishes before the rewrite or is refusedrows_the_rewrite_does_not_move_are_listed_as_left(a CREATING row landing after the checks is left and reported) andthe_wait_covers_every_store_it_is_given; wiretest_a_create_backup_during_the_moves_wait_is_refused(PostgreSQL)--move-store Astops on a row with one data file missing and--forget-backuplets the move finish, sync reports that prefix as incomplete and does not adopt it, and adopts it once the file is copiedthe_reconciler_reports_a_held_maintenance_claim_and_still_resolves_dead_ownerscovers the cycle under the claim, andthe_wait_covers_every_store_it_is_giventhe wait the options share; theFakelists RUNNING records from a table its FAILED write does not update, so a cell joining both needs aFakechange--forget-storedeletes that root's rows outright, leaving no DELETED tombstoneforget_store_removes_...(a cycle afterwards deletes nothing); wire, the another-roots cell (backup listempty, the tree byte-identical)--move-storeand--forget-storematchstore_rootexactly as stored, the?endpoint=suffix included/, a prefix); enginelocate_root_...(the S3 label shape); wire, the move cell (a trailing/)move_store_resolves_...(the next cycle deletes a moved FAILED row's prefix and a DELETED row's objects and removes the tombstones)--forget-backupis the way outforget_backup_of_the_newest_rows_deleted_backup_heals_the_store; wiretest_forget_backup_clears_a_store_whose_newest_manifests_are_gone(SQLite, PostgreSQL, MongoDB; beside servers the running server's/healthturns healthy)Cassandra has no wire leg (its CreateBackup stays in-catalog, item 19); its share is the direct tests above.
Not in this PR
extenddb backup migrate-legacy <arn>|--all, with its acceptance cells.backup_index_by_statusentries:--forget-storereports the store ids they keep for a root, and nothing removes them yet (item 34 says the same of the status-entry orphans).Breaking changes
None.
backup sync's report gains a section, and a sync that cannot read the rows of other stores now exits 1.OTHER_STORE's text names the move; tests assert the constant by identity. Each FAILED-prefix, DELETED-prefix and tombstone deletion of the reconciler reads its row once more.DCO
Signed-off-by: Scott Robinson robinnsc@amazon.com