carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "errors"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/registry"
12)
13
14func TestStartupWaitUsesWakeTimeoutResponse(t *testing.T) {
15 old := wakeWaitBound
16 wakeWaitBound = 20 * time.Millisecond
17 t.Cleanup(func() { wakeWaitBound = old })
18 a := New(Config{}, &fakeBackend{}, nil)
19 a.Startup = make(chan struct{})
20 defer a.lifeStop()
21 rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true}
22 w := httptest.NewRecorder()
23 a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://shop.example.com/", nil), rt, http.NotFoundHandler())
24 if w.Code != http.StatusServiceUnavailable || w.Header().Get("Retry-After") != "5" || w.Body.String() != "instance is waking; retry shortly\n" {
25 t.Fatalf("startup timeout: %d, %v, %q", w.Code, w.Header(), w.Body.String())
26 }
27 if a.Owns(rt.Host) {
28 t.Fatal("timed-out request created a wake")
29 }
30}
31
32func TestStartupBlocksWorkerAndSidecarAdmission(t *testing.T) {
33 old := wakeWaitBound
34 wakeWaitBound = 20 * time.Millisecond
35 t.Cleanup(func() { wakeWaitBound = old })
36 a := New(Config{}, &fakeBackend{}, nil)
37 a.Startup = make(chan struct{})
38 defer a.lifeStop()
39 rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true}
40 if err := a.WakeWorker(rt); !errors.Is(err, errWakeTimeout) {
41 t.Fatalf("worker: %v", err)
42 }
43 if err := a.BeginSidecarRequest(t.Context(), rt); !errors.Is(err, errWakeTimeout) {
44 t.Fatalf("sidecar: %v", err)
45 }
46 ctx, cancel := context.WithCancel(t.Context())
47 cancel()
48 if _, err := a.begin(ctx, rt, true); !errors.Is(err, context.Canceled) {
49 t.Fatalf("cancelled request: %v", err)
50 }
51 if a.Owns(rt.Host) {
52 t.Fatal("startup admission created a wake")
53 }
54}
55
56func TestStartupShutdownAnswersServiceUnavailable(t *testing.T) {
57 for _, ready := range []bool{false, true} {
58 t.Run(map[bool]string{false: "held", true: "released"}[ready], func(t *testing.T) {
59 a := New(Config{}, &fakeBackend{}, nil)
60 started := make(chan struct{})
61 a.Startup = started
62 if ready {
63 close(started)
64 }
65 a.Shutdown(t.Context())
66 rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true}
67 w := httptest.NewRecorder()
68 a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://shop.example.com/", nil), rt, http.NotFoundHandler())
69 if w.Code != http.StatusServiceUnavailable {
70 t.Fatalf("service shutdown answered %d: %q", w.Code, w.Body.String())
71 }
72 if a.Owns(rt.Host) {
73 t.Fatal("shutdown request created a wake")
74 }
75 })
76 }
77}
78
79type startupRoutes map[string]registry.Route
80
81func (s startupRoutes) Get(host string) (*registry.Route, error) {
82 rt, ok := s[host]
83 if !ok {
84 return nil, nil
85 }
86 return &rt, nil
87}
88
89// A request held through startup carries the proxy the edge chose before
90// recovery ran. When recovery moved the host's socket, it is answered as a
91// wake in progress, so the retry is routed afresh, never sent to the old
92// socket.
93func TestStartupHeldRequestRefusesStaleUpstream(t *testing.T) {
94 t.Parallel()
95 for _, moved := range []bool{false, true} {
96 a := New(Config{}, &fakeBackend{}, nil)
97 ready := make(chan struct{})
98 a.Startup = ready
99 rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true, Socket: "/run/carlos/shop.example.com.sock"}
100 fresh := rt
101 if moved {
102 fresh.Socket = "/run/carlos/instances/shop.example.com/app.sock"
103 }
104 a.Routes = startupRoutes{rt.Host: fresh}
105 proxied := false
106 proxy := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proxied = true })
107 w := httptest.NewRecorder()
108 done := make(chan struct{})
109 go func() {
110 defer close(done)
111 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
112 defer cancel()
113 a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://shop.example.com/", nil).WithContext(ctx), rt, proxy)
114 }()
115 time.Sleep(20 * time.Millisecond)
116 close(ready)
117 <-done
118 a.lifeStop()
119 if moved {
120 if proxied || w.Code != http.StatusServiceUnavailable || w.Header().Get("Retry-After") != "5" {
121 t.Fatalf("moved socket: proxied=%v code=%d", proxied, w.Code)
122 }
123 } else if w.Code == http.StatusServiceUnavailable && w.Body.String() == "instance is waking; retry shortly\n" {
124 t.Fatalf("unchanged socket was refused as stale")
125 }
126 }
127}
128
129// The race the waiting check misses: the edge chose its proxy, recovery
130// moved the socket and Startup closed before serve looked. A request near
131// startup is still checked; one long after it is not.
132func TestStartupClosedNearStartupRefusesStaleUpstream(t *testing.T) {
133 for _, late := range []bool{false, true} {
134 a := New(Config{}, &fakeBackend{}, nil)
135 ready := make(chan struct{})
136 close(ready)
137 a.Startup = ready
138 if late {
139 a.startupSeenAt.Store(time.Now().Add(-2 * startupFreshWindow).UnixNano())
140 }
141 rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true, Socket: "/run/carlos/shop.example.com.sock"}
142 fresh := rt
143 fresh.Socket = "/run/carlos/instances/shop.example.com/app.sock"
144 a.Routes = startupRoutes{rt.Host: fresh}
145 proxied := false
146 proxy := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proxied = true })
147 w := httptest.NewRecorder()
148 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
149 a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://shop.example.com/", nil).WithContext(ctx), rt, proxy)
150 cancel()
151 a.lifeStop()
152 stale := !proxied && w.Code == http.StatusServiceUnavailable && w.Body.String() == "instance is waking; retry shortly\n"
153 if !late && !stale {
154 t.Fatalf("near startup: moved socket not refused: proxied=%v code=%d", proxied, w.Code)
155 }
156 if late && stale {
157 t.Fatal("long after startup: the route was re-checked")
158 }
159 }
160}
161