carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "strconv"
8 "strings"
9 "sync/atomic"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/registry"
14 "github.com/carlosframework/platform/internal/schedule"
15)
16
17func TestSleepingInstanceRearmsItsNextDeliveryBeforeReturning(t *testing.T) {
18 t.Parallel()
19 const host = "rearm.bab.oncarlos.com"
20 const idle = 200 * time.Millisecond
21 entered := make(chan struct{}, 1)
22 release := make(chan struct{})
23 events := make(chan string, 4)
24 var calls atomic.Int32
25 var reg *registry.Registry
26 now := time.Now().UTC()
27 var clock atomic.Int64
28 clock.Store(now.UnixNano())
29 next := now.Add(time.Hour).Unix()
30 var firstError atomic.Pointer[error]
31 var instReg *registry.Registry
32 instReg, inst, svc, dd := wakeFixture(t, host, idle, func(w http.ResponseWriter, req *http.Request) {
33 if req.Method != http.MethodPost || req.URL.Path != "/jobs/reminders" || !strings.HasPrefix(req.Header.Get("Authorization"), "Bearer ") {
34 w.WriteHeader(http.StatusForbidden)
35 return
36 }
37 if calls.Add(1) == 1 {
38 if err := reg.UpsertTimer(registry.Timer{Host: host, Name: "next-due", Kind: schedule.KindOnce,
39 Spec: "at " + strconv.FormatInt(next, 10), Path: "/jobs/reminders", NextAt: next, DueAt: next}); err != nil {
40 firstError.Store(&err)
41 }
42 entered <- struct{}{}
43 <-release
44 }
45 w.WriteHeader(http.StatusNoContent)
46 })
47 reg = instReg
48 t.Cleanup(func() {
49 select {
50 case <-release:
51 default:
52 close(release)
53 }
54 })
55 runner := &schedule.Runner{Reg: reg, Deliver: &activatorDeliverer{svc: svc}, Token: scheduleToken(dd, svc, reg),
56 Now: func() time.Time { return time.Unix(0, clock.Load()) }, Timeout: 10 * time.Second,
57 Event: func(_ registry.Route, _, msg string) { events <- msg },
58 }
59 due := now.Unix() - 1
60 if err := reg.UpsertTimer(registry.Timer{Host: host, Name: "next-due", Kind: schedule.KindOnce,
61 Spec: fmt.Sprintf("at %d", due), Path: "/jobs/reminders", NextAt: due, DueAt: due}); err != nil {
62 t.Fatal(err)
63 }
64 runner.Tick(context.Background())
65 select {
66 case <-entered:
67 case <-time.After(10 * time.Second):
68 t.Fatal("sleeping instance never received its due delivery")
69 }
70 if err := firstError.Load(); err != nil {
71 t.Fatal(*err)
72 }
73 time.Sleep(3 * idle)
74 if !inst.running() || !svc.Owns(host) {
75 t.Fatal("instance slept while its scheduled handler was still running")
76 }
77 close(release)
78 select {
79 case <-events:
80 case <-time.After(10 * time.Second):
81 t.Fatal("first delivery did not finish")
82 }
83 timer, err := reg.GetTimer(host, "next-due")
84 if err != nil {
85 t.Fatal(err)
86 }
87 if timer.NextAt != next {
88 t.Fatalf("completion erased the replacement delivery: next_at=%d, want %d", timer.NextAt, next)
89 }
90 deadline := time.Now().Add(10 * time.Second)
91 for inst.running() || svc.Owns(host) {
92 if time.Now().After(deadline) {
93 t.Fatal("instance did not hibernate between deliveries")
94 }
95 time.Sleep(5 * time.Millisecond)
96 }
97 clock.Store(time.Unix(next, 0).UnixNano())
98 for calls.Load() < 2 {
99 runner.Tick(context.Background())
100 if time.Now().After(deadline) {
101 t.Fatal("replacement delivery never woke the instance")
102 }
103 time.Sleep(5 * time.Millisecond)
104 }
105 select {
106 case <-events:
107 case <-time.After(10 * time.Second):
108 t.Fatal("second delivery did not finish")
109 }
110 timer, err = reg.GetTimer(host, "next-due")
111 if err != nil {
112 t.Fatal(err)
113 }
114 if timer.NextAt != 0 || timer.LastStatus != "204" {
115 t.Fatalf("second delivery not completed: %+v", timer)
116 }
117 if inst.startCount() != 2 {
118 t.Fatalf("got %d wakes, want 2", inst.startCount())
119 }
120}
121