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>
2 files changed,
+60
−0
internal/activator/seedplace.go+9 −0internal/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) |
| + } |
| + }) |
| + } |
| +} |