Repository navigation
feat(backup): scope a backup to one or more databases (#1084) - #1090
Merged
Merged
Conversation
POST /api/v1/backup takes databases: [...]. A scoped backup copies the named databases' data files, their _schema/ anchors and their _compaction_state/ manifests and nothing else; the manifest records scope, the list and the status endpoint show it, backup_type stays full, and an unscoped manifest is byte-identical to before. include_metadata and include_config default to false when scoped and an explicit include_metadata is refused. Names use the storage-segment rule; a known database has tier rows, a listable object under <db>/ or _schema/<db>/, or, failing those, hidden keys under <db>/. The prefix tests go through a new bounded storage.PrefixProber on the local, S3 and Azure backends. The scoped databases' Iceberg namespace directories are excluded and counted. Cluster: the manifest cross-check and the late-arrivals recheck consider only entries whose path first segment is in scope. A scoped backup restored with no mode runs in replace on a node whose Raft manifest is wired, resolved by one function the handler and the manager share; replace selects the entries to remove by path first segment, and is refused when the backup holds no data files for one of its databases, since it would only delete. A non-empty request body must be JSON-typed: a form-typed body parsed into an empty request and ran a whole-instance backup. Closes #1084
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.
Closes #1084. Stage A of the internal backup series (#1083–#1087); community contributions are not accepted for this series.
Summary
POST /api/v1/backuptakesdatabases: ["audit", ...]. A scoped backup copies those databases and nothing else: their data files, their_schema/anchors and their_compaction_state/manifests (parked.quarantinedones included). The manifest recordsscope; the list and the status endpoint show it; the 202 echoesdatabases.backup_typestaysfull. Empty or absent is today's whole-instance backup, byte for byte (unscoped manifests have noscopekey).include_metadata/include_configdefault to false when scoped; explicitinclude_metadata: true→ 400 (the SQLite is every database's state). Names use the storage-segment rule (isSafeStoragePathSegment), no reserved roots, no duplicates, ≤ 256; unknown names → 400 naming them. "Known" = tier rows (one indexed query, first), or a listable object under<db>/, or under_schema/<db>/, or, only after those three said no, hidden keys under<db>/(so the all-unaddressable fatal still names the real problem). The two prefix tests go through a new boundedstorage.PrefixProber(local walk with early stop, S3/Azure first accepted key per page); backends without it fall back toListObjects.["prod"]does not include edge-syncspoke1/prod,["spoke1"]takes the spoke with its anchors and compaction state (_schema/<spoke>/<db>/is an exact second-segment rule,_compaction_state/<tier>/<spoke>/<db>/a boundary-prefix rule on the database part; both agree).iceberg_namespace_files_excluded,iceberg_namespaces_excluded), at the root or under a subdirectory warehouse (fix(iceberg): warehouseRelKey must trim the storage root, not the warehouse #534 shape) or in an outside-root warehouse; an object-store warehouse is excluded but not counted (documented).Database, which is canonical for spokes).findUnaddressableenumerates only the scope's prefixes.backup.ResolveRestoreMode(raw, scope, clustered)is the one resolver; a scoped backup with nomoderestores inreplaceon a node whose Raft manifest is wired (handler and manager agree viaManager.ClusterManifestWired(), not the coordinator's presence); explicitmergehonoured; standalone stays merge; the 202 echoes the effective mode and restore progress carriesscope. Replace selects the entries to remove by path first segment when scoped. Replace is refused (400 / failed) when the scoped backup holds no data files for one of its databases (fully cold, or dropped and re-created with stale tier rows): it would only delete.restart_required/stagedonly when the backup actually holds metadata/config.-ddefault (form) was parsed by Fiber into an empty request and ran a whole-instance backup (seen live); now 400. Empty body still means the defaults. arcli already sendsapplication/json.backupManager.SetTierLookup(tieringManager)next toSetTierRecorder;tiering.Manager.DatabaseHasTierRows(nil-receiver safe).arcli backup create --database(arcli#44), scoping below the storage-root segment.Review
Configuration matrix + one deep reviewer + a cluster-ops reviewer (CLAUDE.md). Folded in: the pure-delete replace of a zero-file scoped backup (Blocker, both reviewers → refusal); form-typed bodies running whole-instance backups (High, also seen live); whole-root hidden-key walk on scoped runs (High); manager belt for
include_metadata; Iceberg exclusion count moved before the data copy; replace admission keyed on the manifest hook;restart_requiredgating; restore progressscope; doc fixes.Test plan
gofmt -l,go build,go vetclean (-tags=duckdb_arrow, plusobjectstorefor storage)go test -race:internal/storage,internal/backup,internal/tiering,internal/api(duckdb_arrow, ~2 min),cmd/arcgo test -overlay), each alone with-race -count=20TestRestoreBackupModeValidation/clusterencoded the old keying (coordinator present ⇒ replace admitted); replace admission is now keyed on the Raft manifest hook, and the coordinator-without-Raft cell moved toTestRestoreBackupEchoesTheResolvedMode-tags=objectstore) including the newTestS3HasObjectsUnderPrefixagainst a live SeaweedFS 4.47a/+_schema/a/, manifestscopeset andbackup_typefull, metadata refusal, unknown/traversal/duplicate/empty/reserved names → 400, form/text/no-Content-Type/malformed bodies → 400, fully cold database (no hot files, no anchor, tier rows only) accepted and completed with 0 files, unscoped manifest has noscope, scoped merge restore touched onlya, replace on standalone → 400b; scoped["a"]reports none and holds onlya; scoped restore with no mode echoes and runsreplaceunder the backup (stage 0 follow-up): a replace restore can race an in-flight compaction job; add a cluster-wide compaction pause #1087 pause (5 acks),buntouched on every node; explicitmergehonoured; no warn/error linesaz): unknown name → 400 through the real probers (0.03 s on S3, 0.35 s on Azure, three probes plus the hidden-key enumeration), scoped backups held only the scope, scoped merge restore wrote the files back into the object store and the query saw them; no warn/error linesreplace) and an explicitreplaceboth answered 400 with the refusal, an explicitmergecompleted with 0 files under the pause, and the other database was untouchedFiled from the live runs: #1091 (a dropped database keeps its tier rows and cold objects) and #1094 (
DELETE /api/v1/databases/:nameon a cluster node removes only that node's files; the manifest entries and the replicas on the other nodes remain, which the cluster run showed asmanifest_only_files: 2on the scoped backup).