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 "sync"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostwrite"
11)
12
13var errClaimChanged = errors.New("test: claim changed")
14
15// swapWork is claim-backed work. denyAt makes its n-th With refuse before
16// running fn, which is how hostguard refuses a changed claim or stamp.
17type swapWork struct {
18 mu sync.Mutex
19 calls int
20 denyAt int
21}
22
23func (w *swapWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error {
24 w.mu.Lock()
25 w.calls++
26 n := w.calls
27 w.mu.Unlock()
28 if id != (hostwrite.Identity{}) {
29 return errors.New("test: a swap must not claim config or release inputs")
30 }
31 if n == w.denyAt {
32 return errClaimChanged
33 }
34 return fn()
35}
36
37func (w *swapWork) withCalls() int {
38 w.mu.Lock()
39 defer w.mu.Unlock()
40 return w.calls
41}
42
43type swapSelector struct {
44 work *swapWork
45 err error
46}
47
48func (s *swapSelector) Select(_ context.Context, _, action string) (hostwrite.Work, error) {
49 if action != "spawn" {
50 return nil, errors.New("test: unexpected action " + action)
51 }
52 if s.work == nil {
53 return nil, s.err
54 }
55 return s.work, s.err
56}
57
58// The four §7.4 checks, one at a time.
59func TestSwapChecks(t *testing.T) {
60 a, b, _, _ := testActivator(t, time.Hour)
61 host := "checks.example.com"
62 arrangeAwake(t, a, testRoute(host))
63 at := Attempt{Host: host, SchedRev: 4}
64 four := func(string) uint64 { return 4 }
65 check := func(newest func(string) uint64) error {
66 a.mu.Lock()
67 defer a.mu.Unlock()
68 return a.swapChecksLocked(a.entries[host], at, newest)
69 }
70 if err := check(four); err != nil {
71 t.Fatalf("an awake, leased, newest, unfenced entry was refused: %v", err)
72 }
73 if err := check(func(string) uint64 { return 5 }); !errors.Is(err, errSwapSuperseded) {
74 t.Fatalf("superseded: %v", err)
75 }
76 a.mu.Lock()
77 a.fences = map[string]uint64{host: 3}
78 a.mu.Unlock()
79 if err := check(four); !errors.Is(err, errSwapFenced) {
80 t.Fatalf("fenced: %v", err)
81 }
82 a.mu.Lock()
83 delete(a.fences, host)
84 a.shuttingDown = true
85 a.mu.Unlock()
86 if err := check(four); !errors.Is(err, errSwapShutdown) {
87 t.Fatalf("shutting down: %v", err)
88 }
89 a.mu.Lock()
90 a.shuttingDown = false
91 a.mu.Unlock()
92 b.lastRep().setErr(errors.New("renewal failed"))
93 if err := check(four); !errors.Is(err, errSwapLeaseLost) {
94 t.Fatalf("lease lost: %v", err)
95 }
96 b.lastRep().setErr(nil)
97 a.mu.Lock()
98 a.entries[host].state = actStopping
99 a.mu.Unlock()
100 if err := check(four); !errors.Is(err, errSwapNotAwake) {
101 t.Fatalf("stopping: %v", err)
102 }
103 a.mu.Lock()
104 a.entries[host].state = actAwake
105 err := a.swapChecksLocked(nil, at, four)
106 a.mu.Unlock()
107 if !errors.Is(err, errSwapNotAwake) {
108 t.Fatalf("asleep: %v", err)
109 }
110}
111
112// Spec §7.4: ownership is checked apart from supersession, so an exec that
113// is not an attempt's (a crash restart, a fallback, a restore, a sidecar)
114// can ask it. A superseded attempt still owns the host; a lost lease does
115// not, whatever the revision says.
116func TestOwnershipIsCheckedApartFromSupersession(t *testing.T) {
117 a, b, _, _ := testActivator(t, time.Hour)
118 host := "owned.example.com"
119 arrangeAwake(t, a, testRoute(host))
120 owned := func(waking bool) error {
121 a.mu.Lock()
122 defer a.mu.Unlock()
123 return a.ownedLocked(a.entries[host], waking)
124 }
125 if err := owned(false); err != nil {
126 t.Fatalf("an awake, leased, unfenced entry: %v", err)
127 }
128 b.lastRep().setErr(errors.New("renewal failed"))
129 if err := owned(false); !errors.Is(err, errSwapLeaseLost) {
130 t.Fatalf("lease lost: %v", err)
131 }
132 if err := owned(true); err != nil {
133 t.Fatalf("a waking entry's lease is the wake's own: %v", err)
134 }
135 b.lastRep().setErr(nil)
136 a.mu.Lock()
137 a.fences = map[string]uint64{host: 1}
138 a.mu.Unlock()
139 if err := owned(true); !errors.Is(err, errSwapFenced) {
140 t.Fatalf("fenced while waking: %v", err)
141 }
142}
143
144// Claims off: hostwrite's legacy work. The check runs alone, under the
145// entry lock that fn takes itself.
146func TestAdmitClaimsOffRunsTheCheckAlone(t *testing.T) {
147 a := New(Config{}, &fakeBackend{}, nil)
148 work, err := a.selectWork(context.Background(), "h.example.com")
149 if err != nil || !hostwrite.Legacy(work) {
150 t.Fatalf("claims off selected %T, %v; want the legacy work", work, err)
151 }
152 ran := 0
153 if err := a.admit(context.Background(), work, func() error { ran++; return nil }); err != nil || ran != 1 {
154 t.Fatalf("admit = %v, ran %d", err, ran)
155 }
156}
157
158// Claims on: the claim and stamp are validated first, and a refusal is
159// final. A selector that cannot answer refuses. Nothing is ever read as
160// claims off.
161func TestAdmitClaimsOnValidatesFirstAndNeverFallsBack(t *testing.T) {
162 ctx := context.Background()
163 w := &swapWork{}
164 a := New(Config{HostWrites: hostwrite.Config{Enabled: true, Selector: &swapSelector{work: w}}}, &fakeBackend{}, nil)
165 work, err := a.selectWork(ctx, "h.example.com")
166 if err != nil || hostwrite.Legacy(work) {
167 t.Fatalf("claims on selected %T, %v", work, err)
168 }
169 ran := 0
170 fn := func() error { ran++; return nil }
171 if err := a.admit(ctx, work, fn); err != nil || ran != 1 || w.withCalls() != 1 {
172 t.Fatalf("admit = %v, ran %d, With %d", err, ran, w.withCalls())
173 }
174 w.denyAt = 2
175 if err := a.admit(ctx, work, fn); !errors.Is(err, errClaimChanged) || ran != 1 {
176 t.Fatalf("refused claim: err %v, ran %d (fn must not run)", err, ran)
177 }
178 for name, sel := range map[string]hostwrite.Selector{
179 "selector error": &swapSelector{err: hostwrite.ErrUnavailable},
180 "no selector": nil,
181 "nil work": &swapSelector{},
182 } {
183 a.cfg.HostWrites.Selector = sel
184 if _, err := a.selectWork(ctx, "h.example.com"); !errors.Is(err, hostwrite.ErrUnavailable) {
185 t.Errorf("%s: %v, want hostwrite.ErrUnavailable", name, err)
186 }
187 }
188}
189