Skip to content

backup: add verify command - #1337

Merged
bigbes merged 5 commits into
masterfrom
bigbes/tntp-8670-backup-verify
Jul 31, 2026
Merged

backup: add verify command#1337
bigbes merged 5 commits into
masterfrom
bigbes/tntp-8670-backup-verify

Conversation

@bigbes

@bigbes bigbes commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Add tt backup verify, a read-only health check of a backup storage. It loads the manifest chain, recomputes sha256 of every referenced archive straight from the GET stream (archives are never spooled to disk), and lists archives no manifest refers to.

Reported problem classes: unreadable and structurally invalid manifests, missing and unreadable archives, checksum mismatches, breaks in the previous_backup_id chain (orphans and forks), increments whose vclock_begin does not continue their ancestor, and dangling archives. Chain problems come from cli/backup/chain, inherited ones included.

chain.Load is split into Load and LoadPartial: a diagnostic caller must report a single unreadable manifest and keep checking the rest of the storage, the way tt cluster topology degrades per instance, while a caller that recovers from the chain still fails loud on a missing link.

Nothing is ever written or deleted: removing backups stays with tt backup gc. The command exits with 2 when problems are found, so a cron job tells an unhealthy storage from a run that could not check anything at all (exit code 1); 2 matches the fail-loud code of tt backup start.

Two prerequisite fixes come first in the branch, each independently reviewable and each a defect in code already on master rather than anything verify introduces. storage/fs swept stale .tt-backup-* files when the storage was opened, which made verify's own promise ("Nothing is ever deleted or repaired") untrue before it had read a byte; the sweep moved to the first Put, so writers still clean up after writers and readers delete nothing. And a manifest with an empty shards map validated as healthy while referring to no archive at all, so verify called it fine and its archive unreferenced - which reads as "delete this".

Closes TNTP-8670

@bigbes
bigbes force-pushed the bigbes/tntp-8670-backup-verify branch 3 times, most recently from 3560f19 to e5385aa Compare July 30, 2026 06:13
Base automatically changed from g.rybakov/tntp-8608-start-rework-tests to master July 30, 2026 08:56
@bigbes
bigbes force-pushed the bigbes/tntp-8670-backup-verify branch 2 times, most recently from c145fc0 to b6d7a38 Compare July 30, 2026 12:49
@bigbes
bigbes force-pushed the bigbes/tntp-8670-backup-verify branch 3 times, most recently from 6ff9419 to 7efa1bb Compare July 30, 2026 19:56
bigbes added 2 commits July 31, 2026 09:15
Opening a filesystem storage walked its whole root and removed every
leftover .tt-backup-* file older than a day. Only Put creates those
files, but every caller paid for the sweep - including the ones that
promise to change nothing at all.

`tt backup verify` is the first of those, and the promise is explicit:
"Nothing is ever deleted or repaired". It was untrue for a file
storage before verify had read a single byte, and what it removed was
the residue of an interrupted upload - the very thing an operator
running verify may be trying to look at.

The sweep now runs on the first Put instead, so writers still clean up
after writers and a reader deletes nothing. The existing read-only
assertions could not have caught this: they drive an in-memory store
and never construct an fs.Storage.

Part of TNTP-8608
Validation required the shards map to be present but not to hold
anything, so a manifest with an empty one passed as healthy while
referring to no archive at all.

Nothing downstream expects that. `tt backup verify` reported the
manifest as fine and its archive as unreferenced, which reads as
"delete this"; `tt backup gc` would then collect the live archive of a
backup that still exists once it aged past --orphan-age.

A backup of no shards is not a backup, so it is invalid. Empty and nil
stay separate errors: one is a hand-edited manifest, the other a
missing field, and an operator reading the report wants to know which.

