package schedule import ( "context" "fmt" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" "testing" "time" ) type protectedTimerWork struct { t *testing.T reg *registry.Registry held bool calls int } func (w *protectedTimerWork) With(_ context.Context, _ hostwrite.Identity, f func() error) error { if w.held { w.t.Fatal("recursive host exclusion") } w.held = true defer func() { w.held = false }() return f() } func (w *protectedTimerWork) Select(context.Context, string, string) (hostwrite.Work, error) { return w, nil } func (w *protectedTimerWork) MutateTimer(_ context.Context, row registry.Timer, expected string, remove bool) (*registry.Timer, error) { if w.held { w.t.Fatal("root timer RPC inside host exclusion") } w.calls++ if remove { return nil, w.reg.DeleteTimerOwnedRegistration(row) } rt, err := w.reg.Get(row.Host) if err != nil { return nil, err } row.ClaimOwnership = rt.ClaimOwnership row.Registration = fmt.Sprint("root-", w.calls) row.ClaimRowID = row.Registration return w.reg.MaterializeTimerRegistration(row, expected) } func TestPlan14TimerProducerTransactions(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "1") reg, _ := convergeFixture(t) host := "a.bab.oncarlos.com" rt, err := reg.Get(host) if err != nil { t.Fatal(err) } owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "route"} if err := reg.AdoptClaimRoute(t.Context(), *rt, owner); err != nil { t.Fatal(err) } rt.ClaimOwnership = owner w := &protectedTimerWork{t: t, reg: reg} writes := hostwrite.Config{Enabled: true, Selector: w} s := ClaimTimers{TimerStore: reg, Writes: writes, Context: t.Context()} row := registry.Timer{Host: host, Name: "once", Kind: KindOnce, Spec: "at 2000", Path: "/once", NextAt: 2000} if err := s.UpsertTimer(row); err != nil { t.Fatal(err) } first, _ := reg.GetTimer(host, row.Name) if err := s.UpsertTimer(row); err != nil { t.Fatal(err) } second, _ := reg.GetTimer(host, row.Name) if first.Registration == second.Registration { t.Fatal("once timer was not re-registered") } if err := s.DeleteTimer(host, row.Name); err != nil { t.Fatal(err) } ctx := context.WithValue(t.Context(), timerWorkKey{}, timerWork{ctx: t.Context(), work: w}) c := NewConverger(reg, nil, "box", nil) c.HostWrites = writes d := Declaration{Schedules: []Schedule{{Name: "tick", Every: "1h", Path: "/tick"}}} if _, err := c.convergeHost(ctx, *rt, d, "", time.Unix(1000, 0)); err != nil { t.Fatal(err) } original, _ := reg.GetTimer(host, "tick") if _, err := c.convergeHost(ctx, *rt, d, "tick", time.Unix(1100, 0)); err != nil { t.Fatal(err) } fired, _ := reg.GetTimer(host, "tick") if original.Registration == fired.Registration || fired.NextAt != 1100 { t.Fatal("run-now skipped root mutation", fired) } if _, err := c.convergeHost(ctx, *rt, Declaration{}, "", time.Unix(1200, 0)); err != nil { t.Fatal(err) } if w.calls != 6 { t.Fatalf("producers used %d root mutations, want 6", w.calls) } selected := SelectedTimers{Store: reg, Work: w, Context: t.Context(), Host: host} if err := selected.ReplaceTimers(host, []registry.Timer{row}); err == nil || w.calls != 6 { t.Fatal("import minted missing registration", err, w.calls) } row.Registration = "snapshot" if err := selected.ReplaceTimers(host, []registry.Timer{row, row}); err == nil || w.calls != 6 { t.Fatal("duplicate import mutated rows", err, w.calls) } if err := selected.ReplaceTimers(host, []registry.Timer{row}); err != nil { t.Fatal(err) } if err := selected.ReplaceTimers(host, nil); err != nil { t.Fatal(err) } if w.calls != 8 { t.Fatal("landing bypassed root mutations", w.calls) } if err := s.UpsertTimer(row); err != nil { t.Fatal(err) } old, _ := reg.GetTimer(host, row.Name) if err := deleteSelectedTimer(ctx, reg, *old); err != nil { t.Fatal(err) } if w.calls != 10 { t.Fatal("runner prune bypassed root mutation", w.calls) } }