package registry import ( "context" "path/filepath" "testing" ) // Region HA ruling 31: "relocating" is a hold status. The edge never // serves it and ticks refuse it (IsHold); SetHold accepts it; the hold is // released only under its own op. func TestRelocatingIsAHoldStatus(t *testing.T) { if !IsHold("relocating") { t.Fatal("relocating is not a hold") } reg, err := Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer reg.Close() if err := reg.Upsert(Route{Host: "shop.example", Status: "ready", Socket: "/run/shop.sock"}); err != nil { t.Fatal(err) } if err := reg.SetHold("shop.example", "relocating", "op-1"); err != nil { t.Fatal(err) } if changed, err := reg.ReleaseHold("shop.example", "op-2"); err != nil || changed { t.Fatalf("another op released the hold: %v %v", changed, err) } row, err := reg.Get("shop.example") if err != nil || row.Status != "relocating" || row.HoldOp != "op-1" { t.Fatalf("row %+v %v", row, err) } if changed, err := reg.ReleaseHold("shop.example", "op-1"); err != nil || !changed { t.Fatalf("its own op did not release the hold: %v %v", changed, err) } if row, _ := reg.Get("shop.example"); row.Status != "ready" || row.HoldOp != "" { t.Fatalf("after release: %+v", row) } } // Region HA Task 10b (R5-3): the source's row is held with SetHoldIf // expecting it unheld. That write never takes a row another op holds, a // replay of it matches nothing and changes nothing, it mints no row, and // it leaves the row's claim ownership as it was, so the source's // park-local retires the held row by ownership (RetireClaimRows), which // ends the hold. func TestSetHoldIfRelocatingNeverTakesAnotherOpsRow(t *testing.T) { ctx := context.Background() reg, err := Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer reg.Close() owner := ClaimOwnership{ClaimIncarnation: "inc-1", ClaimStamp: 7, ClaimOwner: "owner-1", ClaimRowID: "row-1"} if err := reg.Upsert(Route{Host: "shop.example", Status: "ready", Socket: "/run/shop.sock", ClaimOwnership: owner}); err != nil { t.Fatal(err) } if held, err := reg.SetHoldIf("shop.example", "relocating", "rel-1", ""); err != nil || !held { t.Fatalf("the hold: %v %v", held, err) } held, err := reg.Get("shop.example") if err != nil || held.Status != "relocating" || held.HoldOp != "rel-1" || held.ClaimOwnership != owner { t.Fatalf("held row %+v %v", held, err) } // A replay matches nothing and changes nothing. if again, err := reg.SetHoldIf("shop.example", "relocating", "rel-1", ""); err != nil || again { t.Fatalf("a replay wrote: %v %v", again, err) } // Another op neither takes the hold expecting an unheld row nor by // naming the holder. if took, err := reg.SetHoldIf("shop.example", "relocating", "rel-2", ""); err != nil || took { t.Fatalf("another op took the row: %v %v", took, err) } if took, err := reg.SetHoldIf("shop.example", "relocating", "rel-2", "rel-1"); err == nil || took { t.Fatalf("a takeover was accepted: %v %v", took, err) } if now, err := reg.Get("shop.example"); err != nil || *now != *held { t.Fatalf("the row changed: %+v (was %+v) %v", now, held, err) } // No row: nothing matches and nothing is minted. if minted, err := reg.SetHoldIf("absent.example", "relocating", "rel-1", ""); err != nil || minted { t.Fatalf("a missing row: %v %v", minted, err) } if row, err := reg.Get("absent.example"); err != nil || row != nil { t.Fatalf("a row was minted: %+v %v", row, err) } // The park-local's retirement deletes the held row by its ownership. target := []ClaimRow{{Kind: "route", Host: "shop.example", Ownership: owner}} if err := reg.RetireClaimRows(ctx, "retire-1", target, func([]ClaimRowArchive) error { return nil }); err != nil { t.Fatalf("the park-local's retirement of the held row: %v", err) } if row, err := reg.Get("shop.example"); err != nil || row != nil { t.Fatalf("after the retirement: %+v %v", row, err) } }