package host import ( "context" "fmt" "net/http" "strconv" "strings" "sync/atomic" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/schedule" ) func TestSleepingInstanceRearmsItsNextDeliveryBeforeReturning(t *testing.T) { t.Parallel() const host = "rearm.bab.oncarlos.com" const idle = 200 * time.Millisecond entered := make(chan struct{}, 1) release := make(chan struct{}) events := make(chan string, 4) var calls atomic.Int32 var reg *registry.Registry now := time.Now().UTC() var clock atomic.Int64 clock.Store(now.UnixNano()) next := now.Add(time.Hour).Unix() var firstError atomic.Pointer[error] var instReg *registry.Registry instReg, inst, svc, dd := wakeFixture(t, host, idle, func(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost || req.URL.Path != "/jobs/reminders" || !strings.HasPrefix(req.Header.Get("Authorization"), "Bearer ") { w.WriteHeader(http.StatusForbidden) return } if calls.Add(1) == 1 { if err := reg.UpsertTimer(registry.Timer{Host: host, Name: "next-due", Kind: schedule.KindOnce, Spec: "at " + strconv.FormatInt(next, 10), Path: "/jobs/reminders", NextAt: next, DueAt: next}); err != nil { firstError.Store(&err) } entered <- struct{}{} <-release } w.WriteHeader(http.StatusNoContent) }) reg = instReg t.Cleanup(func() { select { case <-release: default: close(release) } }) runner := &schedule.Runner{Reg: reg, Deliver: &activatorDeliverer{svc: svc}, Token: scheduleToken(dd, svc, reg), Now: func() time.Time { return time.Unix(0, clock.Load()) }, Timeout: 10 * time.Second, Event: func(_ registry.Route, _, msg string) { events <- msg }, } due := now.Unix() - 1 if err := reg.UpsertTimer(registry.Timer{Host: host, Name: "next-due", Kind: schedule.KindOnce, Spec: fmt.Sprintf("at %d", due), Path: "/jobs/reminders", NextAt: due, DueAt: due}); err != nil { t.Fatal(err) } runner.Tick(context.Background()) select { case <-entered: case <-time.After(10 * time.Second): t.Fatal("sleeping instance never received its due delivery") } if err := firstError.Load(); err != nil { t.Fatal(*err) } time.Sleep(3 * idle) if !inst.running() || !svc.Owns(host) { t.Fatal("instance slept while its scheduled handler was still running") } close(release) select { case <-events: case <-time.After(10 * time.Second): t.Fatal("first delivery did not finish") } timer, err := reg.GetTimer(host, "next-due") if err != nil { t.Fatal(err) } if timer.NextAt != next { t.Fatalf("completion erased the replacement delivery: next_at=%d, want %d", timer.NextAt, next) } deadline := time.Now().Add(10 * time.Second) for inst.running() || svc.Owns(host) { if time.Now().After(deadline) { t.Fatal("instance did not hibernate between deliveries") } time.Sleep(5 * time.Millisecond) } clock.Store(time.Unix(next, 0).UnixNano()) for calls.Load() < 2 { runner.Tick(context.Background()) if time.Now().After(deadline) { t.Fatal("replacement delivery never woke the instance") } time.Sleep(5 * time.Millisecond) } select { case <-events: case <-time.After(10 * time.Second): t.Fatal("second delivery did not finish") } timer, err = reg.GetTimer(host, "next-due") if err != nil { t.Fatal(err) } if timer.NextAt != 0 || timer.LastStatus != "204" { t.Fatalf("second delivery not completed: %+v", timer) } if inst.startCount() != 2 { t.Fatalf("got %d wakes, want 2", inst.startCount()) } }