carlos / platform Public

activator: an own-replica restore of a seed writes its marker (final review carry item)

A placed or committed seed's wake restores its own lineage (or keeps the
warm copy) through restoreOwn, and left the <db>.seed marker as it was:
absent after a move or a lost directory, or naming another seed. The
rollback's missing-row purge and an aborted --from seed's local purge
decide by that marker, and a database without one is left in place.

restoreOwn now writes the marker naming the row's seed, through
instancefs, after the restore; a write that fails fails the wake, as
placeSeed's marker write does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev aa03daa697cb4764a00a63da31b5bc816dd53806 parent af78194
2 files changed, +60 −0
  • internal/activator/seedplace.go +9 −0
  • internal/activator/seedplace_linux_test.go +51 −0
diff --git a/internal/activator/seedplace.go b/internal/activator/seedplace.go
index 0fdc1343b..15bd4cdbb 100644
--- a/internal/activator/seedplace.go
+++ b/internal/activator/seedplace.go
@@ -112,6 +112,11 @@ func (b *ExecBackend) seededRestore(ctx context.Context, rt registry.Route, src
// included, and never reports fresh: a host that has (or was listed as
// having) a replica of its own and finds none must not boot empty. The
// next wake asks again.
+ //
+ // The database it leaves is this seed's lineage, so its <db>.seed
+ // marker is written to name the seed (final review carry item): the
+ // rollback's missing-row purge and an aborted seed's local purge decide
+ // by the marker, and a database with none is left in place.
restoreOwn := func() (bool, error) {
fresh, err := b.restorePlain(ctx, rt)
if err != nil {
@@ -120,6 +125,10 @@ func (b *ExecBackend) seededRestore(ctx context.Context, rt registry.Route, src
if fresh {
return fail(fmt.Errorf("seed fallback for %s: found no replica of its own to restore", host))
}
+ db := b.pathsFor(rt).dbPath
+ if err := instancefs.WriteAtomic(filepath.Dir(db), filepath.Base(db)+SeedMarkerSuffix, []byte(src.ID+"\n"), 0o600); err != nil {
+ return fail(seedPlaceFailed(host, err))
+ }
return false, nil
}
// A placed seed stays on the row for life, and placed is set only once
diff --git a/internal/activator/seedplace_linux_test.go b/internal/activator/seedplace_linux_test.go
index afc2fbb0f..2a26e5ea7 100644
--- a/internal/activator/seedplace_linux_test.go
+++ b/internal/activator/seedplace_linux_test.go
@@ -755,3 +755,54 @@ func TestSeedPlacedIsFinishedOnTheNextWake(t *testing.T) {
t.Fatal(err)
}
}
+
+// Final review carry item: a placed or committed seed's own-replica
+// restore, or its warm copy, leaves a <db>.seed marker naming the seed, so
+// the rollback's missing-row purge and an aborted seed's local purge can
+// tell the database is the seed's. A marker naming another seed is
+// rewritten.
+func TestSeededRestoreOfItsOwnReplicaWritesTheMarker(t *testing.T) {
+ for _, c := range []struct {
+ name string
+ route func(t *testing.T, b *ExecBackend, rt registry.Route) registry.Route
+ warm bool
+ }{
+ {"committed import, restored", func(t *testing.T, b *ExecBackend, rt registry.Route) registry.Route {
+ rt, _ = committedImport(t, b, rt)
+ return rt
+ }, false},
+ {"placed seed, warm copy", func(t *testing.T, b *ExecBackend, rt registry.Route) registry.Route {
+ rt, _, _ = seededRoute(t, b, rt, seeds.StatePlaced)
+ return rt
+ }, true},
+ } {
+ t.Run(c.name, func(t *testing.T) {
+ countOwnReplicaLists(t, false)
+ b, rt := nestedBackend(t, &fakeControl{}, &fakeUnit{})
+ rt = c.route(t, b, rt)
+ col, err := seeds.ParseRowSeed(rt.SeedSource)
+ if err != nil {
+ t.Fatal(err)
+ }
+ oldRestore := restoreReplica
+ restoreReplica = func(_ context.Context, dbPath, _, _, _ string) (bool, error) {
+ return true, writeMinimalSQLite(dbPath)
+ }
+ defer func() { restoreReplica = oldRestore }()
+ os.MkdirAll(filepath.Dir(rt.DBPath), 0o700)
+ if c.warm {
+ if err := writeMinimalSQLite(rt.DBPath); err != nil {
+ t.Fatal(err)
+ }
+ }
+ other, _ := seeds.NewID()
+ os.WriteFile(rt.DBPath+SeedMarkerSuffix, []byte(other+"\n"), 0o600)
+ if fresh, err := b.Restore(context.Background(), rt); err != nil || fresh {
+ t.Fatalf("fresh %v err %v", fresh, err)
+ }
+ if got, err := ReadSeedMarker(rt.DBPath); err != nil || got != col.ID {
+ t.Fatalf("marker after the restore = %q, %v; want %s", got, err, col.ID)
+ }
+ })
+ }
+}