| 1 | //go:build unix |
| 2 | |
| 3 | package hostbox |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/genselect" |
| 14 | "github.com/carlosframework/platform/internal/hostclaim" |
| 15 | "github.com/carlosframework/platform/internal/lease/leasetest" |
| 16 | "github.com/superfly/ltx" |
| 17 | ) |
| 18 | |
| 19 | // restorePlan is a restore plan whose one selected file is the snapshot |
| 20 | // [1, max] of generation gen ("" is the location's own files) under |
| 21 | // location: what Select hands a loss admission. |
| 22 | func restorePlan(location, gen string, max uint64) *genselect.Plan { |
| 23 | base := strings.TrimSuffix(location, "/") + "/" |
| 24 | if gen != "" && gen != hostclaim.RootGeneration { |
| 25 | base += "g-" + gen + "/" |
| 26 | } |
| 27 | f := genselect.File{Key: base + "0009/" + ltx.FormatFilename(1, ltx.TXID(max)), VersionID: "v1", Level: 9, MinTXID: 1, MaxTXID: max, Size: 100} |
| 28 | g := gen |
| 29 | if g == "" { |
| 30 | g = hostclaim.RootGeneration |
| 31 | } |
| 32 | return &genselect.Plan{Generation: g, Files: []genselect.File{f}, Root: &f, MaxTXID: max} |
| 33 | } |
| 34 | |
| 35 | // moves Task 4 (platform-1e's ruling on #236): a loss admission's restore |
| 36 | // point is consistent with its own plan: the admission's RemoteMaxTXID is |
| 37 | // the plan's maximum, which genselect.GenerationMax reads from the plan's |
| 38 | // selected files of its own generation, never a fresh listing. A plan that |
| 39 | // carries only a maximum, or whose files do not reach it, is refused before |
| 40 | // the loss sources run and before the claim CAS; no bucket is read, so a |
| 41 | // writer with none still admits a consistent plan. |
| 42 | func TestLossRestorePointIsTheGenerationMax(t *testing.T) { |
| 43 | for _, path := range []string{"declared", "fenced"} { |
| 44 | for _, mode := range []string{"only a maximum", "files short of it", "another input", "consistent, no bucket"} { |
| 45 | t.Run(path+"/"+mode, func(t *testing.T) { |
| 46 | _, store, o := workFixture(t) |
| 47 | o, err := hostclaim.Update(t.Context(), store, o, func(c *hostclaim.Claim) error { |
| 48 | c.Op = hostclaim.Op{Kind: hostclaim.KindRestoring, ID: "op", Phase: hostclaim.PhaseProbing, Exec: "box", Participants: []string{"box"}, Intent: map[string]string{"target": "target"}} |
| 49 | return nil |
| 50 | }) |
| 51 | if err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | w := Writer{Claims: store, Box: "box", Region: "ie"} |
| 55 | in := hostclaim.Admission{Box: "box", Mode: "cold", Region: "ie", RemoteMaxTXID: 3} |
| 56 | baseline := restorePlan(o.Claim.Holder.Location, "src", 3) |
| 57 | switch mode { |
| 58 | case "only a maximum": |
| 59 | baseline = &genselect.Plan{Generation: "src", MaxTXID: 3} |
| 60 | case "files short of it": |
| 61 | baseline = restorePlan(o.Claim.Holder.Location, "src", 2) |
| 62 | baseline.MaxTXID = 3 |
| 63 | case "another input": |
| 64 | in.RemoteMaxTXID = 4 |
| 65 | } |
| 66 | err = w.restorePoint(t.Context(), o, in, baseline) |
| 67 | if mode == "consistent, no bucket" { |
| 68 | if err != nil { |
| 69 | t.Fatalf("a consistent plan was refused: %v", err) |
| 70 | } |
| 71 | return |
| 72 | } |
| 73 | if !errors.Is(err, ErrChanged) { |
| 74 | t.Fatalf("refused with %v", err) |
| 75 | } |
| 76 | sources := 0 |
| 77 | w.Loss = &LossAdmission{PublicKey: make([]byte, 32), Owner: &plan12LossOwner{}, Sources: func(context.Context, hostclaim.Observed, hostclaim.LossRecord, hostclaim.Admission, *genselect.Plan) ([]hostclaim.ValidatedWriteSource, error) { |
| 78 | sources++ |
| 79 | return nil, nil |
| 80 | }} |
| 81 | if path == "declared" { |
| 82 | _, err = w.admitLoss(t.Context(), o, in, baseline, "loss_1") |
| 83 | } else { |
| 84 | _, err = w.admitFencedLoss(t.Context(), o, in, baseline, hostclaim.LossRecord{}) |
| 85 | } |
| 86 | if !errors.Is(err, ErrChanged) || sources != 0 { |
| 87 | t.Fatalf("%s: %v, the loss sources ran %d times", path, err, sources) |
| 88 | } |
| 89 | }) |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // Fix round 1, item 2: the restore point honours the authorized pin, as |
| 95 | // Select does. An ordinary selection whose only usable root is the marker's |
| 96 | // pinned snapshot, surviving only as a historical version (its current |
| 97 | // version a retention tombstone), is accepted at the pin's maximum. |
| 98 | func TestLossRestorePointHonoursThePinnedRoot(t *testing.T) { |
| 99 | _, store, o := workFixture(t) |
| 100 | o, err := hostclaim.Update(t.Context(), store, o, func(c *hostclaim.Claim) error { |
| 101 | c.Holder.Generation = "src" |
| 102 | c.Op = hostclaim.Op{Kind: hostclaim.KindRestoring, ID: "op", Phase: hostclaim.PhaseProbing, Exec: "box", Participants: []string{"box"}, Intent: map[string]string{"target": "target"}} |
| 103 | return nil |
| 104 | }) |
| 105 | if err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | h := o.Claim.Holder |
| 109 | data := leasetest.NewVersionedBucket() |
| 110 | data.AddBucket("data") |
| 111 | header, err := (<x.Header{Version: ltx.Version, Flags: ltx.HeaderFlagNoChecksum, PageSize: 4096, MinTXID: 1, MaxTXID: 5}).MarshalBinary() |
| 112 | if err != nil { |
| 113 | t.Fatal(err) |
| 114 | } |
| 115 | root := strings.TrimSuffix(h.Location, "/") + "/g-src/0009/" + ltx.FormatFilename(1, 5) |
| 116 | version := data.Put("data", root, header) |
| 117 | marker, err := json.Marshal(genselect.Marker{Op: "src", Incarnation: h.Incarnation, Host: o.Host, Box: "box", InstalledAt: time.Unix(1, 0).UTC(), |
| 118 | Pin: genselect.Pin{SnapshotKey: root, SnapshotVersion: version, SnapshotTXID: 5}}) |
| 119 | if err != nil { |
| 120 | t.Fatal(err) |
| 121 | } |
| 122 | data.Put("data", strings.TrimSuffix(h.Location, "/")+"/generation.json", marker) |
| 123 | data.BareDelete("data", root) |
| 124 | w := Writer{Claims: store, Box: "box", Region: "ie", Bucket: "data", Data: data} |
| 125 | plan, err := w.Baseline(t.Context(), o) |
| 126 | if err != nil { |
| 127 | t.Fatal(err) |
| 128 | } |
| 129 | if plan.Root == nil || plan.Root.VersionID != version || plan.MaxTXID != 5 { |
| 130 | t.Fatalf("the selection is not the pinned historical root: %+v", plan) |
| 131 | } |
| 132 | if err := w.restorePoint(t.Context(), o, hostclaim.Admission{RemoteMaxTXID: plan.MaxTXID}, plan); err != nil { |
| 133 | t.Fatalf("a valid pinned restore point was refused: %v", err) |
| 134 | } |
| 135 | } |
| 136 | |