| 1 | package park |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostclaim" |
| 11 | ) |
| 12 | |
| 13 | func coverageFixture(t *testing.T, foreign bool) *fixture { |
| 14 | t.Helper() |
| 15 | f := newFixture(t) |
| 16 | allowFixtureMoveSweep(f) |
| 17 | obs, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| 18 | if err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | _, err = hostclaim.Update(t.Context(), f.e.Claims, obs, func(c *hostclaim.Claim) error { |
| 22 | c.Obligations = nil |
| 23 | c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"} |
| 24 | c.Holder.Generation = "live" |
| 25 | c.Holder.Session = 3 |
| 26 | c.Holder.Admissions = []hostclaim.AdmissionRecord{ |
| 27 | {Region: "uk", PredecessorRegion: "ie", Predecessor: "dead"}, |
| 28 | {Region: "ie", PredecessorRegion: "uk", Predecessor: "dead"}, |
| 29 | {Region: "ie", PredecessorRegion: "ie", Predecessor: "unseen"}, |
| 30 | {Region: "ie", PredecessorRegion: "ie", Predecessor: "stage-only"}, |
| 31 | {Region: "ie", PredecessorRegion: "ie", Predecessor: "old-only"}, |
| 32 | {Region: "uk", PredecessorRegion: "ie", Predecessor: ""}, |
| 33 | } |
| 34 | return nil |
| 35 | }) |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | base := f.ob.Park.Location + "/" |
| 40 | for _, key := range []string{ |
| 41 | base + "dead/data", base + "0000/data", |
| 42 | "moves/" + f.w.Host + "/stage-only/data", |
| 43 | "instances/" + f.w.Host + "/" + strings.Repeat("a", 64) + "/old-only/data", |
| 44 | } { |
| 45 | f.b.Put("data", key, []byte(key)) |
| 46 | } |
| 47 | f.e.View = func(_ context.Context, o hostclaim.Observed) (View, error) { |
| 48 | authority := "ie" |
| 49 | if foreign { |
| 50 | authority = "uk" |
| 51 | } |
| 52 | return View{ClaimETag: o.ETag, Region: "ie", AuthoritativeRegion: authority, |
| 53 | MarkerKnown: true, MarkerGeneration: "live", GenerationOf: func(r Ref) (string, bool) { |
| 54 | parts := strings.Split(r.Key, "/") |
| 55 | g := parts[len(parts)-2] |
| 56 | if g == "0000" { |
| 57 | g = "" |
| 58 | } |
| 59 | return g, true |
| 60 | }}, nil |
| 61 | } |
| 62 | return f |
| 63 | } |
| 64 | |
| 65 | func TestSweepQueueCoversOnlyQueuedPredecessors(t *testing.T) { |
| 66 | for _, foreign := range []bool{false, true} { |
| 67 | name := "generation" |
| 68 | if foreign { |
| 69 | name = "replica" |
| 70 | } |
| 71 | t.Run(name, func(t *testing.T) { |
| 72 | f := coverageFixture(t, foreign) |
| 73 | before := f.b.Snapshot() |
| 74 | f.held.failed = true |
| 75 | if _, err := f.e.Sweep(t.Context(), f.w.Host, f.ob.Park.Incarnation, f.ob.Park.EvidenceAt, time.Second); !errors.Is(err, ErrChanged) { |
| 76 | t.Fatal(err) |
| 77 | } |
| 78 | obs, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| 79 | if err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | if len(obs.Claim.Obligations) == 0 || f.b.Snapshot() != before { |
| 83 | t.Fatal("queue boundary did not preserve pending work and storage") |
| 84 | } |
| 85 | for i, a := range obs.Claim.Holder.Admissions { |
| 86 | if want := i == 0 || i == 5; a.Covered != want { |
| 87 | t.Errorf("queue admission %d covered=%v, want %v", i, a.Covered, want) |
| 88 | } |
| 89 | } |
| 90 | }) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func TestSweepAcknowledgementCoversOnlyRetiredPredecessors(t *testing.T) { |
| 95 | for _, scenario := range []string{"generation", "replica", "empty", "old-incarnation", "staging"} { |
| 96 | t.Run(scenario, func(t *testing.T) { |
| 97 | f := coverageFixture(t, scenario == "replica") |
| 98 | f.held.failed = true |
| 99 | if _, err := f.e.Sweep(t.Context(), f.w.Host, f.ob.Park.Incarnation, f.ob.Park.EvidenceAt, time.Second); !errors.Is(err, ErrChanged) { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | obs, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| 103 | if err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | var chore hostclaim.Obligation |
| 107 | for _, o := range obs.Claim.Obligations { |
| 108 | match := o.Park.Location == f.ob.Park.Location && (o.Generation == "dead" || scenario == "replica") |
| 109 | if scenario == "old-incarnation" { |
| 110 | match = strings.HasSuffix(o.Park.Location, strings.Repeat("a", 64)) |
| 111 | } else if scenario == "staging" { |
| 112 | match = strings.HasPrefix(o.Park.Location, "moves/") |
| 113 | } |
| 114 | if match { |
| 115 | chore = o |
| 116 | break |
| 117 | } |
| 118 | } |
| 119 | if chore.ID == "" { |
| 120 | t.Fatal("target chore was not queued") |
| 121 | } |
| 122 | if scenario == "empty" { |
| 123 | versions, err := f.e.list(t.Context(), f.w.Host, chore.Park.Location) |
| 124 | if err != nil { |
| 125 | t.Fatal(err) |
| 126 | } |
| 127 | for _, x := range versions { |
| 128 | if strings.Contains(x.Key, "/dead/") { |
| 129 | if err := f.b.DeleteVersion(t.Context(), "data", x.Key, x.VersionID); err != nil { |
| 130 | t.Fatal(err) |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | _, err = hostclaim.Update(t.Context(), f.e.Claims, obs, func(c *hostclaim.Claim) error { |
| 136 | later := append([]hostclaim.AdmissionRecord(nil), c.Holder.Admissions...) |
| 137 | for i := range later { |
| 138 | later[i].Covered = false |
| 139 | } |
| 140 | c.Holder.Admissions = append(c.Holder.Admissions, later...) |
| 141 | return nil |
| 142 | }) |
| 143 | if err != nil { |
| 144 | t.Fatal(err) |
| 145 | } |
| 146 | f.held.failed = false |
| 147 | f.e.After = func(step string) error { |
| 148 | if step == "before-completion" { |
| 149 | return ErrPending |
| 150 | } |
| 151 | return nil |
| 152 | } |
| 153 | w := Work{f.w.Host, chore.ID, chore.Op} |
| 154 | if err := f.e.Run(t.Context(), w); !errors.Is(err, ErrPending) { |
| 155 | t.Fatal(err) |
| 156 | } |
| 157 | pending, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| 158 | if err != nil { |
| 159 | t.Fatal(err) |
| 160 | } |
| 161 | for _, a := range pending.Claim.Holder.Admissions[6:] { |
| 162 | if a.Covered { |
| 163 | t.Fatal("later admission covered before acknowledgement") |
| 164 | } |
| 165 | } |
| 166 | f.e.After = nil |
| 167 | f.b.FailOn = func(op, key string) error { |
| 168 | if op == "copy" { |
| 169 | t.Error("completion retry copied again") |
| 170 | return ErrUnsafe |
| 171 | } |
| 172 | return nil |
| 173 | } |
| 174 | if err := f.e.Run(t.Context(), w); err != nil { |
| 175 | t.Fatal(err) |
| 176 | } |
| 177 | completed, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| 178 | if err != nil { |
| 179 | t.Fatal(err) |
| 180 | } |
| 181 | if _, present := find(completed.Claim, w); present || f.held.released != 1 || completed.Claim.QuietAfter["ie"].IsZero() { |
| 182 | t.Fatal("retirement was not acknowledged") |
| 183 | } |
| 184 | for i, a := range completed.Claim.Holder.Admissions[6:] { |
| 185 | want := (i == 0 && (scenario == "generation" || scenario == "replica")) || (i == 5 && scenario == "replica") |
| 186 | if a.Covered != want { |
| 187 | t.Errorf("ack admission %d covered=%v, want %v", i, a.Covered, want) |
| 188 | } |
| 189 | } |
| 190 | }) |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | func TestSweepCoverageSurvivesRetirementRotation(t *testing.T) { |
| 195 | f := coverageFixture(t, true) |
| 196 | obs, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| 197 | if err != nil { |
| 198 | t.Fatal(err) |
| 199 | } |
| 200 | _, err = hostclaim.Update(t.Context(), f.e.Claims, obs, func(c *hostclaim.Claim) error { |
| 201 | c.Holder.Admissions = append(c.Holder.Admissions, hostclaim.AdmissionRecord{Region: "uk", PredecessorRegion: "ie", Predecessor: "late"}) |
| 202 | return nil |
| 203 | }) |
| 204 | if err != nil { |
| 205 | t.Fatal(err) |
| 206 | } |
| 207 | f.held.failed = true |
| 208 | if _, err := f.e.Sweep(t.Context(), f.w.Host, f.ob.Park.Incarnation, f.ob.Park.EvidenceAt, time.Second); !errors.Is(err, ErrChanged) { |
| 209 | t.Fatal(err) |
| 210 | } |
| 211 | obs, err = hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| 212 | if err != nil { |
| 213 | t.Fatal(err) |
| 214 | } |
| 215 | var w Work |
| 216 | for _, o := range obs.Claim.Obligations { |
| 217 | if o.Park.Location == f.ob.Park.Location { |
| 218 | w = Work{f.w.Host, o.ID, o.Op} |
| 219 | } |
| 220 | } |
| 221 | if w.ID == "" || obs.Claim.Holder.Admissions[6].Covered { |
| 222 | t.Fatal("late generation was queued or covered without an object") |
| 223 | } |
| 224 | f.held.failed = false |
| 225 | for _, next := range []string{"late", "last"} { |
| 226 | f.e.After = func(step string) error { |
| 227 | if step == "manifest" { |
| 228 | f.b.Put("data", f.ob.Park.Location+"/"+next+"/data", []byte(next)) |
| 229 | } |
| 230 | return nil |
| 231 | } |
| 232 | if err := f.e.Run(t.Context(), w); !errors.Is(err, ErrPending) { |
| 233 | t.Fatalf("retirement rotation: %v", err) |
| 234 | } |
| 235 | } |
| 236 | f.e.After = nil |
| 237 | if err := f.e.Run(t.Context(), w); err != nil { |
| 238 | t.Fatal(err) |
| 239 | } |
| 240 | completed, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| 241 | if err != nil { |
| 242 | t.Fatal(err) |
| 243 | } |
| 244 | if !completed.Claim.Holder.Admissions[6].Covered || completed.Claim.Holder.Admissions[2].Covered { |
| 245 | t.Fatalf("coverage lost a retired generation or covered unseen data: %+v", completed.Claim.Holder.Admissions) |
| 246 | } |
| 247 | if _, present := find(completed.Claim, w); present { |
| 248 | t.Fatal("rotated retirement did not complete") |
| 249 | } |
| 250 | } |
| 251 | |