| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "io" |
| 8 | "net/http" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/dbdoor" |
| 14 | "github.com/carlosframework/platform/internal/genselect" |
| 15 | "github.com/carlosframework/platform/internal/hostclaim" |
| 16 | "github.com/superfly/ltx" |
| 17 | ) |
| 18 | |
| 19 | func doorLTXHeader(t *testing.T, lo, hi uint64) []byte { |
| 20 | t.Helper() |
| 21 | h := ltx.Header{Version: ltx.Version, Flags: ltx.HeaderFlagNoChecksum, PageSize: 4096, MinTXID: ltx.TXID(lo), MaxTXID: ltx.TXID(hi)} |
| 22 | b, err := h.MarshalBinary() |
| 23 | if err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | return b |
| 27 | } |
| 28 | |
| 29 | func TestDoorPlanHTTPGenerationSelection(t *testing.T) { |
| 30 | for _, mode := range []string{"origin", "published", "unfinished", "unfinished-origin", "gap", "missing-pin", "partial-list", "disagreement", "bad-header", "holder-change", "op-change"} { |
| 31 | t.Run(mode, func(t *testing.T) { |
| 32 | fx := dbFixture(t) |
| 33 | model := enableDoorPlan(t, fx) |
| 34 | fx.srv.cfg.HomeRegion = "ie" |
| 35 | obs, err := hostclaim.Read(t.Context(), fx.rel, dbTestHost) |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | c := cloneDoorClaim(obs.Claim) |
| 40 | loc := c.Holder.Location |
| 41 | gen := "live" |
| 42 | if mode == "origin" || mode == "unfinished-origin" { |
| 43 | gen = "" |
| 44 | } |
| 45 | c.Holder.Generation = gen |
| 46 | base := loc |
| 47 | if gen != "" { |
| 48 | base += "/g-" + gen |
| 49 | } |
| 50 | put := func(level int, lo, hi uint64) genselect.File { |
| 51 | key := dbVersionKey(base, level, lo, hi) |
| 52 | b := doorLTXHeader(t, lo, hi) |
| 53 | id := model.Put("data-test", key, b) |
| 54 | return genselect.File{Key: key, VersionID: id, Level: level, MinTXID: lo, MaxTXID: hi, Size: int64(len(b))} |
| 55 | } |
| 56 | root := put(9, 1, 11) |
| 57 | cont := put(1, 10, 13) |
| 58 | put(0, 11, 11) |
| 59 | put(0, 12, 12) |
| 60 | if gen != "" { |
| 61 | marker := genselect.Marker{Op: gen, Host: dbTestHost, Incarnation: c.Holder.Incarnation, Box: "box", InstalledAt: time.Unix(1, 0).UTC(), Pin: genselect.Pin{SnapshotKey: root.Key, SnapshotVersion: root.VersionID, SnapshotTXID: 11}} |
| 62 | raw, err := json.Marshal(marker) |
| 63 | if err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | model.Put("data-test", loc+"/generation.json", raw) |
| 67 | model.Put("data-test", root.Key, []byte("late duplicate must never be selected")) |
| 68 | model.Put("data-test", dbVersionKey(loc, 9, 1, 99), doorLTXHeader(t, 1, 99)) |
| 69 | } |
| 70 | switch mode { |
| 71 | case "unfinished", "unfinished-origin": |
| 72 | predecessor := gen |
| 73 | if predecessor == "" { |
| 74 | predecessor = hostclaim.RootGeneration |
| 75 | } |
| 76 | c.Holder.Generation = "next" |
| 77 | c.Holder.Admissions = []hostclaim.AdmissionRecord{{Generation: "next", Predecessor: predecessor, Region: "ie", PredecessorRegion: "ie", Mode: "cold"}} |
| 78 | case "gap": |
| 79 | model.BareDelete("data-test", cont.Key) |
| 80 | model.BareDelete("data-test", dbVersionKey(base, 0, 12, 12)) |
| 81 | put(0, 13, 13) |
| 82 | case "missing-pin": |
| 83 | if err := model.DeleteVersion(t.Context(), "data-test", root.Key, root.VersionID); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | case "partial-list": |
| 87 | model.PageSize, model.FailListAfterPages = 1, 1 |
| 88 | case "disagreement": |
| 89 | c.Holder.Generation = "unknown" |
| 90 | case "bad-header": |
| 91 | model.Put("data-test", dbVersionKey(base, 9, 1, 20), []byte("broken successor")) |
| 92 | } |
| 93 | writeDoorClaim(t, fx.srv, dbRequest{host: dbTestHost}, c) |
| 94 | if mode == "holder-change" || mode == "op-change" { |
| 95 | fx.srv.cfg.DBVersions = doorOpenHook{VersionedStore: model, open: func(ctx context.Context, bucket, key, version string) (io.ReadCloser, error) { |
| 96 | rc, err := model.OpenVersion(ctx, bucket, key, version) |
| 97 | next := cloneDoorClaim(c) |
| 98 | if mode == "holder-change" { |
| 99 | next.Holder.Session++ |
| 100 | } else { |
| 101 | next.Op.Exec = "changed" |
| 102 | } |
| 103 | writeDoorClaim(t, fx.srv, dbRequest{host: dbTestHost}, next) |
| 104 | return rc, err |
| 105 | }} |
| 106 | } |
| 107 | status, h, body := rawReq(t, "GET", fx.url(dbdoor.ListPath("bab", "titogo", dbTestHost, 9)), fx.member, "") |
| 108 | if mode != "origin" && mode != "published" && mode != "unfinished" && mode != "unfinished-origin" { |
| 109 | if status != 409 || h.Get("Retry-After") != "1" || strings.Contains(body, "files") || strings.Contains(body, "instances/") || strings.Contains(body, "genselect:") { |
| 110 | t.Fatalf("refusal: %d %v %s", status, h, body) |
| 111 | } |
| 112 | return |
| 113 | } |
| 114 | if status != 200 { |
| 115 | t.Fatalf("list: %d %s", status, body) |
| 116 | } |
| 117 | var list dbdoor.ListResponse |
| 118 | if err := json.Unmarshal([]byte(body), &list); err != nil { |
| 119 | t.Fatal(err) |
| 120 | } |
| 121 | if list.Protocol != 2 || len(list.Files) != 2 { |
| 122 | t.Fatalf("list = %+v", list) |
| 123 | } |
| 124 | for i, want := range []genselect.File{root, cont} { |
| 125 | f := list.Files[i] |
| 126 | handle, err := verifyDBHandle(fx.srv.dbHandleKey, f.Handle, time.Now()) |
| 127 | if err != nil || handle.Key != want.Key || handle.VersionID != want.VersionID || handle.Size != want.Size || f.Size != want.Size || f.Level != want.Level || f.MinTXID != want.MinTXID || f.MaxTXID != want.MaxTXID { |
| 128 | t.Fatalf("file %d: %+v %+v %v; want %+v", i, f, handle, err, want) |
| 129 | } |
| 130 | model.Put("data-test", want.Key, []byte("overwritten after listing")) |
| 131 | req, err := http.NewRequest("GET", fx.url(dbdoor.GetPath("bab", "titogo", dbTestHost, f.Level, f.MinTXID, f.MaxTXID)), nil) |
| 132 | if err != nil { |
| 133 | t.Fatal(err) |
| 134 | } |
| 135 | req.Header.Set("Authorization", "Bearer "+fx.member) |
| 136 | req.Header.Set(dbdoor.HandleHeader, f.Handle) |
| 137 | req.Header.Set("Range", "bytes=2-7") |
| 138 | res, err := http.DefaultClient.Do(req) |
| 139 | if err != nil { |
| 140 | t.Fatal(err) |
| 141 | } |
| 142 | got, err := io.ReadAll(res.Body) |
| 143 | res.Body.Close() |
| 144 | if err != nil || res.StatusCode != 206 || !bytes.Equal(got, doorLTXHeader(t, want.MinTXID, want.MaxTXID)[2:8]) || res.Header.Get(dbdoor.HandleHeader) != f.Handle { |
| 145 | t.Fatalf("get: %d %x %v", res.StatusCode, got, err) |
| 146 | } |
| 147 | if err := model.DeleteVersion(t.Context(), "data-test", want.Key, want.VersionID); err != nil { |
| 148 | t.Fatal(err) |
| 149 | } |
| 150 | res, err = http.DefaultClient.Do(req) |
| 151 | if err != nil { |
| 152 | t.Fatal(err) |
| 153 | } |
| 154 | res.Body.Close() |
| 155 | if res.StatusCode != 404 { |
| 156 | t.Fatalf("missing version fell back: %d", res.StatusCode) |
| 157 | } |
| 158 | } |
| 159 | if fx.opens.Load() != 0 { |
| 160 | t.Fatal("opened unversioned replica") |
| 161 | } |
| 162 | }) |
| 163 | } |
| 164 | } |
| 165 | |