carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package instancectl
2
3import (
4 "context"
5 "io"
6 "net"
7 "net/http"
8 "strings"
9 "testing"
10 "time"
11)
12
13// Astra round 3 P1: connection 1 (keep-alive) is accepted first and later
14// carries the /stop; a delayed /start arrives on connection 2 and has not
15// registered when the stop fences the host. The fence is the accepted
16// connection watermark at that moment, not the stop's own number, so the
17// delayed start is refused when it finally registers.
18func TestADelayedStartAcceptedBeforeTheFenceIsRefusedWhenItRegisters(t *testing.T) {
19 host := "watermark.example.com"
20 s, _ := newServer(t, &fakeCtl{})
21 _ = s.nextConnSeq() // connection 1: will carry the /stop
22 startConn := s.nextConnSeq() // connection 2: the delayed /start
23 release, err := s.fenceLegacyStarts(context.Background(), host)
24 if err != nil {
25 t.Fatalf("fence: %v", err)
26 }
27 release()
28 if _, finish, err := s.beginLegacyStart(context.Background(), host, startConn); err == nil {
29 finish()
30 t.Fatal("a start accepted before the fence registered after it and was admitted")
31 }
32}
33
34// Astra round 3 P2: a client that reuses its connection gets a fresh stamp
35// for every request, because the server answers each on its own connection.
36// A start after a stop, from the same keep-alive client, runs.
37func TestAKeepAliveClientsStartAfterAStopRuns(t *testing.T) {
38 host := "reuse.example.com"
39 ctl := &fakeCtl{answers: map[string]string{"show -p InvocationID --value": "inv1\n"}}
40 _, sock := serveOnSocket(t, ctl)
41 var dials int
42 c := &http.Client{Transport: &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
43 dials++
44 return (&net.Dialer{}).DialContext(ctx, "unix", sock)
45 }}}
46 for i, path := range []string{"/start?host=" + host + "&role=run&memory_max=536870912", "/stop?host=" + host + "&role=run", "/start?host=" + host + "&role=run&memory_max=536870912"} {
47 resp, err := c.Post("http://unix"+path, "", nil)
48 if err != nil {
49 t.Fatalf("POST %s: %v", path, err)
50 }
51 body, _ := io.ReadAll(resp.Body)
52 resp.Body.Close()
53 if resp.StatusCode/100 != 2 {
54 t.Fatalf("request %d %s: status %d: %s", i, path, resp.StatusCode, body)
55 }
56 }
57 if dials != 3 {
58 t.Fatalf("dials = %d, want one connection per request", dials)
59 }
60}
61
62// Liveness 1: the fence covers only starts accepted before the stop. A
63// /start accepted after the /stop has returned runs normally, and nothing
64// of a fence outlives the server: a restarted instance-control has none.
65func TestTheFenceCoversOnlyStartsAcceptedBeforeTheStop(t *testing.T) {
66 host := "after.example.com"
67 ctl := &fakeCtl{answers: map[string]string{"show -p InvocationID --value": "inv1\n"}}
68 s, c := newServer(t, ctl)
69 stop := post(t, c, "/stop?host="+host+"&role=run")
70 stop.Body.Close()
71 if stop.StatusCode != http.StatusNoContent {
72 t.Fatalf("/stop status %d", stop.StatusCode)
73 }
74 start := post(t, c, "/start?host="+host+"&role=run&memory_max=536870912")
75 start.Body.Close()
76 if start.StatusCode != http.StatusOK {
77 t.Fatalf("a start accepted after the stop returned: status %d, want 200", start.StatusCode)
78 }
79 s.legacyMu.Lock()
80 stopping := s.legacyStopping[host]
81 s.legacyMu.Unlock()
82 if stopping != 0 {
83 t.Fatalf("the stop's refusal of new starts outlived it (%d)", stopping)
84 }
85 // A restarted service: the fence lived in memory only.
86 _, c2 := newServer(t, &fakeCtl{answers: map[string]string{"show -p InvocationID --value": "inv2\n"}})
87 again := post(t, c2, "/start?host="+host+"&role=run&memory_max=536870912")
88 again.Body.Close()
89 if again.StatusCode != http.StatusOK {
90 t.Fatalf("a restarted instance-control refused a start: status %d", again.StatusCode)
91 }
92}
93
94// heldStart parks host's legacy /start before its tier step until release
95// is closed, and returns once the handler is parked there. The handler does
96// not see its own cancellation (unseenCancel).
97func heldStart(t *testing.T, ctl *fakeCtl, c *http.Client, host string) (release chan struct{}) {
98 t.Helper()
99 entered, gate := make(chan struct{}), make(chan struct{})
100 ctl.mu.Lock()
101 if ctl.before == nil {
102 ctl.before = map[string]func(){}
103 }
104 ctl.before["set-property"] = func() {
105 close(entered)
106 <-gate
107 }
108 ctl.unseenCancel = true
109 ctl.mu.Unlock()
110 postAsync(c, "/start?host="+host+"&role=run&memory_max=536870912")
111 select {
112 case <-entered:
113 case <-time.After(5 * time.Second):
114 t.Fatal("the held start never reached its tier step")
115 }
116 return gate
117}
118
119// Liveness 2: the join is per host. Host A's attempt will not join; /start
120// and /stop of host B answer at once while A's /stop is still joining.
121func TestAStuckJoinDelaysNoOtherHost(t *testing.T) {
122 ctl := &fakeCtl{answers: map[string]string{"show -p InvocationID --value": "inv1\n"}}
123 _, c := newServer(t, ctl)
124 gate := heldStart(t, ctl, c, "stuck.example.com")
125 stopA := postAsync(c, "/stop?host=stuck.example.com&role=run")
126 defer func() { close(gate); <-stopA }()
127 time.Sleep(50 * time.Millisecond) // A's stop is joining
128 for _, path := range []string{"/start?host=free.example.com&role=run&memory_max=536870912", "/stop?host=free.example.com&role=run"} {
129 done := postAsync(c, path)
130 select {
131 case code := <-done:
132 if code/100 != 2 {
133 t.Fatalf("%s: status %d", path, code)
134 }
135 case <-time.After(2 * time.Second):
136 t.Fatalf("%s waited behind another host's stuck join", path)
137 }
138 }
139 select {
140 case <-stopA:
141 t.Fatal("A's stop answered while its attempt was still stuck")
142 default:
143 }
144}
145
146// Liveness 3: a stuck stop answers once, within the bound, and the next
147// /stop of the host does not hang behind the stuck attempt: it answers the
148// same error within the bound, and joins it once it returns.
149func TestAStuckStopAnswersAndTheNextStopDoesNotHang(t *testing.T) {
150 host := "wedged.example.com"
151 ctl := &fakeCtl{answers: map[string]string{"show -p InvocationID --value": "inv1\n"}}
152 _, c := newServer(t, ctl, func(s *Server) { s.legacyJoin = 200 * time.Millisecond })
153 gate := heldStart(t, ctl, c, host)
154 stopOnce := func() (int, string, time.Duration) {
155 begun := time.Now()
156 resp := post(t, c, "/stop?host="+host+"&role=run")
157 body, _ := io.ReadAll(resp.Body)
158 resp.Body.Close()
159 return resp.StatusCode, string(body), time.Since(begun)
160 }
161 for i := 0; i < 2; i++ {
162 code, body, took := stopOnce()
163 if code != http.StatusBadGateway || !strings.Contains(body, "still in flight") {
164 t.Fatalf("stop %d with the attempt stuck: %d %q, want 502 naming the start in flight", i+1, code, body)
165 }
166 if took > 2*time.Second {
167 t.Fatalf("stop %d took %s, past its bound", i+1, took)
168 }
169 }
170 close(gate)
171 time.Sleep(50 * time.Millisecond)
172 if code, body, _ := stopOnce(); code != http.StatusNoContent {
173 t.Fatalf("stop once the attempt returned: %d %q, want 204", code, body)
174 }
175}
176
177// Liveness 4: the other verbs pass through unchanged during a legacy stop.
178// While host A's legacy /stop is joining a stuck attempt, A's sidecar
179// /start and seed /stop answer at once.
180func TestOtherVerbsPassThroughDuringALegacyStop(t *testing.T) {
181 host := "verbs.example.com"
182 ctl := &fakeCtl{answers: map[string]string{"show -p InvocationID --value": "inv1\n"}}
183 _, c := newServer(t, ctl)
184 gate := heldStart(t, ctl, c, host)
185 stop := postAsync(c, "/stop?host="+host+"&role=run")
186 defer func() { close(gate); <-stop }()
187 time.Sleep(50 * time.Millisecond) // the legacy stop is joining
188 for _, path := range []string{"/start?host=" + host + "&role=sidecar&memory_max=536870912", "/stop?host=" + host + "&role=seed"} {
189 done := postAsync(c, path)
190 select {
191 case code := <-done:
192 if code/100 != 2 {
193 t.Fatalf("%s during a legacy stop: status %d", path, code)
194 }
195 case <-time.After(2 * time.Second):
196 t.Fatalf("%s waited behind a legacy stop", path)
197 }
198 }
199}
200
201// postAsync POSTs path from its own goroutine, never touching t, and
202// delivers the status (0 on a transport error).
203func postAsync(c *http.Client, path string) <-chan int {
204 done := make(chan int, 1)
205 go func() {
206 resp, err := c.Post("http://unix"+path, "", nil)
207 if err != nil {
208 done <- 0
209 return
210 }
211 resp.Body.Close()
212 done <- resp.StatusCode
213 }()
214 return done
215}
216