| index 3c3bd1965..7c8e36d6c 100644 |
| --- a/internal/hostbox/reconcile_relocation_test.go |
| +++ b/internal/hostbox/reconcile_relocation_test.go |
| @@ -225,7 +225,11 @@ func TestReconcileWithAFenceObserverLeavesMovesToMain(t *testing.T) { |
| } |
| |
| // Install asks the fence observer once for its own decision: the hold-only |
| -// path acts on the answer the decision was made from. |
| +// path acts on the answer the decision was made from. The source in |
| +// releasing is the one row case that depends on the observer, so it is |
| +// asked here three times (check, Install, holdOnly), as before the final |
| +// review's fix; a box whose case does not depend on it is never asked |
| +// (TestReconcileRelocationTargetWithNoLocalRecordMintsItsRow). |
| func TestReconcileInstallAsksTheFenceObserverOnce(t *testing.T) { |
| r, selected, holds, _ := relocationReconciler(t) |
| relocate(t, r, selected.Host, hostclaim.PhaseReleasing) |
| @@ -252,6 +256,75 @@ func TestReconcileInstallAsksTheFenceObserverOnce(t *testing.T) { |
| } |
| } |
| |
| +// targetReconciler is relocationReconciler moved onto the target "dst": |
| +// its own guard directory, with no local record for the host, and the |
| +// real fence observer over it, which errors on a box with no record. |
| +func targetReconciler(t *testing.T, r Reconciler) (Reconciler, hostguard.Guard, *int) { |
| + t.Helper() |
| + dir := t.TempDir() |
| + locks := filepath.Join(dir, "locks") |
| + for _, p := range []string{locks, filepath.Join(dir, "hostguard")} { |
| + if err := os.Mkdir(p, 0770); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + guard := hostguard.Guard{DataDir: dir, LockDir: locks, Claims: hostguard.StoreReader{Store: r.Claims}} |
| + target := WorkSource{Guard: guard, Box: "dst", Region: "ie", Now: r.Now, |
| + Policy: Policy{Held: func(context.Context, string) (string, error) { return "", nil }, Relocation: RelocationPolicy("dst")}.Check, |
| + Local: func(context.Context, *hostclaim.Claim, Selection) error { return nil }} |
| + r.Box, r.Guard, r.Writes.Selector = "dst", guard, target |
| + r.Local = park.Local{Guard: guard, Claims: r.Claims, DataDir: dir, Box: "dst"} |
| + calls := new(int) |
| + r.FenceClosed = func(_ context.Context, o hostclaim.Observed) (bool, error) { |
| + *calls++ |
| + captured, err := hostguard.Capture(dir, o.Host) |
| + if err != nil { |
| + return false, err |
| + } |
| + return guard.AdmissionFenced(o.Host, o.Claim, captured) |
| + } |
| + return r, guard, calls |
| +} |
| + |
| +// Final review Important 2: with region HA on, a relocation target that |
| +// has no local guard record yet mints its row through Install. Its row |
| +// case (held, as op.exec and a participant) does not depend on the fence, |
| +// so the observer, which errors without a record, is never asked; the |
| +// guarded path activates the installation and runs the write. |
| +func TestReconcileRelocationTargetWithNoLocalRecordMintsItsRow(t *testing.T) { |
| + r, selected, holds, _ := relocationReconciler(t) |
| + r, guard, calls := targetReconciler(t, r) |
| + ctx := context.Background() |
| + obs, err := hostclaim.Read(ctx, r.Claims, selected.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + obs, err = hostclaim.Update(ctx, r.Claims, obs, func(c *hostclaim.Claim) error { |
| + c.Op = hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "rel-1", Phase: hostclaim.PhaseInstalling, Exec: "dst", Participants: []string{"box", "dst"}, |
| + Intent: map[string]string{"region": "ie", "source_box": "box", "target_box": "dst"}, Journal: map[string]string{"exec_ack.box": "{}", "exec_ack.dst": "{}"}} |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + // The production observer cannot answer here: no record. |
| + if _, err := r.FenceClosed(ctx, obs); err == nil { |
| + t.Fatal("the observer answered for a box with no local record") |
| + } |
| + *calls = 0 |
| + writes := 0 |
| + if err := r.Install(ctx, selected, func() error { writes++; return nil }); err != nil { |
| + t.Fatalf("the target's first install: %v", err) |
| + } |
| + if writes != 1 || len(*holds) != 0 || *calls != 0 { |
| + t.Fatalf("writes %d holds %v observer calls %d", writes, *holds, *calls) |
| + } |
| + captured, err := hostguard.Capture(guard.DataDir, selected.Host) |
| + if err != nil || captured.Stamp != obs.Claim.Holder.Stamp { |
| + t.Fatalf("the target's installation was not activated: %+v %v", captured, err) |
| + } |
| +} |
| + |
| // snapshotDir is every regular file under dir, by relative path. |
| func snapshotDir(t *testing.T, dir string) map[string]string { |
| t.Helper() |