package registry import ( "errors" "path/filepath" "testing" ) func TestPlan12RebindClaimRows(t *testing.T) { for _, mode := range []string{"ok", "changed-route", "changed-owner", "changed-payload", "extra-timer"} { t.Run(mode, func(t *testing.T) { r, e := Open(filepath.Join(t.TempDir(), "registry.db")) if e != nil { t.Fatal(e) } defer r.Close() row := Route{Host: "app.example", Kind: "instance", ClaimOwnership: ClaimOwnership{"inc", 1, "old", "old-row"}} if e = r.Upsert(row); e != nil { t.Fatal(e) } saved, e := r.Get(row.Host) if e != nil { t.Fatal(e) } old := ColdRows{Route: *saved} next := old next.Route.ClaimOwnership = ClaimOwnership{"inc", 2, "new", "new-row"} switch mode { case "changed-route": r.sql.Exec("UPDATE routes SET version='other' WHERE host=?", row.Host) case "changed-owner": r.sql.Exec("UPDATE routes SET claim_owner='other' WHERE host=?", row.Host) case "changed-payload": next.Route.Version = "other" case "extra-timer": if e = r.UpsertTimer(Timer{Host: row.Host, Name: "extra", Kind: "cron", Spec: "* * * * *"}); e != nil { t.Fatal(e) } } e = r.RebindClaimRows(t.Context(), old, next) if mode != "ok" { if !errors.Is(e, ErrClaimOwnership) { t.Fatal("accepted changed installation", e) } return } if e != nil { t.Fatal(e) } if e = r.RebindClaimRows(t.Context(), old, next); e != nil { t.Fatal("replay", e) } got, e := r.Get(row.Host) if e != nil || *got != next.Route { t.Fatal(got, e) } }) } } func TestPlan12RebindRefusesForeignTimerOwnership(t *testing.T) { for _, mode := range []string{"foreign-owner", "stale-incarnation", "stale-stamp"} { t.Run(mode, func(t *testing.T) { r, err := Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer r.Close() row := Route{Host: "app.example", Kind: "instance", ClaimOwnership: ClaimOwnership{"inc", 1, "old", "row"}} if err = r.Upsert(row); err != nil { t.Fatal(err) } saved, err := r.Get(row.Host) if err != nil { t.Fatal(err) } timer := Timer{Host: row.Host, Name: "extra", Kind: "cron", Spec: "* * * * *", ClaimOwnership: ClaimOwnership{"inc", 1, "old", "timer"}} switch mode { case "foreign-owner": timer.ClaimOwner = "foreign" case "stale-incarnation": timer.ClaimIncarnation = "stale" case "stale-stamp": timer.ClaimStamp = 9 } if err = r.UpsertTimer(timer); err != nil { t.Fatal(err) } if _, err = r.sql.Exec("UPDATE timers SET claim_incarnation=?,claim_stamp=?,claim_owner=?,claim_row_id=? WHERE host=?", timer.ClaimIncarnation, timer.ClaimStamp, timer.ClaimOwner, timer.ClaimRowID, row.Host); err != nil { t.Fatal(err) } timers, err := r.TimersFor(row.Host) if err != nil { t.Fatal(err) } old := ColdRows{Route: *saved, Timers: timers} next := ColdRows{Route: *saved, Timers: append([]Timer(nil), timers...)} next.Route.ClaimOwnership = ClaimOwnership{"inc", 2, "new", "new-row"} next.Timers[0].ClaimOwnership = ClaimOwnership{"inc", 2, "new", "new-timer"} if err = r.RebindClaimRows(t.Context(), old, next); !errors.Is(err, ErrClaimOwnership) { t.Fatalf("stole timer: %v", err) } got, err := r.Get(row.Host) if err != nil || *got != *saved { t.Fatal("refusal changed route", err) } }) } }