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 "strings"
7 "testing"
8 "time"
9)
10
11func TestObserveSettle(t *testing.T) {
12 t.Parallel()
13 if r := observeSettle(context.Background(), &fakeProc{}, 20*time.Millisecond); r != settleOK {
14 t.Fatalf("a process that stayed up: %v", r)
15 }
16 // A process that dies inside the window: alive at the first check, dead
17 // at the observation's next poll (no sleep places the death).
18 p := &fakeProc{}
19 p.exitOnPoll(2)
20 if r := observeSettle(context.Background(), p, time.Second); r != settleExited {
21 t.Fatalf("a process that died: %v", r)
22 }
23 // An observation cancelled inside the window: the cancel happens at its
24 // first poll after the opening check.
25 ctx, cancel := context.WithCancel(context.Background())
26 defer cancel()
27 if r := observeSettle(ctx, &cancelOnPoll{at: 2, cancel: cancel}, time.Second); r != settleCancelled {
28 t.Fatalf("a cancelled observation: %v", r)
29 }
30 // Cancellation wins over an exit seen at the same moment (spec §6.4).
31 dead := &fakeProc{}
32 dead.exit()
33 gone, stop := context.WithCancel(context.Background())
34 stop()
35 if r := observeSettle(gone, dead, time.Second); r != settleCancelled {
36 t.Fatalf("cancelled and exited: %v, want cancelled", r)
37 }
38}
39
40// cancelOnPoll is a live process whose at-th Exited() call cancels the
41// observation watching it.
42type cancelOnPoll struct {
43 polls int
44 at int
45 cancel context.CancelFunc
46}
47
48func (p *cancelOnPoll) Stop(context.Context) error { return nil }
49func (p *cancelOnPoll) Exited() bool {
50 p.polls++
51 if p.polls == p.at {
52 p.cancel()
53 }
54 return false
55}
56
57func TestSwapCanarySettle(t *testing.T) {
58 old := swapSettle
59 t.Cleanup(func() { swapSettle = old })
60
61 t.Run("exit during the settle fails the build", func(t *testing.T) {
62 swapSettle = 2 * time.Second
63 f := newSwapFixture(t)
64 f.a.genHealth = func(context.Context, Gen) error {
65 // Alive at the settle's opening check, dead at its next poll.
66 _, p := f.b.lastGen()
67 p.exitOnPoll(2)
68 return nil
69 }
70 at := f.attempt(ModeOverlap)
71 at.Canary = true
72 res := f.a.Swap(context.Background(), at, f.newest)
73 if res.Outcome != SwapFailed || !strings.Contains(res.Reason, errSettleExit.Error()) {
74 t.Fatalf("Swap = %+v", res)
75 }
76 if got := f.appSocket(t); got != f.incumbent.Socket {
77 t.Fatalf("AppSocket = %q, want the incumbent", got)
78 }
79 })
80 t.Run("a canary waits out the settle", func(t *testing.T) {
81 swapSettle = 100 * time.Millisecond
82 f := newSwapFixture(t)
83 at := f.attempt(ModeOverlap)
84 at.Canary = true
85 start := time.Now()
86 if res := f.a.Swap(context.Background(), at, f.newest); res.Outcome != SwapDone {
87 t.Fatalf("Swap = %+v", res)
88 }
89 if took := time.Since(start); took < swapSettle {
90 t.Fatalf("switched after %s, inside the %s settle", took, swapSettle)
91 }
92 })
93 t.Run("a swap after the canary does not settle", func(t *testing.T) {
94 swapSettle = time.Hour
95 f := newSwapFixture(t)
96 done := make(chan SwapResult, 1)
97 go func() { done <- f.a.Swap(context.Background(), f.attempt(ModeOverlap), f.newest) }()
98 select {
99 case res := <-done:
100 if res.Outcome != SwapDone {
101 t.Fatalf("Swap = %+v", res)
102 }
103 case <-time.After(5 * time.Second):
104 t.Fatal("a non-canary swap waited for a settle")
105 }
106 })
107}
108
109// An intentional stop of the entry during the settle cancels the
110// observation and is not a failure (spec §6.4). The successor is stopped
111// before the incumbent, through the barrier.
112func TestSwapIntentionalStopDuringSettleIsNotAFailure(t *testing.T) {
113 old := swapSettle
114 swapSettle = 5 * time.Second
115 t.Cleanup(func() { swapSettle = old })
116 for _, tc := range []struct {
117 name string
118 stop func(f *swapFixture)
119 }{
120 {"restart", func(f *swapFixture) { f.a.Bounce(f.host) }},
121 {"hibernate", func(f *swapFixture) { f.clk.advance(2 * time.Hour); f.a.sweepOnce() }},
122 {"lease lost", func(f *swapFixture) { f.b.lastRep().setErr(errors.New("renewal failed")); f.a.sweepOnce() }},
123 {"maintenance fence", func(f *swapFixture) {
124 go func() {
125 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
126 defer cancel()
127 f.a.FenceAndStop(ctx, f.host, 1) //nolint:errcheck
128 }()
129 }},
130 {"move or park retire", func(f *swapFixture) { f.a.RetireExcept(map[string]bool{}) }},
131 {"shutdown", func(f *swapFixture) { go f.a.Shutdown(context.Background()) }},
132 } {
133 t.Run(tc.name, func(t *testing.T) {
134 f := newSwapFixture(t)
135 healthy := make(chan struct{})
136 f.a.genHealth = func(context.Context, Gen) error { close(healthy); return nil }
137 rep := f.b.lastRep()
138 at := f.attempt(ModeOverlap)
139 at.Canary = true
140 results := make(chan SwapResult, 1)
141 go func() { results <- f.a.Swap(context.Background(), at, f.newest) }()
142 <-healthy
143 // The settle has begun once it has polled the successor (nothing
144 // else asks it between the health check and the settle).
145 _, succ := f.b.lastGen()
146 waitFor(t, "the settle's first poll", func() bool { return succ.pollCount() > 0 })
147 tc.stop(f)
148 var res SwapResult
149 select {
150 case res = <-results:
151 case <-time.After(10 * time.Second):
152 t.Fatal("Swap did not return after the stop")
153 }
154 if res.Outcome != SwapAbandoned || !strings.Contains(res.Reason, errSwapStopped.Error()) {
155 t.Fatalf("Swap = %+v, want abandoned by the stop", res)
156 }
157 waitFor(t, "the lease release", rep.isClosed)
158 ev := f.b.events()
159 if i, j := indexPrefix(ev, "stop:gen:"), indexPrefix(ev, "stop:instance"); i < 0 || j < 0 || i > j {
160 t.Fatalf("order %v: the successor must stop before the incumbent", ev)
161 }
162 })
163 }
164}
165