| index 43b8e5692..a19528225 100644 |
| --- a/internal/layout/pass_linux_test.go |
| +++ b/internal/layout/pass_linux_test.go |
| @@ -928,8 +928,11 @@ func TestRecoverUnderAForeignFenceDefersAnAwakeHost(t *testing.T) { |
| |
| // Completing a journal is never gated, but taking the host on toward a |
| // nested intent is, as Converge(ModeNested) is. With the probe failing, a |
| -// revert completes and the host is released flat, which is safe. |
| -func TestRecoverGatesTheLegTowardNestedAndReleasesFlat(t *testing.T) { |
| +// revert completes and the host is held flat, not released: there is no |
| +// degraded mode (design §8.5), and a released flat host is exactly what a |
| +// forged revert wants. The journal at done is the fence, so the pass |
| +// lifts its in-memory one. |
| +func TestRecoverGatesTheLegTowardNestedAndHoldsFlat(t *testing.T) { |
| p, f := box(t) |
| if res := byHost(t, p.Converge(context.Background(), ModeNested)); res[host].Outcome != "migrated" { |
| t.Fatalf("setup: %+v", res) |
| @@ -942,10 +945,10 @@ func TestRecoverGatesTheLegTowardNestedAndReleasesFlat(t *testing.T) { |
| f.events = nil |
| res := byHost(t, p.Recover(context.Background(), ModeNested)) |
| for _, h := range []string{host, replica} { |
| - if r := res[h]; r.Outcome != "recovered" || !strings.Contains(r.Reason, "isolation not ready: probe: not ready") { |
| + if r := res[h]; r.Outcome != "deferred" || !strings.Contains(r.Reason, "isolation not ready: probe: not ready") { |
| t.Errorf("%s: %+v", h, r) |
| } |
| - assertFlatUntouched(t, p, h) |
| + assertHeldFlat(t, p, h) |
| } |
| if calls != 1 { |
| t.Errorf("NestedOK called %d times, want once per recovery", calls) |
| @@ -954,6 +957,29 @@ func TestRecoverGatesTheLegTowardNestedAndReleasesFlat(t *testing.T) { |
| if !reflect.DeepEqual(f.events, want) { |
| t.Fatalf("events = %v\nwant %v", f.events, want) |
| } |
| + // Once the probe passes, the next recovery takes the held host on to |
| + // nested. |
| + p.NestedOK = nil |
| + for h, r := range byHost(t, p.Recover(context.Background(), ModeNested)) { |
| + if r.Outcome != "recovered" || JournalPresent(p.DataDir, h) || rowAt(t, p, h) != endpoints(t, p, h, ModeNested) { |
| + t.Errorf("%s after the probe passed: %+v", h, r) |
| + } |
| + } |
| +} |
| + |
| +// assertHeldFlat is a host whose revert completed and is held: the row |
| +// and files at flat, and the journal still there at done. |
| +func assertHeldFlat(t *testing.T, p *Pass, h string) { |
| + t.Helper() |
| + if got := rowAt(t, p, h); got != endpoints(t, p, h, ModeFlat) { |
| + t.Errorf("%s row not at flat: %+v", h, got) |
| + } |
| + if b, err := os.ReadFile(provision.DefaultDBPath(p.DataDir, h)); err != nil || string(b) != "db "+h { |
| + t.Errorf("%s flat db: %q %v", h, b, err) |
| + } |
| + if j, found, err := ReadJournal(p.DataDir, h); err != nil || !found || j.Mode != ModeFlat || j.Phase != PhaseDone { |
| + t.Errorf("%s journal: %+v %v %v, want a flat journal held at done", h, j, found, err) |
| + } |
| } |
| |
| // addFlatRow adds a fresh flat managed row with its files, after any setup |
| @@ -1130,13 +1156,17 @@ func TestConvergeAsksTheIsolationGateOncePerPass(t *testing.T) { |
| if calls != 1 { |
| t.Fatalf("NestedOK called %d times in one pass", calls) |
| } |
| + want := "recovered" |
| + if tc.gate != nil { |
| + want = "deferred" |
| + } |
| for _, h := range []string{host, replica} { |
| - if res[h].Outcome != "recovered" { |
| - t.Errorf("%s: %+v", h, res[h]) |
| + if res[h].Outcome != want { |
| + t.Errorf("%s: %+v, want %s", h, res[h], want) |
| } |
| } |
| if tc.gate != nil { |
| - assertFlatUntouched(t, p, host) |
| + assertHeldFlat(t, p, host) |
| assertFlatUntouched(t, p, "f.example.com") |
| } else if res["f.example.com"].Outcome != "migrated" || rowAt(t, p, host) != endpoints(t, p, host, ModeNested) { |
| t.Errorf("ready probe: %+v", res) |