Part of TNTP-8608
@bigbes
bigbes force-pushed the bigbes/tntp-8670-backup-verify branch from 7efa1bb to cb7b1a4 Compare July 31, 2026 06:19
@bigbes
bigbes force-pushed the bigbes/tntp-8670-backup-verify branch from 4ccf696 to 7857221 Compare July 31, 2026 08:51
bigbes added 3 commits July 31, 2026 13:24
Add `tt backup verify`, a read-only health check of a backup storage. It
loads the manifest chain, recomputes sha256 of every referenced archive
straight from the GET stream (archives are never spooled to disk), and
lists archives no manifest refers to.

Reported problem classes: unreadable and structurally invalid manifests,
missing and unreadable archives, checksum mismatches, breaks in the
previous_backup_id chain (orphans and forks), increments whose
vclock_begin does not continue their ancestor, and dangling archives.
Chain problems come from cli/backup/chain, inherited ones included.

Split chain.Load into Load and LoadPartial: a diagnostic caller must
report a single unreadable manifest and keep checking the rest of the
storage, the way `tt cluster topology` degrades per instance, while a
caller that recovers from the chain still fails loud on a missing link.
LoadPartial degrades on two more defects that used to abort the whole
run: a manifest with no backup_id and two objects claiming the same one.
Neither can become a chain entry, so both are reported against their
storage key - the only name they have left, now carried on the issue
itself rather than buried in an error string.

Nothing is ever written or deleted: removing backups stays with `tt
backup gc`.

The command exits with 2 when problems are found, so a cron job tells an
unhealthy storage from a run that could not check anything at all (exit
code 1); 2 matches the fail-loud code of `tt backup start`. A cancelled
context or an expired --timeout therefore aborts rather than reporting
the archives it never read as unreadable, which would let a deadline
masquerade as corruption; and --timeout 0 means no limit rather than
"already expired".

An archive whose manifest could not be read is not called dangling: the
manifest does claim it, it just cannot be parsed to say so, and gc acts
on exactly this issue.

Chain problems are attached in backup id order, so a manifest inheriting
from several broken ancestors reports them the same way every run and a
cron consumer diffing --format json sees only real changes. A
previous_backup_id cycle is reported instead of dropping the manifests
involved out of the chain entirely.

Closes TNTP-8670
backup_helpers built the local artifact directory from
realpath("/tmp"), while cli/backup builds it from Go's os.TempDir().
The two agree on Linux, where TMPDIR is normally unset, but on macOS
os.TempDir() returns the per-user $TMPDIR under /var/folders, so the
helper pointed at a directory tt never wrote to.

Two tests failed because of it and neither failure had anything to do
with the code under test: test_backup_start compared the archive path
printed by 'tt backup start' against the wrong parent directory, and
test_finalize failed a step earlier still, creating a neighbouring
artifact in a directory that did not exist.

tempfile.gettempdir() honours TMPDIR the same way os.TempDir() does, so
both sides land in the same directory on either platform.

Part of TNTP-8608
`tt backup upload` writes every archive to data/ and the manifest that
references them last, so that a half-uploaded backup is not a backup.
Between the first archive and the manifest - the whole upload, hours on
a large cluster - those archives are unreferenced by construction, and
verify reported each of them as a dangling archive and exited 2. A cron
verify overlapping the cron backup therefore raised the
storage-unhealthy alert on a cluster doing exactly what it should.

Split the two apart the way gc already does: an unreferenced archive of
a backup newer than every stored manifest, or any unreferenced archive
when the storage holds no manifest at all, is an upload in progress. It
is still reported, because an operator wants to see everything that is
in the storage, but it is marked informational and does not make the
storage unhealthy. An archive whose backup id the pipeline has already
moved past is abandoned as before, and still fails the check.

Findings carry an `informational` flag for that, and Report.Problems
counts the rest, so a JSON consumer can filter and the table output does
not dress an upload up as a defect.

Part of TNTP-8670
@bigbes
bigbes force-pushed the bigbes/tntp-8670-backup-verify branch from 7857221 to a5499b2 Compare July 31, 2026 10:25
@bigbes
bigbes merged commit 23078d7 into master Jul 31, 2026
25 checks passed
@bigbes
bigbes deleted the bigbes/tntp-8670-backup-verify branch July 31, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants