//go:build linux package host import ( "bytes" "fmt" "log" "os" "testing" "github.com/carlosframework/platform/internal/instancectl" ) func TestServingParkRetainedDoesNotBlockRetirement(t *testing.T) { for _, attack := range []string{"unreadable-volume", "changed-volume", "hardlink", "unreadable-binding"} { t.Run(attack, func(t *testing.T) { f, old := servingRebootRecordFixture(t, "complete") servingRenumberHistory(t, f, &old) if attack == "unreadable-volume" { f.fs.volume = func(uint64) string { return "" } } if attack == "changed-volume" { f.fs.volume = func(uint64) string { return "vol11111111111111111" } } root := &servingParkRoot{ledger: f.ledger} if err := root.commitServingReboot(t.Context(), old.Authorization.ID); err != nil { t.Fatal(err) } if attack == "hardlink" { if err := os.Link(f.path(old.Authorization, ""), f.path(old.Authorization, "")+".extra"); err != nil { t.Fatal(err) } } if attack == "unreadable-binding" { if err := f.disk.Write("current-retirement-"+old.Authorization.ID+".json", []byte("invalid")); err != nil { t.Fatal(err) } } var report bytes.Buffer output := log.Writer() log.SetOutput(&report) defer log.SetOutput(output) for n := uint64(2); n <= 3; n++ { body := fmt.Sprintf("tenant writes %d", n) f.put("", body) next, stops := f.evidence(n, false, "started") parked, err := f.park(next, stops) if err != nil { t.Fatalf("retained park blocked retirement %d: %v", n, err) } f.complete(next) f.retained(next, body, parked.ReapAfter) } saved, err := f.ledger.readHistorical(old.Authorization.ID) if err != nil || saved.Closed || !saved.ReapAfter.Equal(old.ReapAfter) { t.Fatal("lost retained record", saved, err) } for suffix, want := range map[string]string{"": "last database", "-wal": "last WAL", "-shm": "last SHM"} { got, err := os.ReadFile(f.path(old.Authorization, suffix)) if err != nil || string(got) != want { t.Fatalf("retained %s: %q %v", suffix, got, err) } } ids, more, err := f.ledger.recoveryIDs(t.Context(), "", servingParkRecoveryLimit) if err != nil || more || len(ids) != 3 { t.Fatal("retained record missing from census", ids, more, err) } if !bytes.Contains(report.Bytes(), []byte(old.Authorization.ID)) { t.Fatal("retained park not reported", report.String()) } if err := f.ledger.maintainParks(t.Context(), func() error { return instancectl.ErrDiskFull }); err == nil { t.Fatal("pressure granted deletion of retained park") } if _, err := os.Stat(f.path(old.Authorization, "")); err != nil { t.Fatal("pressure lost retained park", err) } }) } } func TestServingParkRetainedReplacementWriteFailures(t *testing.T) { for _, before := range []bool{false, true} { for boundary := 1; boundary <= 18; boundary++ { t.Run(fmt.Sprintf("before-%t-write-%d", before, boundary), func(t *testing.T) { f, old := servingRebootRecordFixture(t, "complete") servingRenumberHistory(t, f, &old) f.fs.volume = func(uint64) string { return "" } root := &servingParkRoot{ledger: f.ledger} if err := root.commitServingReboot(t.Context(), old.Authorization.ID); err != nil { t.Fatal(err) } next, stops := f.evidence(2, false, "started") faults := &servingParkFaultStore{Store: f.disk, failAt: boundary, before: before} f.ledger.state, f.fs.state = faults, faults if _, err := f.park(next, stops); err == nil { _ = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.completeLocked(t.Context(), next.ID, next.Successor) }) } f.ledger.state, f.fs.state = f.disk, f.disk parked, err := f.park(next, stops) if err != nil { t.Fatal("retirement retry", err) } f.complete(next) f.retained(next, "successor database", parked.ReapAfter) got, err := os.ReadFile(f.path(old.Authorization, "")) if err != nil || string(got) != "last database" { t.Fatalf("retained bytes after retry: %q %v", got, err) } saved, err := f.ledger.readHistorical(old.Authorization.ID) if err != nil || saved.Closed || !saved.ReapAfter.Equal(old.ReapAfter) { t.Fatal("retained record after retry", saved, err) } }) } } }