| index f93f87dd9..a762be914 100644 |
| --- a/internal/host/snapshot_loop_test.go |
| +++ b/internal/host/snapshot_loop_test.go |
| @@ -35,7 +35,7 @@ func TestSnapshotPassWritesCurrentSidecarAndGeneration(t *testing.T) { |
| t.Fatal(err) |
| } |
| |
| - raw, _, err := s.Get(ctx, snapshotKey) |
| + raw, _, err := s.Get(ctx, snapshotDBKey("box1")) |
| if err != nil { |
| t.Fatalf("current snapshot missing: %v", err) |
| } |
| @@ -43,7 +43,7 @@ func TestSnapshotPassWritesCurrentSidecarAndGeneration(t *testing.T) { |
| t.Fatal("current snapshot is empty") |
| } |
| |
| - metaRaw, _, err := s.Get(ctx, snapshotMetaKey) |
| + metaRaw, _, err := s.Get(ctx, snapshotMetaKey("box1")) |
| if err != nil { |
| t.Fatalf("sidecar missing: %v", err) |
| } |
| @@ -58,11 +58,11 @@ func TestSnapshotPassWritesCurrentSidecarAndGeneration(t *testing.T) { |
| t.Fatalf("lastFP %q not updated to %q", lastFP, meta.Fingerprint) |
| } |
| |
| - gens, err := s.List(ctx, snapshotGenPrefix) |
| + gens, err := s.List(ctx, snapshotGenPrefix("box1")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if len(gens) != 1 || gens[0] != snapshotGenPrefix+"20260812T030000Z.db" { |
| + if len(gens) != 1 || gens[0] != snapshotGenPrefix("box1")+"20260812T030000Z.db" { |
| t.Fatalf("generations = %v", gens) |
| } |
| } |
| @@ -80,7 +80,7 @@ func TestSnapshotPassSkipsWhenFingerprintUnchanged(t *testing.T) { |
| if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(5*time.Minute), &lastFP); err != nil { |
| t.Fatal(err) |
| } |
| - gens, err := s.List(ctx, snapshotGenPrefix) |
| + gens, err := s.List(ctx, snapshotGenPrefix("box1")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -95,7 +95,7 @@ func TestSnapshotPassSkipsWhenFingerprintUnchanged(t *testing.T) { |
| if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(10*time.Minute), &lastFP); err != nil { |
| t.Fatal(err) |
| } |
| - gens, err = s.List(ctx, snapshotGenPrefix) |
| + gens, err = s.List(ctx, snapshotGenPrefix("box1")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -114,7 +114,7 @@ func TestSnapshotPassSkipsWhenFingerprintUnchanged(t *testing.T) { |
| if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(15*time.Minute), &lastFP); err != nil { |
| t.Fatal(err) |
| } |
| - gens, err = s.List(ctx, snapshotGenPrefix) |
| + gens, err = s.List(ctx, snapshotGenPrefix("box1")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -129,7 +129,7 @@ func TestSnapshotPassSkipsWhenFingerprintUnchanged(t *testing.T) { |
| if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(20*time.Minute), &lastFP); err != nil { |
| t.Fatal(err) |
| } |
| - gens, err = s.List(ctx, snapshotGenPrefix) |
| + gens, err = s.List(ctx, snapshotGenPrefix("box1")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -154,7 +154,7 @@ func TestSnapshotSidecarCarriesSnapshotFingerprint(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - metaRaw, _, err := s.Get(ctx, snapshotMetaKey) |
| + metaRaw, _, err := s.Get(ctx, snapshotMetaKey("box1")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -167,17 +167,19 @@ func TestSnapshotSidecarCarriesSnapshotFingerprint(t *testing.T) { |
| } |
| } |
| |
| -// readSnapshotMeta is what makes the foreign-box overwrite loud (the |
| -// cold-standby clobber the runbook's step 0 exists to prevent). It must |
| -// report the prior writer's box, and must degrade to nil rather than |
| -// erroring when there is no sidecar at all. |
| +// readSnapshotMeta is box-scoped: it must report the prior writer at THIS |
| +// box's own key, degrade to nil when there is no sidecar there yet, and |
| +// never see another box's sidecar at all — per-box keys is exactly what |
| +// makes a foreign-differently-named box's write harmless (it lands under a |
| +// different key entirely, unlike the pre-multi-region shared key this |
| +// guard was written against). |
| func TestReadSnapshotMetaReportsPriorWriter(t *testing.T) { |
| s := release.NewDirStore(t.TempDir()) |
| ctx := context.Background() |
| reg := snapTestRegistry(t) |
| at := time.Date(2026, 8, 12, 3, 0, 0, 0, time.UTC) |
| |
| - if got := readSnapshotMeta(ctx, s); got != nil { |
| + if got := readSnapshotMeta(ctx, s, "dead-box"); got != nil { |
| t.Fatalf("readSnapshotMeta with no sidecar = %+v, want nil", got) |
| } |
| |
| @@ -185,23 +187,31 @@ func TestReadSnapshotMetaReportsPriorWriter(t *testing.T) { |
| if err := registrySnapshotPass(ctx, s, reg, "dead-box", at, &lastFP); err != nil { |
| t.Fatal(err) |
| } |
| - got := readSnapshotMeta(ctx, s) |
| + got := readSnapshotMeta(ctx, s, "dead-box") |
| if got == nil || got.Box != "dead-box" { |
| t.Fatalf("readSnapshotMeta = %+v, want Box \"dead-box\"", got) |
| } |
| |
| - // The standby case: a different box overwrites. The pass does NOT refuse |
| - // it (a legitimate promotion is indistinguishable) — it logs — so the |
| - // write must still land, now naming the standby. |
| + // A different box name never sees dead-box's sidecar — per-box keys |
| + // isolate them, unlike the pre-multi-region shared key. |
| + if got := readSnapshotMeta(ctx, s, "standby-box"); got != nil { |
| + t.Fatalf("readSnapshotMeta(\"standby-box\") = %+v, want nil (no cross-box read)", got) |
| + } |
| + |
| + // The runbook's restore path: a rebuilt standby deliberately reuses the |
| + // dead box's name, so its pass writes to the SAME key. The write must |
| + // still land (the pass never refuses it — a legitimate promotion looks |
| + // identical from here), and readSnapshotMeta for that name reports the |
| + // newest sidecar. |
| if err := reg.Upsert(registry.Route{Host: "c.example.com", Kind: "service", Addr: "10.0.0.3:3000", Status: "ready"}); err != nil { |
| t.Fatal(err) |
| } |
| - if err := registrySnapshotPass(ctx, s, reg, "standby-box", at.Add(5*time.Minute), &lastFP); err != nil { |
| + if err := registrySnapshotPass(ctx, s, reg, "dead-box", at.Add(5*time.Minute), &lastFP); err != nil { |
| t.Fatal(err) |
| } |
| - got = readSnapshotMeta(ctx, s) |
| - if got == nil || got.Box != "standby-box" { |
| - t.Fatalf("after a standby write readSnapshotMeta = %+v, want Box \"standby-box\"", got) |
| + got = readSnapshotMeta(ctx, s, "dead-box") |
| + if got == nil || !got.CreatedAt.Equal(at.Add(5*time.Minute)) { |
| + t.Fatalf("after the reused-name write readSnapshotMeta = %+v, want CreatedAt %s", got, at.Add(5*time.Minute)) |
| } |
| } |
| |
| @@ -221,7 +231,7 @@ func TestSnapshotPassPrunesGenerationsBeyondKeep(t *testing.T) { |
| t.Fatal(err) |
| } |
| } |
| - gens, err := s.List(ctx, snapshotGenPrefix) |
| + gens, err := s.List(ctx, snapshotGenPrefix("box1")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -229,14 +239,52 @@ func TestSnapshotPassPrunesGenerationsBeyondKeep(t *testing.T) { |
| t.Fatalf("kept %d generations, want %d: %v", len(gens), snapshotKeep, gens) |
| } |
| // The survivors must be the newest ones: the earliest kept stamp is round 3's. |
| - if gens[0] != snapshotGenPrefix+"20260812T001500Z.db" { |
| + if gens[0] != snapshotGenPrefix("box1")+"20260812T001500Z.db" { |
| t.Fatalf("oldest kept generation = %q, want the round-3 stamp", gens[0]) |
| } |
| } |
| |
| +func TestSnapshotKeysArePerBox(t *testing.T) { |
| + if got, want := snapshotDBKey("annie"), "control/registry-snapshot/annie.db"; got != want { |
| + t.Fatalf("db key = %q, want %q", got, want) |
| + } |
| + if got, want := snapshotMetaKey("annie"), "control/registry-snapshot/annie.json"; got != want { |
| + t.Fatalf("meta key = %q, want %q", got, want) |
| + } |
| + if got, want := snapshotGenPrefix("annie"), "control/registry-snapshots/annie/"; got != want { |
| + t.Fatalf("gen prefix = %q, want %q", got, want) |
| + } |
| +} |
| + |
| +func TestTwoBoxesSnapshotWithoutColliding(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + regA, regB := snapTestRegistry(t), snapTestRegistry(t) |
| + var fpA, fpB string |
| + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) |
| + if err := registrySnapshotPass(context.Background(), s, regA, "annie", now, &fpA); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := registrySnapshotPass(context.Background(), s, regB, "betty", now, &fpB); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, key := range []string{ |
| + "control/registry-snapshot/annie.db", "control/registry-snapshot/annie.json", |
| + "control/registry-snapshot/betty.db", "control/registry-snapshot/betty.json", |
| + } { |
| + if _, _, err := s.Get(context.Background(), key); err != nil { |
| + t.Fatalf("missing %s: %v", key, err) |
| + } |
| + } |
| + // betty's pass must not have disturbed annie's generations. |
| + gens, err := s.List(context.Background(), "control/registry-snapshots/annie/") |
| + if err != nil || len(gens) != 1 { |
| + t.Fatalf("annie generations = %v, %v; want exactly 1", gens, err) |
| + } |
| +} |
| + |
| func TestSeedSnapshotFingerprintEmptyWhenNoSidecar(t *testing.T) { |
| s := release.NewDirStore(t.TempDir()) |
| - if got := seedSnapshotFingerprint(context.Background(), s); got != "" { |
| + if got := seedSnapshotFingerprint(context.Background(), s, "box1"); got != "" { |
| t.Fatalf("seedSnapshotFingerprint = %q, want empty with no sidecar written yet", got) |
| } |
| } |
| @@ -261,7 +309,7 @@ func TestSeedSnapshotFingerprintMatchingSidecarSkipsFirstPass(t *testing.T) { |
| } |
| |
| // A fresh process seeds lastFP from the bucket instead of starting empty. |
| - seeded := seedSnapshotFingerprint(ctx, s) |
| + seeded := seedSnapshotFingerprint(ctx, s, "box1") |
| if seeded == "" || seeded != priorFP { |
| t.Fatalf("seeded fingerprint = %q, want %q", seeded, priorFP) |
| } |
| @@ -270,7 +318,7 @@ func TestSeedSnapshotFingerprintMatchingSidecarSkipsFirstPass(t *testing.T) { |
| if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(5*time.Minute), &lastFP); err != nil { |
| t.Fatal(err) |
| } |
| - gens, err := s.List(ctx, snapshotGenPrefix) |
| + gens, err := s.List(ctx, snapshotGenPrefix("box1")) |
| if err != nil { |
| t.Fatal(err) |
| } |