carlos / platform Public

host: the cleaning parks .corrupt and the litestream shadow only when the install recorded them absent (final re-review residual)

The database install journals pre_install inside its guard, right before
the rename: which of the two siblings it does not refuse (the quarantine
and the litestream shadow) were absent, Lstat'ed. A replay before the
rename looks again. The target's cleaning parks either only when the
database was installed and pre_install records it absent; present then,
or no record, is left in place and logged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3f7db3a863919fca2f0c1bc460b11010733a98a9 parent 2a19f4f
4 files changed, +188 −7
  • internal/host/movesland.go +1 −0
  • internal/host/moveslandinstall.go +60 −0
  • internal/host/movesrollback.go +26 −7
  • internal/host/movesrollback_finalfix_test.go +101 −0
diff --git a/internal/host/movesland.go b/internal/host/movesland.go
index 7087ab6e5..dd1a46700 100644
--- a/internal/host/movesland.go
+++ b/internal/host/movesland.go
@@ -112,6 +112,7 @@ const (
landStepVerify = "verify" // the restore-verification entry is gone
landStepFilesManifest = "files_manifest" // the prepared files manifest (C8; skipped until it exists)
landStepFilesGen = "files_gen" // .files-gen written
+ landStepPreInstall = "pre_install" // the database's siblings absent before its install (JSON preInstall)
)
// landingFilesManifestSkipped is what the files_manifest step records while
diff --git a/internal/host/moveslandinstall.go b/internal/host/moveslandinstall.go
index 1d8825d09..6f5c56245 100644
--- a/internal/host/moveslandinstall.go
+++ b/internal/host/moveslandinstall.go
@@ -594,6 +594,9 @@ func (ir *installRun) restore(ctx context.Context, obs hostclaim.Observed) (sha
return err
}
}
+ if err := ir.notePreInstall(); err != nil {
+ return err
+ }
if err := os.Rename(built, dbPath); err != nil {
return fmt.Errorf("install the database: %w", err)
}
@@ -616,6 +619,63 @@ func (ir *installRun) restore(ctx context.Context, obs hostclaim.Observed) (sha
return want, restoreInstalled, nil
}
+// preInstall is the landStepPreInstall record: which of the database's
+// unchecked siblings (installSiblingPaths) were absent, relative to the
+// data directory, when the install was about to put the database in place.
+// Only a sibling recorded absent can later count as this op's (the target's
+// cleaning, rollbackOwnership; the final re-review's residual): one already
+// there was not made by anything of this install's.
+type preInstall struct {
+ Absent []string `json:"absent"`
+}
+
+// installSiblingPaths are the database's siblings the install does not
+// refuse beside an absent database (it refuses a WAL and shared memory):
+// the quarantine and the litestream shadow, relative to the data directory.
+func installSiblingPaths(host string) []string {
+ db := host + ".db"
+ return []string{db + ".corrupt", ".activator-" + db + "-litestream"}
+}
+
+// notePreInstall journals preInstall inside the database install's guard,
+// right before the rename: each sibling Lstat'ed, never followed. A replay
+// before the rename (a crash after the step) looks again and journals what
+// it sees, so the record is always the look that immediately preceded the
+// rename, under the same guard. A replay that finds the database already
+// installed never reaches here, so a step lost that way is missing, and its
+// siblings are never counted as the op's.
+func (ir *installRun) notePreInstall() error {
+ j, err := ir.journal()
+ if err != nil {
+ return err
+ }
+ raw, replay := j.Steps[landStepPreInstall]
+ rec := preInstall{Absent: []string{}}
+ for _, p := range installSiblingPaths(ir.rec.Host) {
+ _, err := os.Lstat(filepath.Join(ir.cfg().DataDir, p))
+ switch {
+ case errors.Is(err, os.ErrNotExist):
+ rec.Absent = append(rec.Absent, p)
+ case err != nil:
+ return err
+ default:
+ log.Printf("moves: %s: %s is already there before the database is installed; it is not the %s's", ir.rec.Host, p, ir.src.Name)
+ }
+ }
+ body, err := json.Marshal(rec)
+ if err != nil {
+ return err
+ }
+ if replay && string(body) == raw {
+ return nil
+ }
+ j.Mark(landStepPreInstall, string(body))
+ if err := j.Write(ir.cfg().DataDir); err != nil {
+ return err
+ }
+ return ir.fault("after-" + landStepPreInstall)
+}
+
// refuseInstall writes the install's refusal for a build that does not
// verify. A refusal that landed is restoreMarked; one refused (the record
// moved on) is restoreWaiting, and nothing is installed either way.
diff --git a/internal/host/movesrollback.go b/internal/host/movesrollback.go
index 3cef68bcc..c9d5d85d0 100644
--- a/internal/host/movesrollback.go
+++ b/internal/host/movesrollback.go
@@ -1344,11 +1344,16 @@ func (r *rollbackRun) parkLeftovers(ctx context.Context, j *moves.Journal, mark
// - the files directory and <host>.env: the landing's placement receipt
// (files_placed), or before it the staged extraction's identities
// (staged), which a rename keeps: the live path must have that identity;
-// - the database, its marker, WAL, shared memory, quarantine and
-// litestream shadow: the landing installed its database, as the record
-// says (landing.installed_db_sha256, stamped by the CAS that ends
-// configured); the install refused a WAL or shared memory beside an
-// absent database, so what sits beside it since is the op's instance's;
+// - the database, its marker, WAL and shared memory: the landing installed
+// its database, as the record says (landing.installed_db_sha256, stamped
+// by the CAS that ends configured); the install refused a WAL or shared
+// memory beside an absent database, so what sits beside it since is the
+// op's instance's;
+// - the quarantine and the litestream shadow, which the install does not
+// refuse: the database installed as above, and the install's journal
+// records the path absent just before it put the database in place
+// (pre_install, notePreInstall). Present then, or no record, is never
+// the op's (the final re-review's residual);
// - the rendered config, <host>.config.env: the held render's ack on this
// box names this op, and the file's sha256 is the one the ack recorded;
// - the sidecar's env: never, since a moved host has no sidecar.
@@ -1356,7 +1361,8 @@ type rollbackOwnership struct {
host, dataDir string
placed stagedFiles // the landing's receipt, or its staged identities
installed bool
- configSHA256 string // the held render's ack, "sha256:<hex>", or ""
+ absentBefore map[string]bool // the siblings pre_install recorded absent
+ configSHA256 string // the held render's ack, "sha256:<hex>", or ""
}
// ownership reads the evidence: the landing's journal, the record and the
@@ -1379,6 +1385,17 @@ func (r *rollbackRun) ownership(ctx context.Context, j *moves.Journal) (rollback
if l := r.rec.Move.Landing; l != nil && l.InstalledDBSHA256 != "" {
ev.installed = true
}
+ if raw, ok := j.Steps[landStepPreInstall]; ok {
+ var pre preInstall
+ if err := json.Unmarshal([]byte(raw), &pre); err != nil {
+ log.Printf("moves: %s: the journal's %s step is unreadable (%v); the database's siblings are not counted as %s's", host, landStepPreInstall, err, id)
+ } else {
+ ev.absentBefore = map[string]bool{}
+ for _, p := range pre.Absent {
+ ev.absentBefore[p] = true
+ }
+ }
+ }
body, _, err := r.s.Get(ctx, appconfig.ConfigAckKey(cfg.Box, host))
switch {
case errors.Is(err, os.ErrNotExist):
@@ -1416,8 +1433,10 @@ func (ev rollbackOwnership) owns(path, identity string) (bool, error) {
return false, err
}
return got == ev.configSHA256, nil
- case db, db + ".gen", db + "-wal", db + "-shm", db + ".corrupt", ".activator-" + db + "-litestream":
+ case db, db + ".gen", db + "-wal", db + "-shm":
return ev.installed, nil
+ case db + ".corrupt", ".activator-" + db + "-litestream":
+ return ev.installed && ev.absentBefore[path], nil
}
return false, nil
}
diff --git a/internal/host/movesrollback_finalfix_test.go b/internal/host/movesrollback_finalfix_test.go
index 24ca5662e..d227e650d 100644
--- a/internal/host/movesrollback_finalfix_test.go
+++ b/internal/host/movesrollback_finalfix_test.go
@@ -4,6 +4,7 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
+ "encoding/json"
"errors"
"fmt"
"io/fs"
@@ -433,3 +434,103 @@ func TestLandingWritesPassThePolicy(t *testing.T) {
}
p.want(t, "local/held-timers", "configured/held-config", "configured/held-release", "configured/restore", "configured/install")
}
+
+// plantSiblings writes a quarantine file and a litestream shadow directory
+// beside the database at the flat paths.
+func plantSiblings(t *testing.T, dir, body string) []string {
+ t.Helper()
+ corrupt := p1Host + ".db.corrupt"
+ shadow := ".activator-" + p1Host + ".db-litestream"
+ if err := os.MkdirAll(filepath.Join(dir, shadow), 0o750); err != nil {
+ t.Fatal(err)
+ }
+ for name, b := range map[string]string{corrupt: body, filepath.Join(shadow, "generation"): body} {
+ if err := os.WriteFile(filepath.Join(dir, name), []byte(b), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return []string{corrupt, shadow}
+}
+
+// Finding 1, the re-review's residual (task-final-rereview-1.md): the
+// quarantine file and the litestream shadow are the op's only when the
+// install journalled them absent before it installed the database. One
+// already there when the landing installed survives the cleaning byte for
+// byte; ones made after the install are parked; with the journal entry
+// missing (an install that recorded none), they are left.
+func TestRollbackCleaningOwnsDatabaseSiblingsOnlyWhenRecordedAbsent(t *testing.T) {
+ t.Run("present before the install", func(t *testing.T) {
+ h := &rollbackHarness{landingHarness: newLandingHarness(t)}
+ h.flip(t)
+ foreign := plantSiblings(t, h.tdir, "not the landing's")
+ before := treeSnapshot(t, h.tdir, foreign...)
+ h.land(t)
+ h.installed(t)
+ h.step(t, provision.LandRestored, provision.LandProbing)
+ h.tpos = h.n
+ h.rollBack(t)
+ h.cleanTarget(t)
+ if after := treeSnapshot(t, h.tdir, foreign...); !sameSnapshot(before, after) {
+ t.Fatalf("the siblings the landing found changed:\nbefore %v\nafter %v", before, after)
+ }
+ })
+ t.Run("made after the install", func(t *testing.T) {
+ h := newRollbackHarness(t)
+ made := plantSiblings(t, h.tdir, "the landing's instance")
+ h.rollBack(t)
+ h.cleanTarget(t)
+ for _, p := range made {
+ if _, err := os.Lstat(filepath.Join(h.tdir, p)); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("%s is still live: %v", p, err)
+ }
+ }
+ parked := parkedUnder(t, h.tdir)
+ if !parkedHas(parked, "/artifacts/"+made[0], "the landing's instance") || !parkedHas(parked, "/artifacts/"+made[1]+"/generation", "the landing's instance") {
+ t.Fatalf("the op's siblings were not parked: %v", parked)
+ }
+ })
+ t.Run("no record of their absence", func(t *testing.T) {
+ h := newRollbackHarness(t)
+ j := h.landJournal(t)
+ if _, ok := j.Steps[landStepPreInstall]; !ok {
+ t.Fatalf("the install journalled no %s step: %v", landStepPreInstall, j.Steps)
+ }
+ delete(j.Steps, landStepPreInstall)
+ if err := j.Write(h.tdir); err != nil {
+ t.Fatal(err)
+ }
+ made := plantSiblings(t, h.tdir, "unrecorded")
+ before := treeSnapshot(t, h.tdir, made...)
+ h.rollBack(t)
+ h.cleanTarget(t)
+ if after := treeSnapshot(t, h.tdir, made...); !sameSnapshot(before, after) {
+ t.Fatalf("siblings with no recorded absence changed:\nbefore %v\nafter %v", before, after)
+ }
+ })
+}
+
+// A crash after pre_install is journalled and before the database's rename:
+// the replay looks again right before its rename, so a sibling that
+// appeared in between is recorded present and survives the cleaning.
+func TestPreInstallReplayLooksAgain(t *testing.T) {
+ h := &rollbackHarness{landingHarness: newLandingHarness(t)}
+ h.flip(t)
+ h.landAt = "after-" + landStepPreInstall
+ h.crashLand(t)
+ foreign := plantSiblings(t, h.tdir, "appeared after the first look")
+ before := treeSnapshot(t, h.tdir, foreign...)
+ h.freshTarget()
+ h.land(t)
+ h.installed(t)
+ var pre preInstall
+ if err := json.Unmarshal([]byte(h.landJournal(t).Steps[landStepPreInstall]), &pre); err != nil || len(pre.Absent) != 0 {
+ t.Fatalf("pre_install after the replay: %+v %v", pre, err)
+ }
+ h.step(t, provision.LandRestored, provision.LandProbing)
+ h.tpos = h.n
+ h.rollBack(t)
+ h.cleanTarget(t)
+ if after := treeSnapshot(t, h.tdir, foreign...); !sameSnapshot(before, after) {
+ t.Fatalf("siblings that appeared before the replay changed:\nbefore %v\nafter %v", before, after)
+ }
+}