package registry import ( "errors" "fmt" "path/filepath" "testing" "time" ) func TestClaimReviewOpenRetainsNormalDurability(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "") r, err := Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer r.Close() var mode int if err := r.sql.QueryRow("PRAGMA synchronous").Scan(&mode); err != nil { t.Fatal(err) } if mode != 1 { t.Fatalf("flag-off synchronous=%d, want NORMAL (1)", mode) } } func TestClaimReviewFlagOffTimersKeepLegacyStatements(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "") r, err := Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer r.Close() if _, err := r.sql.Exec("DROP TABLE routes"); err != nil { t.Fatal(err) } timer := Timer{Host: "app.test", Name: "tick", Kind: "schedule", Spec: "every 1h0m0s", Path: "/tick", NextAt: 12, DueAt: 12} if err := r.UpsertTimer(timer); err != nil { t.Errorf("legacy upsert unexpectedly reads routes: %v", err) } timer.Registration = "imported" if err := r.ReplaceTimers(timer.Host, []Timer{timer}); err != nil { t.Fatalf("legacy replace unexpectedly reads routes: %v", err) } got, err := r.GetTimer(timer.Host, timer.Name) if err != nil || *got != timer { t.Fatalf("legacy import=%+v err=%v", got, err) } } func TestClaimReviewTimerWritersWaitForOtherConnection(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "1") path := filepath.Join(t.TempDir(), "registry.db") r, err := Open(path) if err != nil { t.Fatal(err) } defer r.Close() other, err := Open(path) if err != nil { t.Fatal(err) } defer other.Close() timer := Timer{Host: "app.test", Name: "tick", Kind: "schedule", Path: "/tick", Registration: "imported"} for _, replace := range []bool{false, true} { for i := 0; i < 100; i++ { tx, err := other.sql.Begin() if err != nil { t.Fatal(err) } if _, err := tx.Exec("UPDATE routes SET status=status"); err != nil { t.Fatal(err) } done := make(chan error, 1) go func() { if replace { done <- r.ReplaceTimers(timer.Host, []Timer{timer}) } else { done <- r.UpsertTimer(timer) } }() time.Sleep(time.Millisecond) if err := tx.Commit(); err != nil { t.Fatal(err) } if err := <-done; err != nil { t.Fatalf("replace=%v writer failed instead of waiting: %v", replace, err) } } } } func TestClaimReviewOwnershipMigrationDoesNotOwnHoldColumn(t *testing.T) { r, err := Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer r.Close() if _, err := r.sql.Exec("ALTER TABLE routes DROP COLUMN hold_op"); err != nil { t.Fatal(err) } if err := r.migrateClaimOwnership(); err != nil { t.Fatal(err) } if _, err := r.sql.Exec("SELECT hold_op FROM routes"); err == nil { t.Fatal("claim ownership migration redundantly added main's hold_op") } } func TestClaimReviewDurabilityScopedToClaimWrites(t *testing.T) { r, err := Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer r.Close() rt := Route{Host: "app.test"} if err := r.Upsert(rt); err != nil { t.Fatal(err) } timer := Timer{Host: rt.Host, Name: "tick", Kind: "schedule"} if err := r.UpsertTimer(timer); err != nil { t.Fatal(err) } observed, err := r.Get(rt.Host) if err != nil { t.Fatal(err) } observedTimer, err := r.GetTimer(rt.Host, timer.Name) if err != nil { t.Fatal(err) } for _, table := range []string{"routes", "timers", "claim_row_retirements"} { for _, op := range []string{"UPDATE", "DELETE", "INSERT"} { q := fmt.Sprintf(`CREATE TRIGGER require_full_%s_%s BEFORE %s ON %s BEGIN SELECT CASE WHEN (SELECT synchronous FROM pragma_synchronous) != 2 THEN RAISE(ABORT,'claim write without FULL durability') END; END`, table, op, op, table) if _, err := r.sql.Exec(q); err != nil { t.Fatal(err) } } } check := func(err error) { t.Helper() if err != nil { t.Fatal(err) } var mode int if err := r.sql.QueryRow("PRAGMA synchronous").Scan(&mode); err != nil || mode != 1 { t.Fatalf("normal durability not restored: %d %v", mode, err) } } owner := ClaimOwnership{"inc", 1, "owner", "route"} check(r.AdoptClaimRoute(t.Context(), *observed, owner)) timerOwner := owner timerOwner.ClaimRowID = observedTimer.Registration check(r.AdoptClaimTimer(t.Context(), *observedTimer, timerOwner)) check(r.SetClaimHold(t.Context(), rt.Host, owner, "", "hold")) if err := r.SetClaimHold(t.Context(), rt.Host, owner, "wrong", ""); err != ErrClaimOwnership { t.Fatal(err) } check(nil) targets := []ClaimRow{{Kind: "route", Host: rt.Host, Ownership: owner}, {Kind: "timer", Host: rt.Host, Name: timer.Name, Registration: observedTimer.Registration, Ownership: timerOwner}} failed := errors.New("archive failed") if err := r.RetireClaimRows(t.Context(), "retire", targets, func([]ClaimRowArchive) error { return failed }); !errors.Is(err, failed) { t.Fatal(err) } check(nil) check(r.RetireClaimRows(t.Context(), "retire", targets, func([]ClaimRowArchive) error { return nil })) }