| 1 | package registry |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "path/filepath" |
| 6 | "testing" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | const ( |
| 11 | holdOpA = "mv_abcdefghijklmnopqrstuvwxyz" |
| 12 | holdOpB = "mv_other0000000000000000000000" |
| 13 | ) |
| 14 | |
| 15 | func holdFixture(t *testing.T, host string) *Registry { |
| 16 | t.Helper() |
| 17 | r := openTestRegistry(t) |
| 18 | if err := r.Upsert(Route{Host: host, Kind: "instance", Socket: "/s", App: "a", Hibernate: true}); err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | return r |
| 22 | } |
| 23 | |
| 24 | func TestHoldIsConditional(t *testing.T) { |
| 25 | r := holdFixture(t, "h") |
| 26 | if err := r.SetHold("h", "moving", holdOpA); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | rt, _ := r.Get("h") |
| 30 | if rt.Status != "moving" || rt.HoldOp != holdOpA || !IsHold(rt.Status) { |
| 31 | t.Fatalf("held row: %+v", rt) |
| 32 | } |
| 33 | if err := r.SetHold("h", "ready", "x"); err == nil { |
| 34 | t.Fatal("ready is not a hold") |
| 35 | } |
| 36 | // §5.2: a late write from a finished operation matches nothing. |
| 37 | if changed, err := r.ReleaseHold("h", holdOpB); err != nil || changed { |
| 38 | t.Fatalf("release under another op: %v %v", changed, err) |
| 39 | } |
| 40 | if changed, err := r.ReleaseHold("h", holdOpA); err != nil || !changed { |
| 41 | t.Fatalf("release: %v %v", changed, err) |
| 42 | } |
| 43 | rt, _ = r.Get("h") |
| 44 | if rt.Status != "ready" || rt.HoldOp != "" { |
| 45 | t.Fatalf("after release: %+v", rt) |
| 46 | } |
| 47 | // Upsert preserves hold_op through routeCols. |
| 48 | if err := r.SetHold("h", "landing", holdOpA); err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | rt, _ = r.Get("h") |
| 52 | if err := r.Upsert(*rt); err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | rt, _ = r.Get("h") |
| 56 | if rt.HoldOp == "" { |
| 57 | t.Fatal("Upsert dropped hold_op") |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | func TestSetHoldRefusals(t *testing.T) { |
| 62 | r := holdFixture(t, "h") |
| 63 | for _, status := range []string{"moving", "landing", "restoring", "releasing"} { |
| 64 | if err := r.SetHold("h", status, holdOpA); err != nil { |
| 65 | t.Fatalf("%s: %v", status, err) |
| 66 | } |
| 67 | if rt, _ := r.Get("h"); rt.Status != status || rt.HoldOp != holdOpA { |
| 68 | t.Fatalf("%s: %+v", status, rt) |
| 69 | } |
| 70 | } |
| 71 | for _, status := range []string{"", "ready", "pending", "failed", "Moving"} { |
| 72 | if err := r.SetHold("h", status, holdOpA); err == nil { |
| 73 | t.Fatalf("status %q accepted as a hold", status) |
| 74 | } |
| 75 | } |
| 76 | if err := r.SetHold("h", "moving", ""); err == nil { |
| 77 | t.Fatal("a hold with no op was accepted") |
| 78 | } |
| 79 | if err := r.SetHold("nope", "moving", holdOpA); err == nil { |
| 80 | t.Fatal("SetHold on an unknown host did not error") |
| 81 | } |
| 82 | if IsHold("ready") || !IsHold("releasing") { |
| 83 | t.Fatal("IsHold disagrees with HoldStatuses") |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | // An Upsert from a writer that knows nothing of holds (carlos add, the |
| 88 | // reconciler's mint paths) must not release one: the incoming row has an empty |
| 89 | // hold_op and a ready status, and the held row keeps both (F29). |
| 90 | func TestUpsertDoesNotClearHold(t *testing.T) { |
| 91 | r := holdFixture(t, "h") |
| 92 | if err := r.SetHold("h", "moving", holdOpA); err != nil { |
| 93 | t.Fatal(err) |
| 94 | } |
| 95 | if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s2", App: "a", Hibernate: true}); err != nil { |
| 96 | t.Fatal(err) |
| 97 | } |
| 98 | rt, _ := r.Get("h") |
| 99 | if rt.Status != "moving" || rt.HoldOp != holdOpA { |
| 100 | t.Fatalf("Upsert released the hold: %+v", rt) |
| 101 | } |
| 102 | if rt.Socket != "/s2" { |
| 103 | t.Fatalf("the rest of the row did not update: %+v", rt) |
| 104 | } |
| 105 | // Only ReleaseHold ends it, after which Upsert is an ordinary write again. |
| 106 | if ok, err := r.ReleaseHold("h", holdOpA); err != nil || !ok { |
| 107 | t.Fatalf("release: %v %v", ok, err) |
| 108 | } |
| 109 | if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s3", Status: "starting"}); err != nil { |
| 110 | t.Fatal(err) |
| 111 | } |
| 112 | if rt, _ = r.Get("h"); rt.Status != "starting" || rt.HoldOp != "" { |
| 113 | t.Fatalf("Upsert on an unheld row: %+v", rt) |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | func TestHoldRows(t *testing.T) { |
| 118 | r := openTestRegistry(t) |
| 119 | if rows, err := r.HoldRows(); err != nil || len(rows) != 0 { |
| 120 | t.Fatalf("empty registry: %v %v", rows, err) |
| 121 | } |
| 122 | for _, h := range []string{"a", "b", "c"} { |
| 123 | if err := r.Upsert(Route{Host: h, Kind: "instance", Socket: "/s"}); err != nil { |
| 124 | t.Fatal(err) |
| 125 | } |
| 126 | } |
| 127 | if err := r.SetHold("c", "restoring", holdOpA); err != nil { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | if err := r.SetHold("a", "landing", holdOpB); err != nil { |
| 131 | t.Fatal(err) |
| 132 | } |
| 133 | rows, err := r.HoldRows() |
| 134 | if err != nil { |
| 135 | t.Fatal(err) |
| 136 | } |
| 137 | if len(rows) != 2 || rows[0].Host != "a" || rows[0].HoldOp != holdOpB || rows[0].Status != "landing" || |
| 138 | rows[1].Host != "c" || rows[1].HoldOp != holdOpA || rows[1].Status != "restoring" { |
| 139 | t.Fatalf("HoldRows = %+v", rows) |
| 140 | } |
| 141 | // A bare "moving" status written by SetStatus has no op, so it is not a |
| 142 | // held row in this sense. |
| 143 | if err := r.SetStatus("b", "moving"); err != nil { |
| 144 | t.Fatal(err) |
| 145 | } |
| 146 | if rows, _ = r.HoldRows(); len(rows) != 2 { |
| 147 | t.Fatalf("a status without an op counted as a hold: %+v", rows) |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | func TestDeleteIfHold(t *testing.T) { |
| 152 | r := holdFixture(t, "h") |
| 153 | // Not held: nothing is deleted. |
| 154 | if ok, err := r.DeleteIfHold("h", holdOpA); err != nil || ok { |
| 155 | t.Fatalf("unheld row: %v %v", ok, err) |
| 156 | } |
| 157 | if err := r.SetHold("h", "releasing", holdOpA); err != nil { |
| 158 | t.Fatal(err) |
| 159 | } |
| 160 | // Held by another op: nothing is deleted. |
| 161 | if ok, err := r.DeleteIfHold("h", holdOpB); err != nil || ok { |
| 162 | t.Fatalf("row held by another op: %v %v", ok, err) |
| 163 | } |
| 164 | // An empty op must not match unheld rows' empty hold_op. |
| 165 | if err := r.Upsert(Route{Host: "plain", Kind: "instance", Socket: "/s"}); err != nil { |
| 166 | t.Fatal(err) |
| 167 | } |
| 168 | if ok, err := r.DeleteIfHold("plain", ""); err != nil || ok { |
| 169 | t.Fatalf("empty op deleted an unheld row: %v %v", ok, err) |
| 170 | } |
| 171 | if rt, _ := r.Get("plain"); rt == nil { |
| 172 | t.Fatal("unheld row was deleted") |
| 173 | } |
| 174 | if rt, _ := r.Get("h"); rt == nil || rt.HoldOp != holdOpA { |
| 175 | t.Fatalf("held row changed by the refusals: %+v", rt) |
| 176 | } |
| 177 | if ok, err := r.DeleteIfHold("h", holdOpA); err != nil || !ok { |
| 178 | t.Fatalf("delete under the holding op: %v %v", ok, err) |
| 179 | } |
| 180 | if rt, _ := r.Get("h"); rt != nil { |
| 181 | t.Fatalf("row survived: %+v", rt) |
| 182 | } |
| 183 | if ok, err := r.DeleteIfHold("h", holdOpA); err != nil || ok { |
| 184 | t.Fatalf("second delete: %v %v", ok, err) |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | // The source's finalization (moves spec §7.9 step 3, ruling H5(d)): the row |
| 189 | // and the host's timer rows go in one transaction, and only while the op |
| 190 | // holds the row. Any refusal leaves both, and the neighbour's timers stay. |
| 191 | func TestDeleteIfHoldWithTimers(t *testing.T) { |
| 192 | r := holdFixture(t, "h") |
| 193 | if err := r.Upsert(Route{Host: "n", Kind: "instance", Socket: "/n", App: "a", Hibernate: true}); err != nil { |
| 194 | t.Fatal(err) |
| 195 | } |
| 196 | for _, tm := range []Timer{{Host: "h", Name: "nightly", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000}, |
| 197 | {Host: "h", Name: "hourly", Kind: "cron", Spec: "0 * * * *", NextAt: 2000}, |
| 198 | {Host: "n", Name: "nightly", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000}} { |
| 199 | if err := r.UpsertTimer(tm); err != nil { |
| 200 | t.Fatal(err) |
| 201 | } |
| 202 | } |
| 203 | timers := func(host string) int { |
| 204 | t.Helper() |
| 205 | got, err := r.TimersFor(host) |
| 206 | if err != nil { |
| 207 | t.Fatal(err) |
| 208 | } |
| 209 | return len(got) |
| 210 | } |
| 211 | // Not held, held by another op, an empty op: nothing goes. |
| 212 | for _, c := range []struct{ name, hold, op string }{{"unheld", "", holdOpA}, {"another op", holdOpB, holdOpA}, {"empty op", holdOpA, ""}} { |
| 213 | if c.hold != "" { |
| 214 | if err := r.SetHold("h", "moving", c.hold); err != nil { |
| 215 | t.Fatal(err) |
| 216 | } |
| 217 | } |
| 218 | if ok, err := r.DeleteIfHoldWithTimers("h", c.op); err != nil || ok { |
| 219 | t.Fatalf("%s: %v %v", c.name, ok, err) |
| 220 | } |
| 221 | if rt, _ := r.Get("h"); rt == nil || timers("h") != 2 { |
| 222 | t.Fatalf("%s: the row or its timers changed: %+v, %d timers", c.name, rt, timers("h")) |
| 223 | } |
| 224 | if c.hold != "" { |
| 225 | if ok, err := r.ReleaseHold("h", c.hold); err != nil || !ok { |
| 226 | t.Fatal(ok, err) |
| 227 | } |
| 228 | } |
| 229 | } |
| 230 | if err := r.SetHold("h", "moving", holdOpA); err != nil { |
| 231 | t.Fatal(err) |
| 232 | } |
| 233 | if ok, err := r.DeleteIfHoldWithTimers("h", holdOpA); err != nil || !ok { |
| 234 | t.Fatalf("delete under the holding op: %v %v", ok, err) |
| 235 | } |
| 236 | if rt, _ := r.Get("h"); rt != nil || timers("h") != 0 { |
| 237 | t.Fatalf("after the delete: %+v, %d timers", rt, timers("h")) |
| 238 | } |
| 239 | if rt, _ := r.Get("n"); rt == nil || timers("n") != 1 { |
| 240 | t.Fatalf("the neighbour changed: %+v, %d timers", rt, timers("n")) |
| 241 | } |
| 242 | // A replay finds no row held by the op and deletes nothing, timers |
| 243 | // written since included. |
| 244 | if err := r.UpsertTimer(Timer{Host: "h", Name: "late", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000}); err != nil { |
| 245 | t.Fatal(err) |
| 246 | } |
| 247 | if ok, err := r.DeleteIfHoldWithTimers("h", holdOpA); err != nil || ok || timers("h") != 1 { |
| 248 | t.Fatalf("replay: %v %v, %d timers", ok, err, timers("h")) |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | func TestReleaseHoldEmptyOpMatchesNothing(t *testing.T) { |
| 253 | r := holdFixture(t, "h") |
| 254 | if ok, err := r.ReleaseHold("h", ""); err != nil || ok { |
| 255 | t.Fatalf("release with empty op on an unheld row: %v %v", ok, err) |
| 256 | } |
| 257 | if err := r.SetStatus("h", "starting"); err != nil { |
| 258 | t.Fatal(err) |
| 259 | } |
| 260 | if ok, _ := r.ReleaseHold("h", ""); ok { |
| 261 | t.Fatal("an empty op released an unheld row and reset its status") |
| 262 | } |
| 263 | if rt, _ := r.Get("h"); rt.Status != "starting" { |
| 264 | t.Fatalf("status moved: %+v", rt) |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | // The migration is additive and idempotent: opening a registry that predates |
| 269 | // the column adds it, and opening again is a no-op. |
| 270 | func TestHoldOpMigrationIdempotentOnLegacyDB(t *testing.T) { |
| 271 | path := filepath.Join(t.TempDir(), "registry.db") |
| 272 | r, err := Open(path) |
| 273 | if err != nil { |
| 274 | t.Fatal(err) |
| 275 | } |
| 276 | if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s"}); err != nil { |
| 277 | t.Fatal(err) |
| 278 | } |
| 279 | // Strip the column to look like a pre-hold registry. |
| 280 | if _, err := r.sql.Exec(`ALTER TABLE routes DROP COLUMN hold_op`); err != nil { |
| 281 | t.Fatal(err) |
| 282 | } |
| 283 | r.Close() |
| 284 | for i := 0; i < 2; i++ { |
| 285 | r, err = Open(path) |
| 286 | if err != nil { |
| 287 | t.Fatalf("open %d: %v", i, err) |
| 288 | } |
| 289 | rt, err := r.Get("h") |
| 290 | if err != nil || rt == nil || rt.HoldOp != "" || rt.Status != "ready" { |
| 291 | t.Fatalf("open %d: %+v %v", i, rt, err) |
| 292 | } |
| 293 | r.Close() |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | // Fix round 1: on conflict Upsert never changes hold_op, and a held row keeps |
| 298 | // its status whatever the incoming row carries. Only SetHold, ReleaseHold and |
| 299 | // DeleteIfHold change a hold. |
| 300 | func TestUpsertStaleOpCannotTakeAHoldBack(t *testing.T) { |
| 301 | r := holdFixture(t, "h") |
| 302 | if err := r.SetHold("h", "moving", holdOpA); err != nil { |
| 303 | t.Fatal(err) |
| 304 | } |
| 305 | stale, _ := r.Get("h") // a caller reads op A's row ... |
| 306 | if ok, err := r.ReleaseHold("h", holdOpA); err != nil || !ok { |
| 307 | t.Fatal(ok, err) |
| 308 | } |
| 309 | if err := r.SetHold("h", "landing", holdOpB); err != nil { // ... B takes the row ... |
| 310 | t.Fatal(err) |
| 311 | } |
| 312 | if err := r.Upsert(*stale); err != nil { // ... and the stale write arrives. |
| 313 | t.Fatal(err) |
| 314 | } |
| 315 | rt, _ := r.Get("h") |
| 316 | if rt.HoldOp != holdOpB || rt.Status != "landing" { |
| 317 | t.Fatalf("stale Upsert changed B's hold: %+v", rt) |
| 318 | } |
| 319 | if ok, err := r.ReleaseHold("h", holdOpA); err != nil || ok { |
| 320 | t.Fatalf("ReleaseHold under the stale op: %v %v", ok, err) |
| 321 | } |
| 322 | if rt, _ = r.Get("h"); rt.HoldOp != holdOpB { |
| 323 | t.Fatalf("hold lost: %+v", rt) |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | func TestUpsertSameOpWithReadyStatusKeepsHeldStatus(t *testing.T) { |
| 328 | r := holdFixture(t, "h") |
| 329 | if err := r.SetHold("h", "restoring", holdOpA); err != nil { |
| 330 | t.Fatal(err) |
| 331 | } |
| 332 | for _, status := range []string{"ready", "failed", "", "landing"} { |
| 333 | if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s", Status: status, HoldOp: holdOpA}); err != nil { |
| 334 | t.Fatal(err) |
| 335 | } |
| 336 | rt, _ := r.Get("h") |
| 337 | if rt.Status != "restoring" || rt.HoldOp != holdOpA { |
| 338 | t.Fatalf("incoming status %q: %+v", status, rt) |
| 339 | } |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | func TestUpsertCannotHoldAnUnheldRowOnConflict(t *testing.T) { |
| 344 | r := holdFixture(t, "h") |
| 345 | if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s", Status: "moving", HoldOp: holdOpA}); err != nil { |
| 346 | t.Fatal(err) |
| 347 | } |
| 348 | rt, _ := r.Get("h") |
| 349 | if rt.HoldOp != "" { |
| 350 | t.Fatalf("Upsert took a hold on conflict: %+v", rt) |
| 351 | } |
| 352 | if rows, _ := r.HoldRows(); len(rows) != 0 { |
| 353 | t.Fatalf("HoldRows = %+v", rows) |
| 354 | } |
| 355 | if err := r.SetHold("h", "moving", holdOpA); err != nil { |
| 356 | t.Fatal(err) |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | // A held row can be materialised: a fresh INSERT writes the incoming hold. |
| 361 | func TestUpsertInsertMaterialisesAHeldRow(t *testing.T) { |
| 362 | r := openTestRegistry(t) |
| 363 | if err := r.Upsert(Route{Host: "new", Kind: "instance", Socket: "/s", Status: "landing", HoldOp: holdOpA}); err != nil { |
| 364 | t.Fatal(err) |
| 365 | } |
| 366 | rt, _ := r.Get("new") |
| 367 | if rt.Status != "landing" || rt.HoldOp != holdOpA { |
| 368 | t.Fatalf("%+v", rt) |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | // The edge reloads only when Fingerprint moves: a hold handed from one op to |
| 373 | // another with the status unchanged must move it, and an unheld registry must |
| 374 | // hash exactly as it did before the column existed. |
| 375 | func TestFingerprintSeesHoldOpChangeAndIgnoresUnheld(t *testing.T) { |
| 376 | r := holdFixture(t, "h") |
| 377 | fp := func() (string, string) { |
| 378 | a, err := r.Fingerprint() |
| 379 | if err != nil { |
| 380 | t.Fatal(err) |
| 381 | } |
| 382 | b, err := r.SnapshotFingerprint() |
| 383 | if err != nil { |
| 384 | t.Fatal(err) |
| 385 | } |
| 386 | return a, b |
| 387 | } |
| 388 | if err := r.SetHold("h", "moving", holdOpA); err != nil { |
| 389 | t.Fatal(err) |
| 390 | } |
| 391 | f1, s1 := fp() |
| 392 | if ok, _ := r.ReleaseHold("h", holdOpA); !ok { |
| 393 | t.Fatal("release") |
| 394 | } |
| 395 | if err := r.SetHold("h", "moving", holdOpB); err != nil { // same status, other op |
| 396 | t.Fatal(err) |
| 397 | } |
| 398 | f2, s2 := fp() |
| 399 | if f1 == f2 { |
| 400 | t.Fatal("Fingerprint did not move when only hold_op changed") |
| 401 | } |
| 402 | if s1 == s2 { |
| 403 | t.Fatal("SnapshotFingerprint did not move when only hold_op changed") |
| 404 | } |
| 405 | // Direct column change with the status untouched. |
| 406 | if _, err := r.sql.Exec(`UPDATE routes SET hold_op = ? WHERE host = 'h'`, holdOpA); err != nil { |
| 407 | t.Fatal(err) |
| 408 | } |
| 409 | if f3, s3 := fp(); f3 == f2 || s3 == s2 { |
| 410 | t.Fatal("a bare hold_op change did not move both fingerprints") |
| 411 | } |
| 412 | |
| 413 | } |
| 414 | |
| 415 | // Switch-off contract: an unheld registry hashes byte-for-byte as it did |
| 416 | // before hold_op existed. The goldens were produced by the same two rows on |
| 417 | // the parent commit's registry.go (8437004c^). |
| 418 | func TestFingerprintsUnchangedForUnheldRows(t *testing.T) { |
| 419 | u := openTestRegistry(t) |
| 420 | if err := u.Upsert(Route{Host: "u", Kind: "instance", Socket: "/s", App: "a", CreatedAt: 1700000000}); err != nil { |
| 421 | t.Fatal(err) |
| 422 | } |
| 423 | if err := u.Upsert(Route{Host: "v", Kind: "static", Status: "starting", Hibernate: true, CreatedAt: 1700000001}); err != nil { |
| 424 | t.Fatal(err) |
| 425 | } |
| 426 | const wantF = "2:f1013971d496e9401cd6a9928a1f4e18669deda40089d2c2a8b84263b32f34c6" |
| 427 | const wantS = "2:149e8f668771c7493bdb7d4245fdabe03e957b0909c4daa1d037484b11064b80" |
| 428 | if got, _ := u.Fingerprint(); got != wantF { |
| 429 | t.Fatalf("Fingerprint = %s, want %s", got, wantF) |
| 430 | } |
| 431 | if got, _ := u.SnapshotFingerprint(); got != wantS { |
| 432 | t.Fatalf("SnapshotFingerprint = %s, want %s", got, wantS) |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | // WithHoldsFrozen is root's activation guard (moves Task 13): while fn runs, |
| 437 | // no other handle can write the registry, so a SetHold from the agent waits |
| 438 | // and the held rows fn saw are still the held rows when it returns. |
| 439 | func TestWithHoldsFrozenHoldsOffASetHold(t *testing.T) { |
| 440 | r := holdFixture(t, "h") |
| 441 | other, err := Open(r.Path()) |
| 442 | if err != nil { |
| 443 | t.Fatal(err) |
| 444 | } |
| 445 | defer other.Close() |
| 446 | done := make(chan error, 1) |
| 447 | err = r.WithHoldsFrozen(func(held []Route) error { |
| 448 | if len(held) != 0 { |
| 449 | t.Errorf("held = %+v", held) |
| 450 | } |
| 451 | go func() { done <- other.SetHold("h", "moving", holdOpA) }() |
| 452 | select { |
| 453 | case err := <-done: |
| 454 | t.Errorf("SetHold finished while the holds were frozen: %v", err) |
| 455 | done <- err // for the read below |
| 456 | case <-time.After(300 * time.Millisecond): |
| 457 | } |
| 458 | return nil |
| 459 | }) |
| 460 | if err != nil { |
| 461 | t.Fatal(err) |
| 462 | } |
| 463 | if err := <-done; err != nil { |
| 464 | t.Fatalf("SetHold after the freeze: %v", err) |
| 465 | } |
| 466 | var seen []Route |
| 467 | if err := r.WithHoldsFrozen(func(held []Route) error { seen = held; return nil }); err != nil { |
| 468 | t.Fatal(err) |
| 469 | } |
| 470 | if len(seen) != 1 || seen[0].Host != "h" || seen[0].HoldOp != holdOpA { |
| 471 | t.Fatalf("held after SetHold = %+v", seen) |
| 472 | } |
| 473 | // fn's error is the call's, and nothing fn did not do is written. |
| 474 | sentinel := errors.New("refused") |
| 475 | if err := r.WithHoldsFrozen(func([]Route) error { return sentinel }); !errors.Is(err, sentinel) { |
| 476 | t.Fatalf("err = %v", err) |
| 477 | } |
| 478 | } |
| 479 | |
| 480 | // Moves Task 6 fix round 1: SetHoldIf writes a hold only while the row's |
| 481 | // hold_op is what the caller expects, in one statement, so a hold decided |
| 482 | // from an earlier read cannot land on a row another op has taken since. |
| 483 | // expectOp is "" (the row was unheld) or op (the op changes its own hold's |
| 484 | // status); taking over another op's hold is never one statement. |
| 485 | func TestSetHoldIfIsConditionalOnTheCurrentOp(t *testing.T) { |
| 486 | r := holdFixture(t, "h") |
| 487 | if ok, err := r.SetHoldIf("h", "moving", holdOpA, ""); err != nil || !ok { |
| 488 | t.Fatalf("unheld row, expecting unheld: %v %v", ok, err) |
| 489 | } |
| 490 | if rt, _ := r.Get("h"); rt.Status != "moving" || rt.HoldOp != holdOpA { |
| 491 | t.Fatalf("after the first hold: %+v", rt) |
| 492 | } |
| 493 | // Held by A now; a writer that still thinks the row is unheld is refused |
| 494 | // and changes nothing. |
| 495 | if ok, err := r.SetHoldIf("h", "landing", holdOpB, ""); err != nil || ok { |
| 496 | t.Fatalf("stale write over A's hold: %v %v", ok, err) |
| 497 | } |
| 498 | if rt, _ := r.Get("h"); rt.Status != "moving" || rt.HoldOp != holdOpA { |
| 499 | t.Fatalf("the refused write changed the row: %+v", rt) |
| 500 | } |
| 501 | // A may change its own hold's status. |
| 502 | if ok, err := r.SetHoldIf("h", "landing", holdOpA, holdOpA); err != nil || !ok { |
| 503 | t.Fatalf("A changing its own hold: %v %v", ok, err) |
| 504 | } |
| 505 | if rt, _ := r.Get("h"); rt.Status != "landing" || rt.HoldOp != holdOpA { |
| 506 | t.Fatalf("after A's status change: %+v", rt) |
| 507 | } |
| 508 | // B expecting A's hold: not a takeover this statement allows. |
| 509 | if _, err := r.SetHoldIf("h", "moving", holdOpB, holdOpA); err == nil { |
| 510 | t.Fatal("SetHoldIf accepted expectOp of another op") |
| 511 | } |
| 512 | if rt, _ := r.Get("h"); rt.HoldOp != holdOpA { |
| 513 | t.Fatalf("the refused takeover changed the row: %+v", rt) |
| 514 | } |
| 515 | // SetHold's own refusals hold. |
| 516 | if _, err := r.SetHoldIf("h", "ready", holdOpA, holdOpA); err == nil { |
| 517 | t.Fatal("ready accepted as a hold") |
| 518 | } |
| 519 | if _, err := r.SetHoldIf("h", "moving", "", ""); err == nil { |
| 520 | t.Fatal("a hold with no op was accepted") |
| 521 | } |
| 522 | // No such row: nothing matched, not an error (the caller re-judges |
| 523 | // next pass either way). |
| 524 | if ok, err := r.SetHoldIf("nope", "moving", holdOpA, ""); err != nil || ok { |
| 525 | t.Fatalf("unknown host: %v %v", ok, err) |
| 526 | } |
| 527 | // After a release the row is unheld again and "" matches once more. |
| 528 | if ok, _ := r.ReleaseHold("h", holdOpA); !ok { |
| 529 | t.Fatal("release") |
| 530 | } |
| 531 | if ok, err := r.SetHoldIf("h", "restoring", holdOpB, ""); err != nil || !ok { |
| 532 | t.Fatalf("hold after release: %v %v", ok, err) |
| 533 | } |
| 534 | } |
| 535 | |
| 536 | // Ruling J3: a rollback takes over the move's hold in one statement, so the |
| 537 | // row is never unheld between the two ops. |
| 538 | func TestHandHold(t *testing.T) { |
| 539 | const rb = "rb_abcdefghijklmnopqrstuvwxyz" |
| 540 | r := holdFixture(t, "h") |
| 541 | if err := r.SetHold("h", "landing", holdOpA); err != nil { |
| 542 | t.Fatal(err) |
| 543 | } |
| 544 | // Held by another op: nothing moves. |
| 545 | if ok, err := r.HandHold("h", "releasing", rb, holdOpB); err != nil || ok { |
| 546 | t.Fatalf("hand-over from an op that does not hold the row: %v %v", ok, err) |
| 547 | } |
| 548 | if rt, _ := r.Get("h"); rt.Status != "landing" || rt.HoldOp != holdOpA { |
| 549 | t.Fatalf("row changed by the refusal: %+v", rt) |
| 550 | } |
| 551 | // The hand-over: one write, the new op and status. |
| 552 | if ok, err := r.HandHold("h", "releasing", rb, holdOpA); err != nil || !ok { |
| 553 | t.Fatalf("hand-over: %v %v", ok, err) |
| 554 | } |
| 555 | if rt, _ := r.Get("h"); rt.Status != "releasing" || rt.HoldOp != rb { |
| 556 | t.Fatalf("after the hand-over: %+v", rt) |
| 557 | } |
| 558 | // A replay finds the row already handed over and matches nothing. |
| 559 | if ok, err := r.HandHold("h", "releasing", rb, holdOpA); err != nil || ok { |
| 560 | t.Fatalf("replay: %v %v", ok, err) |
| 561 | } |
| 562 | // An unheld row is not taken: that is SetHoldIf's, with expectOp "". |
| 563 | if err := r.Upsert(Route{Host: "plain", Kind: "instance", Socket: "/s"}); err != nil { |
| 564 | t.Fatal(err) |
| 565 | } |
| 566 | if ok, err := r.HandHold("plain", "restoring", rb, holdOpA); err != nil || ok { |
| 567 | t.Fatalf("unheld row: %v %v", ok, err) |
| 568 | } |
| 569 | if rt, _ := r.Get("plain"); rt.HoldOp != "" || IsHold(rt.Status) { |
| 570 | t.Fatalf("unheld row taken: %+v", rt) |
| 571 | } |
| 572 | // An absent row matches nothing. |
| 573 | if ok, err := r.HandHold("absent", "restoring", rb, holdOpA); err != nil || ok { |
| 574 | t.Fatalf("absent row: %v %v", ok, err) |
| 575 | } |
| 576 | // Malformed: not a hold status, an empty op on either side, the same op. |
| 577 | for name, c := range map[string][3]string{ |
| 578 | "ready": {"ready", rb, rb}, |
| 579 | "no to": {"restoring", "", holdOpA}, |
| 580 | "no from": {"restoring", rb, ""}, |
| 581 | "same op": {"restoring", rb, rb}, |
| 582 | } { |
| 583 | if ok, err := r.HandHold("h", c[0], c[1], c[2]); err == nil || ok { |
| 584 | t.Fatalf("%s: %v %v", name, ok, err) |
| 585 | } |
| 586 | } |
| 587 | if rt, _ := r.Get("h"); rt.Status != "releasing" || rt.HoldOp != rb { |
| 588 | t.Fatalf("row changed by a malformed hand-over: %+v", rt) |
| 589 | } |
| 590 | } |
| 591 | |