Repository navigation
feat(cli): add flats backup for a consistent online copy - #238
Merged
Merged
Conversation
The spec for issue #223. A copy of a running host is a directory with a manifest. Databases are snapshots, write-once blobs are copied in two passes, and the copy checks its own board and notebook coherence and fails to be run again when a change during the copy could make it inconsistent.
A snapshot of an outbox database and a snapshot of flats.db that start one after the other leave a gap, and a board or notebook that is written all the time writes into it on every attempt. The two now start together. The coherence check holds for either order of the two snapshots: the import cursor must lie between what the outbox has acknowledged and what it has created. A cursor above the outbox sequence and an outbox that is ahead of the cursor both fail it and start a new attempt. The check also runs in verify. The busy timeout and the size of the group are internal test options. internal/backup joins the services lane of the CI race tests, because the group runs in goroutines.
The app test backs up a host whose board and notebook are written all the time, restores the copy and starts a host from it. The CLI test checks that the exit statuses 2, 78 and 1 reach the process and that the help lists the command.
What to back up names the command, the verify mode, the restore and the limits of a copy of a running host. The CLI table lists the command.
A stopped host of an older release has an older flats.db. The command copied everything and then refused at the check of its own copy, with a message that named the wrong Flats version. It now refuses in the preflight, says that the host migrates flats.db when it starts, and names the copy by hand.
…before the backup The end-to-end test only checked that the restored host starts and that the next write gets a higher sequence. It now counts the board elements and the notebook pages of the restored host and compares them with the writes that the host had acknowledged before the backup began.
…d file once Verify opened flats.db through store.Inspect, which creates a -shm file when a -wal file lies beside the database. It now reports the stray file and opens nothing that can write. The schema check of the preflight moves after the --out refusals, so a refusal creates no -shm file beside the database of a stopped host. A flat's previews directory that is gone is skipped like what lies below it. A write-once file that the first pass skipped and the second pass copied is no longer listed as skipped. The busy-retry test takes a longer margin.
…nd 31 The lock of the busy-retry test now ends in the middle of the second try, with a margin on both sides that does not depend on timer precision. Rule 31 names the empty -shm that the schema refusal can leave, and rule 25 says that a copy with a stray -wal file fails with status 1.
…on Windows The busy-retry test holds a lock for seconds, so it no longer runs in parallel with the other tests of the package. The dangling-link case of the refusal test is a subtest, so a missing symlink privilege skips that case and not the whole test.
The report moves into its own method, and a test checks each line that rule 24 names for the three states of secret.key, with a skipped and an unstable path. The end-to-end test and the damaged-copy test check more of the output of rules 24 and 29. The first completion criterion of the spec is rewrapped.
… a notebook blob Findings of the testing review. A junction needs no privilege on Windows, so the Windows job now runs the case of rule 9 that the symbolic-link tests skip there. A database outside the group that the check finds is snapshotted again with flats.db, and a missing blob of a notebook fails the check, as one of a board does. The busy test keeps the lock for 4 s, which gives a margin of 2 s minus the start delay on the near side, and its comment says so.
CI found a data race with -race in the services lane. The first snapshot of a notebook database registered the collation inside a goroutine of the group, while other goroutines of the group opened connections. The collation table of the SQLite driver is a plain map, so the registration and the opens raced. Run now registers the collation once, in the calling goroutine, before any goroutine starts, and the snapshot no longer registers it. The tests run in parallel in one process, so a test file registers it before any test starts.
…egistration The test init registers the collation too, so a later change that moves the call in Run into a goroutine would pass the race lane. The comment tells a reader to check that by reading.
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 #223.
What this adds
flats backup --out PATHcopies a running or a stopped host into a new folder and writes a manifest with the Flats version and the sizeand SHA-256 of every file.
flats backup --verify PATHchecks such a copy without a host. The design is the spechost-backup.md, which is marked implemented.
The owner decided for the plan of #223 that a running host is never paused. The copy checks itself instead, and when a change during
the copy could make it inconsistent, the command fails with "run the backup again".
How it works:
VACUUM INTOsnapshot from a read-only connection (5 s busy timeout, up to 3 tries).file that keeps changing is kept and listed as unstable.
flats.dband the outbox databases of the boards, notebooks and documents that have an import cursor are snapshotted together. Thecopy is then checked in both directions: the cursor must lie between what the outbox has acknowledged and what it has created
(
acked <= import_seq <= last). A copy that fails the check is snapshotted again, up to 3 attempts.secret.keyfails the copy.PATH.partial-<random>and renames it toPATHwithout replacing anything. A failed or interrupted backupleaves nothing at
PATH.--verifyopens every database withmode=ro&immutable=1, compares each file with the manifest, checks the outbox databasesagainst
flats.db, checks that every blob a flat needs is in the copy, and checks thatsecret.keyopens every sealed value. Itwrites nothing.
--exclude-secret-keyleavessecret.keyout and keeps the recovery copy.Where the change is
internal/backup/: the new package (copy, survey, snapshot, outbox check, manifest, verify), with its tests.internal/app/backupcmd.go,internal/cli/{cli.go,ops.go},cmd/flats/main.go: the command.durable.Open(a shared-read open on Windows),store.OpenImmutableandstore.ErrVersionMismatch,notebook.RegisterCollation.scripts/ci-race.sh:internal/backupjoins theservicesrace lane, because the snapshots of one attempt run in goroutines..gitignore:/flats.exe.docs/configuration.md("What to back up" names the command, the verify mode and the restore),docs/surfaces.md, the spec index.go.modis unchanged.Known limits
The spec states them.
board and a notebook all the time. A server flat's database and files are fixtures.
read revision history. The coherence rule is tested in both directions at the unit level.
-racein a container. macOS and the CI jobs are covered by this PR's checks.Task report
This is the task report that the development protocol asks for. The reviewer's full reports are kept by the author on a local disk.
That copy is not accessible to other reviewers, so the lines below are exact excerpts of the verdicts and of each finding's headline.
Nothing here replaces a verdict.
origin/mainat69286e3.claude-sonnet-5-5.claude-fable-5-1; in the execution metadata of every run,claude-fable-5-1is the model that did the review work (the design runs also show a few small helper calls of the CLI toclaude-haiku-5-5). The reviewer authored no part of the change.which is one run beyond the plan of 2 rounds. Reviewer runs: 8 (design 2, implementation 3, testing 2, impact assessment 1). One implementation run stopped with an API usage limit (429) after 83 turns and gave no verdict, so it is an incomplete
review; I resumed the same session to finish round 1 and did not count it as a round.
main. Finding I2 came from that error of mine, and Irebutted it with the correct diff. Round 2 used the right base.
Verdicts and findings of each review round
DESIGN: REJECTPATH.data/files/.config.json.DESIGN: ACCEPTdocs/spec/README.md, the index row. The row is stale.IMPLEMENTATION: REJECTflats.exeis committed.--verifywrites a file when the copy holds a-walbesideflats.db(rule 25).filesandskipped.ReadDirandInfofails the backup.-shminto the data directory.IMPLEMENTATION: ACCEPTRound 2 closed I1 to I8 and added:
TestASnapshotThatIsBusy/"a later try succeeds"now has its margin at a boundary.TESTING: ACCEPTTestASnapshotThatIsBusyoverstates its margin, and the test fails on a slow start.TESTING: ACCEPTRound 2 closed T1 to T4.
IMPACT: ACCEPTDATA RACE" without the CI result.init()hides a regression of the fix from the race lane.Dispositions: every finding was accepted and corrected, except I2, which I rebutted with evidence (the error above). Optional suggestions were not all adopted. Not adopted: matching the busy error by its code, a separate count of coherence retries, a test of
--jsonoutput of a failure, a direct test of rule 17, and running the checks of the other databases whenflats.dbis missing from a copy. The corrections thatcame after an accepted round are covered by a later review: the corrections of the design round 2 by the implementation review, the
corrections of implementation round 2 by the testing review, and the corrections of testing round 1 by testing round 2. The implementation
reviewer accepted carrying the implementation approval to the final commit, whose difference from the snapshot that it accepted is the
extraction of the report printing in
internal/app/backupcmd.go, two spec sentences and test files. After the CI data race below, the same reviewer assessed the fix and accepted the carry again. R1 is addressed by this description and R2 by a comment at the registration inRun.A data race that CI found
The job
race (services)failed on5e095e6with a data race, and the other 24 checks passed. The first snapshot of a notebook database registered the SQLite collation inside a goroutine of the group, while other goroutines of the group opened connections. The driver writes its table of collations under a mutex and reads it, inDriver.Open, without one, so the two raced.My own
-raceruns before the PR (the Linux container, several times) did not show it, so my earlier statements of "no data race" held for those runs only. I could not reproduce the race locally with the old code in 60 runs, so the CI report is the direct evidence of it. The fix is in4fe2a25:Runregisters the collation once, before any goroutine starts, and the snapshot no longer registers it. A test file registers it ininitas well, because the tests run in parallel in one process; without that, a run of the fix without theinitraced once in 20 runs, in a test fixture. The consequence is that the race lane cannot catch a later move of this call into a goroutine, and the comment inRunsays so.The same hazard exists on
mainoutside this change: the host registers the collation lazily at the first notebook open while other goroutines open databases. I did not touch it.Verification
Results are on the commit that the row names. Nothing skipped or unavailable is counted as a pass.
go build ./...,go vet ./...,make docs-checkequivalentgo testofinternal/backup,cli,store,durable,app,docs(on5e095e6;backup,cliandapprun again on the final commit and pass)golang:1.27.1),go test -raceofinternal/backup, and the backup and secret-key tests ofappandcli4fe2a25, the fix of the CI race (the last commit only adds a comment): 113 pass, 0 fail, 0 skip, no data race. The same kind of run before the fix did not show the race that CI foundOutside this change:
internal/devreviewfails on Windows without this branch too (symlink privilege, case folding). Whole-package-raceruns ofcliandappin the container fail 5 tests that look up*.localhostnames, which the container does not resolve.Not run locally:
-raceon Windows and macOS. CI on5e095e6: 24 of 25 checks passed (thewindowsand macOS jobs among them) andrace (services)failed as described above. CI on9aacb9d, the fix: all 25 checks passed, includingrace (services), andcommit-imagesis skipped by design (it runs on pushes tomain). Code scanning shows no open alert for this PR.For the maintainer
The history is 17 commits. The repository squashes each PR into one commit, so the PR title is the message. The commits are small and
named by what they change, if you read them one by one.