Repository navigation
feat(backup): record the cold-tier files a backup does not carry (#1085) - #1137
Merged
Merged
Conversation
A backup copies hot storage. With tiered storage on, the files migrated to the cold tier are not in it, and nothing said so: a manifest reading total_files: 1400 described 1400 hot files and however many cold ones the deployment had, silently. Every manifest now carries cold_files_excluded, with cold_files_excluded_databases breaking it down per database, because one total does not tell an operator which database has data the backup is missing. The figure also reaches the backup listing, the per-target detail, and a warning the restore logs. cold_files_excluded deliberately does NOT join the replace-mode incompleteness refusal. That refusal exists because replace deletes the current files of the databases it restores, so an incomplete backup loses the difference. A cold-tier file is a different population: no backup carries it yet and replace cannot delete it either, because the delete set comes only from the cluster manifest a migrated file has already left, the shared-backend delete runs against the hot backend, and the tier rows survive. Refusing on this count would prevent nothing while refusing a replace restore for every partly-cold database, making tiered storage and replace-mode restore mutually exclusive. A comment at that refusal says so and a regression test fails if the count is ever added to it. One grouped query rather than one per database: the set that matters is every database with cold rows, and that is not the backup inventory, since a fully cold database has no hot files and is absent from both the listing and manifest.Databases. Grouping covers that by construction and cannot half-fail the way N point queries can. The count is attributed per leg by the same rule the data follows, so a database routed to its own target has its gap on that target's manifest alone, and mergeRunManifests sums the legs into the run-level view every consumer reads. A counting failure warns and the backup completes: the count describes data the backup was never going to carry, so failing the run would trade a healthy backup for no backup over a diagnostic. The marker is the reporting node's view of its own tier metadata, and it is absent on a node without the tiered-storage licence even though its cold objects remain, so the field doc, the release notes and arc.toml all say an absent field is not evidence that nothing was migrated. No new config key.
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 #1085.
A backup copies hot storage. On a deployment with tiered storage on, the files that have been
migrated to the cold tier are not in it, and until now nothing said so: a manifest reading
total_files: 1400was describing 1400 hot files and however many cold ones you had, silently.Every backup manifest now carries
cold_files_excluded, withcold_files_excluded_databasesbreaking it down per database — because a single total does not tell an operator which database
has data the backup is missing, and that is the whole point of the marker.
{ "backup_id": "backup-20261007-141500", "total_files": 1400, "cold_files_excluded": 412, "cold_files_excluded_databases": { "audit": 400, "metrics": 12 } }The figure also appears in the backup listing, per target in
GET /api/v1/backup/:id, and in awarning the restore logs.
This is the last stage of #1085. B1 made the destination configurable, B2a added the Azure key
prefix, B2b-1 allowed a remote destination, B2b-2 added per-database routing; this adds the marker
that says what a backup of a tiered deployment is not carrying. Backing up the cold tier itself is
#1086.
The decision this stage turns on
cold_files_excludeddeliberately does NOT join the replace-mode incompleteness refusal.replaceDatabasesrefuses a replace-mode restore from an incomplete backup, because replacedeletes the current files of every database it restores and then puts the backup back — so the
difference is lost. A cold-tier file is a different population: no backup carries it yet, and
replace cannot delete it either. Three independent mechanisms say so:
migrated file from that manifest as phase 2 of the migration (
releaseHotCopies, manifest first);store at all;
recordHotFileIfNotColdbinds itsON CONFLICTupdate totier = 'hot'.So refusing on this count would prevent nothing, and would refuse a replace restore for every
partly-cold database — the normal state of any deployment with tiering on. It would make tiered
storage and replace-mode restore mutually exclusive. There is a comment at that refusal saying
exactly this, and a regression test that fails if the count is ever added to it.
The comment is deliberately narrow about what it claims. It says the refusal would prevent nothing;
it does not say a partly-cold backup restores leaving tiering consistent, because that is false
in a neighbouring case (see Internal changes).
Shape
backup.ColdCounter— one method,CountColdFilesByDatabase, wired from the tiering manager incmd/arc/main.gobesideSetTierLookup, nil when tiering is off. The same pattern backup (stage A): scope a backup to one or more databases #1084 used, sothe backup package still does not import tiering.
SELECT database, COUNT(*) FROM tier_files WHERE tier = ? AND quarantined_at IS NULL GROUP BY database.The set that matters is "every database with cold rows", and that is not the backup inventory — a
fully cold database has no hot files, so it is absent from the listing and from
manifest.Databaseswhile still holding the rows this counts. Grouping covers that byconstruction, and cannot half-fail the way N point queries can.
run.legFor), since B2b-2 made backups fan out:the audit target's manifest reports the audit database's gap and nothing else. Summed into the
run-level view by
mergeRunManifests.backup was never going to carry, so failing the run would trade a healthy backup for no backup
over a diagnostic.
Test plan
internal/backup/cold_marker_1085b3_test.go,internal/tiering/cold_count_1085b3_test.go)removed · count written to every leg · filtered by the inventory · scope filter dropped · count
failure fatal · cold count added to the replace refusal · restore
Progressmirror dropped ·TargetSliceloses the field ·SummaryFromManifestloses the field ·quarantined_atfilterremoved · nil-receiver guard removed · breakdown map not merged
go test -racegreen oninternal/backup,internal/tiering,internal/config,internal/api,cmd/arcgofmt -l ./internal ./cmdempty;go vetclean{audit:1}only; thedefault leg carries
{cold:1}for a fully cold database absent from the inventory; themerged listing is the two legs summed; a merge restore leaves the cold objects at 2→2; tiering
off → the field is absent
left) backed up at
total_files=6, cold_files_excluded=1, and the replace-mode restorecompleted (
files_registered=4, no error) with cold objects 1→1 and cold tier rows 3→3 —the decision-1 proof, run on a cluster because
mode=replaceis refused outright off-cluster## New features+## Internal changes;arc.tomloperator notemain(it moved 4 commits mid-run); re-ran build, gofmt, vet and therace suite on the combined tree, plus a binary run confirming
cold_counter: trueand nopanics with
tiered_storage.enabled=trueandcold.enabled=falseReview
Two passes, 15 pre-fix proofs. Neither found a blocker. What they did find, and what changed:
Deep reviewer, one High — a licence claim that was simply wrong. I had written that the counter
is wired "whenever tiering is CONFIGURED, including without the tiering licence". It is not: the
tiering block in
main.gois gated on the licence, so an unlicensed node has a niltieringManager, never reachesSetColdCounter, and reports nothing. That matters because it isthe deployment where the marker would help most — migrations stopped, the files already out of hot
storage — and an operator reading my docs would have taken an absent field as proof nothing was ever
migrated. The behaviour stays (the alternative is running a tiering schema on an unlicensed node,
which is the boundary the licence pattern exists to hold); the prose is corrected in all four places
it appeared, and now says plainly that an absent field is not that evidence.
Four Mediums, all taken: the one-leg merge clone aliased the breakdown map — the manifest's first
map field, where in-place writes are natural — so it is now copied, with a test; the matrix row for
"a fully cold database routed to a non-default target" had no test, so it has two now; the
BackupSummarydoc claimed the breakdown reaches the per-target detail, which it does not; and theProgressfield now records that the count survives a run that later fails.SQLite checklist reviewer, no highs — all eight items pass. Its one Medium was a performance
claim of mine that measurement contradicted, and I reproduced it before acting: I had written that
the chosen plan "needs no new index", but a covering index on
(tier, database, quarantined_at)is~6x faster (21 ms → 3 ms) and is picked without
ANALYZE. The decision not to add it stands, oncost rather than optimality — ~7% of the database file and a sixth b-tree on a table the ingest
flush path writes to, to save ~18 ms once per backup run — and the comment now records the scale at
which that trade flips (~1M cold rows). It also caught that the quarantine tests got the production
time domain by coincidence; they now go through
QuarantineFileand get it by construction.Internal changes worth knowing
A pre-existing defect this work surfaced but does not fix. A restore of a file that was hot when
the backup was taken and migrated to cold afterwards writes a hot copy back and registers it, while
the tier report that should flip the row back to hot is silently refused —
recordHotFileIfNotCold'sON CONFLICTupdate is excluded for a cold row, soRowsAffected()is 0,wroteis false, and the switch intiering/replicated.gohits neither the error branch nor thecase wrotebranch: no counter, no log. The query path then unions the hot and cold globs and readsthe file twice. This concerns files the backup does carry, happens with this marker at 0, and
happens in merge mode too, so gating on this count would not address it. Filed separately.
A low count is reachable with no error. The marker is the reporting node's view of its own tier
metadata. On a gated cluster only the primary migrates, the others learn through
syncColdTierMetadata, andCreateBackupis not primary-writer gated — so a reader whose sync hasnot run yet answers a number that is present and too low. The field doc and the log line say so, and
the log names the node.