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 "fmt"
7 "github.com/carlosframework/platform/internal/edge"
8 "github.com/carlosframework/platform/internal/registry"
9 "net/http"
10 "net/http/httptest"
11 "sync/atomic"
12 "testing"
13 "time"
14)
15
16type maintenanceProc struct {
17 calls atomic.Int32
18 exited atomic.Bool
19 gate <-chan struct{}
20 entered chan struct{}
21 err error
22 remains bool
23}
24
25func (p *maintenanceProc) Stop(ctx context.Context) error {
26 if p.calls.Add(1) == 1 && p.entered != nil {
27 close(p.entered)
28 }
29 if p.gate != nil {
30 select {
31 case <-p.gate:
32 case <-ctx.Done():
33 return ctx.Err()
34 }
35 }
36 if !p.remains {
37 p.exited.Store(true)
38 }
39 return p.err
40}
41func (p *maintenanceProc) Exited() bool { return p.exited.Load() }
42
43type maintenanceRep struct {
44 gate <-chan struct{}
45 entered chan struct{}
46 calls atomic.Int32
47 err error
48}
49
50func (r *maintenanceRep) Err() error { return nil }
51func (r *maintenanceRep) Close(ctx context.Context) error {
52 if r.calls.Add(1) == 1 && r.entered != nil {
53 close(r.entered)
54 }
55 if r.gate != nil {
56 select {
57 case <-r.gate:
58 case <-ctx.Done():
59 return ctx.Err()
60 }
61 }
62 return r.err
63}
64func maintenanceContext(t *testing.T, duration time.Duration) context.Context {
65 t.Helper()
66 ctx, cancel := context.WithTimeout(t.Context(), duration)
67 t.Cleanup(cancel)
68 return ctx
69}
70func maintenanceAwake(t *testing.T) (*Service, *actEntry, *maintenanceProc, *maintenanceProc, *maintenanceRep) {
71 t.Helper()
72 a, _, _, reg := testActivator(t, time.Hour)
73 t.Cleanup(func() { reg.Close() })
74 main, side, rep := &maintenanceProc{}, &maintenanceProc{}, &maintenanceRep{}
75 e := &actEntry{host: "fence.example", route: testRoute("fence.example"), state: actAwake, proc: main, sidecar: side, rep: rep, lastActive: a.now()}
76 a.entries[e.host] = e
77 return a, e, main, side, rep
78}
79func TestMaintenanceFenceChecksEveryStopOutcome(t *testing.T) {
80 t.Parallel()
81 for _, scenario := range []string{"success", "main-error", "sidecar-error", "replica-error", "main-alive", "sidecar-alive", "missing-main", "missing-replica", "uncertain-sidecar"} {
82 t.Run(scenario, func(t *testing.T) {
83 a, e, main, side, rep := maintenanceAwake(t)
84 fail := errors.New("private backend detail")
85 switch scenario {
86 case "main-error":
87 main.err = fail
88 case "sidecar-error":
89 side.err = fail
90 case "replica-error":
91 rep.err = fail
92 case "main-alive":
93 main.remains = true
94 case "sidecar-alive":
95 side.remains = true
96 case "missing-main":
97 e.proc = nil
98 case "missing-replica":
99 e.rep = nil
100 case "uncertain-sidecar":
101 e.sidecarUncertain = true
102 }
103 proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 1)
104 if scenario == "success" {
105 if err != nil || !proof.Matches(e.host, 1) || proof.Matches(e.host, 2) {
106 t.Fatal("checked stop missing", err)
107 }
108 } else if err != ErrStopUnproven || proof != nil {
109 t.Fatal("failed stop became evidence", err)
110 }
111 if a.fences[e.host] != 1 {
112 t.Fatal("failure removed fence")
113 }
114 if !a.Quiesced(e.host) {
115 t.Fatal("ordinary entry removal changed")
116 }
117 retry, retryErr := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 1)
118 if scenario != "success" && (retryErr != ErrStopUnproven || retry != nil) {
119 t.Fatal("deleted entry erased failure")
120 }
121 if main.calls.Load() > 1 || side.calls.Load() > 1 || rep.calls.Load() > 1 {
122 t.Fatal("replayed spent stop")
123 }
124 })
125 }
126}
127func TestMaintenanceFenceBlocksEveryAdmissionIncludingStaleRoute(t *testing.T) {
128 t.Parallel()
129 a, _, _, reg := testActivator(t, time.Hour)
130 defer reg.Close()
131 host := "absent.example"
132 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), host, 3); err != ErrStopUnproven || proof != nil {
133 t.Fatal("absence became stop proof", err)
134 }
135 rt := testRoute(host)
136 if _, err := a.beginRequest(t.Context(), rt); err != ErrFenced {
137 t.Fatal("member wake passed", err)
138 }
139 if err := a.BeginSidecarRequest(t.Context(), rt); err != ErrFenced {
140 t.Fatal("sidecar passed", err)
141 }
142 for _, scheduled := range []bool{false, true} {
143 for _, stale := range []bool{false, true} {
144 for _, sidecar := range []bool{false, true} {
145 route := rt
146 if stale {
147 route.Hibernate = false
148 route.Kind = "service"
149 }
150 req := httptest.NewRequest("GET", "https://"+host+"/", nil)
151 req, answered := edge.WithAnswerFlag(req)
152 if sidecar {
153 req.Header.Set(SidecarHeader, "1")
154 }
155 out := httptest.NewRecorder()
156 proxied := false
157 proxy := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { proxied = true })
158 var handled bool
159 if scheduled {
160 handled = a.ServeScheduled(out, req, route, proxy)
161 } else {
162 handled = a.ServeHTTP(out, req, route, proxy)
163 }
164 if !handled || proxied || !*answered || out.Code != 503 || out.Header().Get("Cache-Control") != "no-store" {
165 t.Fatal("fence bypassed by stale or scheduled route")
166 }
167 }
168 }
169 }
170 if _, err := a.FenceAndStop(maintenanceContext(t, time.Second), host, 2); err != ErrFenceGeneration {
171 t.Fatal("older generation accepted")
172 }
173 // A new process has no closure evidence from the old activator's lifetime.
174 fresh := New(a.cfg, a.backend, reg)
175 if proof, err := fresh.FenceAndStop(maintenanceContext(t, time.Second), host, 3); err != ErrStopUnproven || proof != nil {
176 t.Fatal("restart invented old evidence")
177 }
178}
179func TestMaintenanceFenceContextAndGenerationDoNotRestoreAdmission(t *testing.T) {
180 a, e, main, _, _ := maintenanceAwake(t)
181 gate := make(chan struct{})
182 main.gate = gate
183 main.entered = make(chan struct{})
184 first := make(chan error, 1)
185 go func() { _, err := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 1); first <- err }()
186 <-main.entered
187 if proof, err := a.FenceAndStop(maintenanceContext(t, 15*time.Millisecond), e.host, 2); err != ErrStopUnproven || proof != nil {
188 t.Fatal("timed-out stop claimed proof")
189 }
190 if _, err := a.beginRequest(t.Context(), e.route); err != ErrFenced {
191 t.Fatal("timeout reopened admission")
192 }
193 close(gate)
194 if err := <-first; err != ErrFenceGeneration {
195 t.Fatal("old waiter survived newer fence", err)
196 }
197 proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 2)
198 if err != nil || !proof.Matches(e.host, 2) {
199 t.Fatal("checked outcome lost after timeout", err)
200 }
201 for _, ctx := range []context.Context{nil, context.Background(), maintenanceContext(t, -time.Second)} {
202 if p, err := a.FenceAndStop(ctx, e.host, 2); err == nil || p != nil {
203 t.Fatal("unbounded or expired stop accepted")
204 }
205 }
206 a.mu.Lock()
207 started := time.Now()
208 if _, err := a.FenceAndStop(maintenanceContext(t, 15*time.Millisecond), e.host, 2); err != ErrFenceNotInstalled {
209 t.Fatal(err)
210 }
211 a.mu.Unlock()
212 if time.Since(started) > 250*time.Millisecond {
213 t.Fatal("fence mutex escaped deadline")
214 }
215}
216func TestMaintenanceOrdinaryStopFailureRemainsObservable(t *testing.T) {
217 t.Parallel()
218 a, e, main, _, _ := maintenanceAwake(t)
219 main.err = errors.New("stop refused")
220 a.mu.Lock()
221 a.beginStopLocked(e, StopIdle)
222 done := e.stopped
223 a.mu.Unlock()
224 <-done
225 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 4); err != ErrStopUnproven || proof != nil {
226 t.Fatal("ordinary stop erased failure", err)
227 }
228}
229
230func TestMaintenanceLaterOrdinaryStopCannotEraseUnresolvedCleanup(t *testing.T) {
231 t.Parallel()
232 a, e, main, _, _ := maintenanceAwake(t)
233 main.err = errors.New("earlier process may still run")
234 a.mu.Lock()
235 a.beginStopLocked(e, StopIdle)
236 done := e.stopped
237 a.mu.Unlock()
238 <-done
239 replacement := &actEntry{host: e.host, route: e.route, state: actAwake, proc: &maintenanceProc{}, sidecar: &maintenanceProc{}, rep: &maintenanceRep{}}
240 a.mu.Lock()
241 a.entries[e.host] = replacement
242 a.beginStopLocked(replacement, StopIdle)
243 done = replacement.stopped
244 a.mu.Unlock()
245 <-done
246 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 1); err != ErrStopUnproven || proof != nil {
247 t.Fatal("new entry erased unresolved predecessor", err)
248 }
249}
250
251type maintenanceBackend struct {
252 *fakeBackend
253 main *maintenanceProc
254 side *maintenanceProc
255 rep *maintenanceRep
256 acquireGate <-chan struct{}
257 acquireFailure error
258 acquireEntered chan struct{}
259 replicateGate <-chan struct{}
260 replicateEntered chan struct{}
261 sideGate <-chan struct{}
262 sideEntered chan struct{}
263 startFailure error
264 sideFailure error
265 replicateFailure error
266}
267
268func (b *maintenanceBackend) Acquire(ctx context.Context, rt registry.Route) (Lease, error) {
269 if b.acquireEntered != nil {
270 close(b.acquireEntered)
271 }
272 if b.acquireGate != nil {
273 <-b.acquireGate
274 }
275 lease, err := b.fakeBackend.Acquire(ctx, rt)
276 if err != nil {
277 return nil, err
278 }
279 return &maintenanceLease{Lease: lease, b: b}, b.acquireFailure
280}
281func (b *maintenanceBackend) Start(context.Context, registry.Route) (Proc, string, error) {
282 b.mu.Lock()
283 b.starts++
284 b.mu.Unlock()
285 if b.startFailure != nil {
286 if b.main == nil {
287 return nil, "", b.startFailure
288 }
289 return b.main, "", b.startFailure
290 }
291 return b.main, "", nil
292}
293func (b *maintenanceBackend) StartSidecar(context.Context, registry.Route) (Proc, error) {
294 b.mu.Lock()
295 b.sidecarStarts++
296 b.mu.Unlock()
297 if b.sideEntered != nil {
298 close(b.sideEntered)
299 }
300 if b.sideGate != nil {
301 <-b.sideGate
302 }
303 return b.side, b.sideFailure
304}
305
306type maintenanceLease struct {
307 Lease
308 b *maintenanceBackend
309}
310
311func (l *maintenanceLease) Replicate(context.Context, string, time.Duration) (Rep, error) {
312 if l.b.replicateEntered != nil {
313 close(l.b.replicateEntered)
314 }
315 if l.b.replicateGate != nil {
316 <-l.b.replicateGate
317 }
318 if l.b.rep == nil {
319 return nil, l.b.replicateFailure
320 }
321 return l.b.rep, l.b.replicateFailure
322}
323func TestMaintenanceFenceDuringWakeOwnsItsCleanup(t *testing.T) {
324 t.Parallel()
325 for _, phase := range []string{"acquire", "replicate"} {
326 t.Run(phase, func(t *testing.T) {
327 a, base, _, reg := testActivator(t, time.Hour)
328 defer reg.Close()
329 gate, entered := make(chan struct{}), make(chan struct{})
330 b := &maintenanceBackend{fakeBackend: base, main: &maintenanceProc{}, rep: &maintenanceRep{}}
331 if phase == "acquire" {
332 b.acquireGate = gate
333 b.acquireEntered = entered
334 } else {
335 b.replicateGate = gate
336 b.replicateEntered = entered
337 }
338 a.backend = b
339 rt := testRoute("waking.example")
340 request := make(chan error, 1)
341 go func() {
342 release, err := a.beginRequest(t.Context(), rt)
343 if release != nil {
344 release()
345 }
346 request <- err
347 }()
348 <-entered
349 if proof, err := a.FenceAndStop(maintenanceContext(t, 15*time.Millisecond), rt.Host, 1); err != ErrStopUnproven || proof != nil {
350 t.Fatal("in-flight wake yielded proof", err)
351 }
352 close(gate)
353 if err := <-request; err != ErrFenced {
354 t.Fatal("waking request admitted after fence", err)
355 }
356 proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), rt.Host, 1)
357 if phase == "acquire" {
358 if err != ErrStopUnproven || proof != nil {
359 t.Fatal("canceled pre-spawn wake invented closure", err)
360 }
361 if b.starts != 0 || b.restores != 0 {
362 t.Fatal("fenced acquisition continued startup")
363 }
364 } else {
365 if err != nil || !proof.Matches(rt.Host, 1) {
366 t.Fatal("started wake did not yield checked cleanup", err)
367 }
368 if b.main.calls.Load() != 1 || b.rep.calls.Load() != 1 {
369 t.Fatal("partial wake resources not closed")
370 }
371 }
372 })
373 }
374}
375func TestMaintenanceFailedBackendStartRemainsUnproven(t *testing.T) {
376 t.Parallel()
377 a, base, _, reg := testActivator(t, time.Hour)
378 defer reg.Close()
379 a.backend = &maintenanceBackend{fakeBackend: base, startFailure: errors.New("hidden backend cleanup failed")}
380 rt := testRoute("hidden.example")
381 if _, err := a.beginRequest(t.Context(), rt); err == nil {
382 t.Fatal("failed backend started")
383 }
384 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), rt.Host, 1); err != ErrStopUnproven || proof != nil {
385 t.Fatal("hidden cleanup failure became proof", err)
386 }
387}
388
389// Before design §6.7 this restart ran synchronously under a.mu, so a fence
390// attempt that raced a stalled StartSidecar found the lock held and came
391// back ErrFenceNotInstalled without ever touching the entry. Moving the
392// call off a.mu means a stalled restart can't block anything else,
393// including FenceAndStop: it now installs its fence immediately. What
394// still bounds a short-context attempt is stop()'s own wait
395// (sidecarStopTimeout) for the stalled restart to land before it can act
396// on e.sidecar, so the outcome changes (ErrStopUnproven, fence installed)
397// but a stalled sidecar still keeps a checked stop from completing
398// instantly, which is the property this test exists to cover.
399func TestMaintenanceFenceSerializesSidecarSweepReplacement(t *testing.T) {
400 t.Parallel()
401 a, e, _, side, rep := maintenanceAwake(t)
402 gate, entered := make(chan struct{}), make(chan struct{})
403 base := a.backend.(*fakeBackend)
404 base.sidecarStaged = true
405 next := &maintenanceProc{}
406 a.backend = &maintenanceBackend{fakeBackend: base, side: next, sideGate: gate, sideEntered: entered, rep: rep}
407 side.exited.Store(true)
408 e.sidecarWanted = true
409 // sweepOnce itself now returns immediately. The restart it launches
410 // runs in its own goroutine, which is what's actually gated below.
411 a.sweepOnce()
412 <-entered
413
414 if proof, err := a.FenceAndStop(maintenanceContext(t, 15*time.Millisecond), e.host, 1); err != ErrStopUnproven || proof != nil {
415 t.Fatal("blocked replacement yielded proof", err)
416 }
417 if a.wakeAllowed(e.host) {
418 t.Fatal("fenced admission attempt failed to install its fence")
419 }
420 close(gate)
421 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 1); err != nil || !proof.Matches(e.host, 1) {
422 t.Fatal("replacement escaped checked stop", err)
423 }
424 waitFor(t, "the discarded replacement to be stopped", func() bool { return next.calls.Load() == 1 })
425 before := base.sidecarStartCount()
426 a.sweepOnce()
427 if base.sidecarStartCount() != before {
428 t.Fatal("fenced sweep restarted sidecar")
429 }
430}
431
432type maintenanceDue struct {
433 DueStore
434 route registry.Route
435 entered chan struct{}
436 gate <-chan struct{}
437}
438
439func (d *maintenanceDue) DueInstances(int64) ([]registry.Route, error) {
440 close(d.entered)
441 <-d.gate
442 return []registry.Route{d.route}, nil
443}
444func TestMaintenanceFenceStopsStaleScheduledWake(t *testing.T) {
445 t.Parallel()
446 a, b, _, reg := testActivator(t, time.Hour)
447 defer reg.Close()
448 rt := testRoute("scheduled.example")
449 rt.Status = "ready"
450 gate, entered := make(chan struct{}), make(chan struct{})
451 a.due = &maintenanceDue{DueStore: reg, route: rt, entered: entered, gate: gate}
452 swept := make(chan struct{})
453 go func() { a.sweepOnce(); close(swept) }()
454 <-entered
455 if _, err := a.FenceAndStop(maintenanceContext(t, time.Second), rt.Host, 1); err != ErrStopUnproven {
456 t.Fatal(err)
457 }
458 close(gate)
459 <-swept
460 b.mu.Lock()
461 starts, acquires := b.starts, b.acquires
462 b.mu.Unlock()
463 if starts != 0 || acquires != 0 || a.entryCount() != 0 {
464 t.Fatal("stale due query crossed installed fence")
465 }
466}
467func TestMaintenanceAwakeFenceSuppressesSidecarAdoption(t *testing.T) {
468 t.Parallel()
469 a, e, _, side, _ := maintenanceAwake(t)
470 base := a.backend.(*fakeBackend)
471 base.sidecarStaged = true
472 side.exited.Store(true)
473 a.fences = map[string]uint64{e.host: 1}
474 a.sweepOnce()
475 if base.sidecarStartCount() != 0 {
476 t.Fatal("awake fence allowed sidecar adoption")
477 }
478 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 1); err != nil || proof == nil {
479 t.Fatal(err)
480 }
481}
482
483func TestMaintenanceFailedStartCleansReturnedHandleBeforeRelease(t *testing.T) {
484 t.Parallel()
485 a, base, _, reg := testActivator(t, time.Hour)
486 defer reg.Close()
487 gate, entered := make(chan struct{}), make(chan struct{})
488 proc := &maintenanceProc{gate: gate, entered: entered}
489 a.backend = &maintenanceBackend{fakeBackend: base, main: proc, startFailure: errors.New("started then failed")}
490 rt := testRoute("partial.example")
491 done := make(chan error, 1)
492 go func() { _, err := a.beginRequest(t.Context(), rt); done <- err }()
493 select {
494 case <-entered:
495 case <-time.After(3 * time.Second):
496 t.Fatal("returned process cleanup was never attempted")
497 }
498 base.mu.Lock()
499 released := base.releases
500 base.mu.Unlock()
501 if released != 0 {
502 t.Fatal("lease released before returned process cleanup")
503 }
504 close(gate)
505 if err := <-done; err == nil {
506 t.Fatal("partial failed start admitted")
507 }
508 if proc.calls.Load() != 1 || !proc.Exited() {
509 t.Fatal("returned process handle was abandoned")
510 }
511 base.mu.Lock()
512 released = base.releases
513 base.mu.Unlock()
514 if released != 1 {
515 t.Fatal("lease release missing or repeated", released)
516 }
517 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), rt.Host, 1); err != ErrStopUnproven || proof != nil {
518 t.Fatal("partial failure erased uncertainty", err)
519 }
520}
521func TestMaintenanceFailedSidecarHandlesRemainOwned(t *testing.T) {
522 t.Parallel()
523 for _, phase := range []string{"wake", "sweep"} {
524 t.Run(phase, func(t *testing.T) {
525 a, base, _, reg := testActivator(t, time.Hour)
526 defer reg.Close()
527 base.sidecarStaged = true
528 main, partial, rep := &maintenanceProc{}, &maintenanceProc{}, &maintenanceRep{}
529 b := &maintenanceBackend{fakeBackend: base, main: main, side: partial, rep: rep, sideFailure: errors.New("sidecar started then failed")}
530 a.backend = b
531 rt := testRoute("partial-sidecar.example")
532 if phase == "wake" {
533 release, err := a.beginRequest(t.Context(), rt)
534 if err != nil {
535 t.Fatal(err)
536 }
537 release()
538 } else {
539 old := &maintenanceProc{}
540 old.exited.Store(true)
541 a.entries[rt.Host] = &actEntry{host: rt.Host, route: rt, state: actAwake, proc: main, rep: rep, sidecar: old, sidecarWanted: true, lastActive: a.now()}
542 a.sweepOnce()
543 // The restart call runs off a.mu (design §6.7); give it a
544 // moment to land before inspecting what it installed.
545 waitFor(t, "sidecar restart to land", func() bool { return base.sidecarStartCount() == 1 && a.sidecarSettled(rt.Host) })
546 }
547 a.mu.Lock()
548 entry := a.entries[rt.Host]
549 owned := entry.sidecar == partial && entry.sidecarUncertain
550 a.mu.Unlock()
551 if !owned {
552 t.Fatal("failed sidecar handle was discarded")
553 }
554 before := base.sidecarStartCount()
555 a.sweepOnce()
556 if base.sidecarStartCount() != before {
557 t.Fatal("live partial sidecar was replaced")
558 }
559 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), rt.Host, 1); err != ErrStopUnproven || proof != nil {
560 t.Fatal("failed sidecar yielded trusted evidence", err)
561 }
562 if partial.calls.Load() != 1 || !partial.Exited() {
563 t.Fatal("checked stop did not own partial sidecar")
564 }
565 })
566 }
567}
568
569func TestMaintenanceFailedReplicationClosesReturnedReplicaWithoutDoubleRelease(t *testing.T) {
570 t.Parallel()
571 for _, closeFails := range []bool{false, true} {
572 t.Run(fmt.Sprint(closeFails), func(t *testing.T) {
573 a, base, _, reg := testActivator(t, time.Hour)
574 defer reg.Close()
575 mainGate, mainEntered := make(chan struct{}), make(chan struct{})
576 repGate, repEntered := make(chan struct{}), make(chan struct{})
577 main := &maintenanceProc{gate: mainGate, entered: mainEntered}
578 rep := &maintenanceRep{gate: repGate, entered: repEntered}
579 if closeFails {
580 rep.err = errors.New("replica close failed")
581 }
582 a.backend = &maintenanceBackend{fakeBackend: base, main: main, rep: rep, replicateFailure: errors.New("replica opened then failed")}
583 rt := testRoute("partial-replica.example")
584 done := make(chan error, 1)
585 go func() { _, err := a.beginRequest(t.Context(), rt); done <- err }()
586 select {
587 case <-mainEntered:
588 case <-time.After(3 * time.Second):
589 t.Fatal("returned main cleanup missing")
590 }
591 if rep.calls.Load() != 0 {
592 t.Fatal("replica closed before main stopped")
593 }
594 close(mainGate)
595 select {
596 case <-repEntered:
597 case <-time.After(3 * time.Second):
598 t.Fatal("returned replica cleanup missing")
599 }
600 if !main.Exited() {
601 t.Fatal("replica closed before main exited")
602 }
603 base.mu.Lock()
604 released := base.releases
605 base.mu.Unlock()
606 if released != 0 {
607 t.Fatal("lease released while replica close pending")
608 }
609 close(repGate)
610 if err := <-done; err == nil {
611 t.Fatal("partial replica failure admitted")
612 }
613 base.mu.Lock()
614 released = base.releases
615 base.mu.Unlock()
616 if released != 0 || rep.calls.Load() != 1 {
617 t.Fatal("returned replica lease released twice", released, rep.calls.Load())
618 }
619 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), rt.Host, 1); err != ErrStopUnproven || proof != nil {
620 t.Fatal("partial replica uncertainty erased", err)
621 }
622 })
623 }
624}
625
626func TestMaintenanceFailedAcquireReleasesReturnedLease(t *testing.T) {
627 t.Parallel()
628 a, base, _, reg := testActivator(t, time.Hour)
629 defer reg.Close()
630 a.backend = &maintenanceBackend{fakeBackend: base, acquireFailure: errors.New("lease returned with acquisition failure")}
631 rt := testRoute("partial-lease.example")
632 if _, err := a.beginRequest(t.Context(), rt); err == nil {
633 t.Fatal("failed acquisition admitted")
634 }
635 base.mu.Lock()
636 released, starts := base.releases, base.starts
637 base.mu.Unlock()
638 if released != 1 || starts != 0 {
639 t.Fatal("returned lease abandoned or wake continued", released, starts)
640 }
641 if proof, err := a.FenceAndStop(maintenanceContext(t, time.Second), rt.Host, 1); err != ErrStopUnproven || proof != nil {
642 t.Fatal("failed acquisition became stop proof", err)
643 }
644}
645
646// TestFencedReportsAnInstalledFenceUntilItIsLifted: Fenced is the layout
647// pass's own affirmative read of what FenceAndStop and LiftFence leave
648// behind, so it must agree with them at every step rather than invert
649// wakeAllowed's private sense of the same map.
650func TestFencedReportsAnInstalledFenceUntilItIsLifted(t *testing.T) {
651 a, e, _, _, _ := maintenanceAwake(t)
652 if a.Fenced(e.host) {
653 t.Fatal("fence reported before one was installed")
654 }
655 if _, err := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 1); err != nil {
656 t.Fatal(err)
657 }
658 if !a.Fenced(e.host) {
659 t.Fatal("fence not reported as installed")
660 }
661 if !a.LiftFence(e.host, 1) {
662 t.Fatal("LiftFence reported nothing to release")
663 }
664 if a.Fenced(e.host) {
665 t.Fatal("fence still reported after it was released")
666 }
667}
668
669// TestFenceGenerationReportsTheHeldGeneration: the layout pass tells its
670// own kept fence from another operation's by the generation, so the read
671// must say exactly which generation holds, and 0 once none does.
672func TestFenceGenerationReportsTheHeldGeneration(t *testing.T) {
673 a, e, _, _, _ := maintenanceAwake(t)
674 if g := a.FenceGeneration(e.host); g != 0 {
675 t.Fatalf("generation %d before any fence", g)
676 }
677 if _, err := a.FenceAndStop(maintenanceContext(t, time.Second), e.host, 9); err != nil {
678 t.Fatal(err)
679 }
680 if g := a.FenceGeneration(e.host); g != 9 {
681 t.Fatalf("generation = %d, want 9", g)
682 }
683 a.LiftFence(e.host, 9)
684 if g := a.FenceGeneration(e.host); g != 0 {
685 t.Fatalf("generation %d after the lift", g)
686 }
687}
688
689// TestStopUncertainCoversLiveEntriesAndIncompleteHistory: a pass deciding
690// whether ErrStopUnproven can be treated as a completed stop needs both
691// halves of this: an entry that is still live right now (waking, awake or
692// stopping), and a past stop observation that never finished every check.
693// It must answer false for a host it has never seen at all, since that is
694// not evidence of anything.
695func TestStopUncertainCoversLiveEntriesAndIncompleteHistory(t *testing.T) {
696 t.Parallel()
697 a, e, main, _, _ := maintenanceAwake(t)
698
699 if !a.StopUncertain(e.host) {
700 t.Fatal("a live entry must read as stop-uncertain")
701 }
702 if a.StopUncertain("never-seen.example") {
703 t.Fatal("a host with no entry and no history must not read as uncertain")
704 }
705
706 // Drive the entry through an ordinary stop that fails, which removes the
707 // entry but leaves an incomplete observation in stopHistory.
708 main.err = errors.New("stop refused")
709 a.mu.Lock()
710 a.beginStopLocked(e, StopIdle)
711 done := e.stopped
712 a.mu.Unlock()
713 <-done
714 if a.Owns(e.host) {
715 t.Fatal("entry should have been forgotten once the stop finished")
716 }
717 if !a.StopUncertain(e.host) {
718 t.Fatal("an incomplete stop observation must read as uncertain")
719 }
720
721 // A different host's clean stop completes its observation and leaves no
722 // live entry either. StopUncertain must read false for it, proving the
723 // predicate is per-host rather than sticky once anything has failed.
724 clean, ce, _, _, _ := maintenanceAwake(t)
725 if proof, err := clean.FenceAndStop(maintenanceContext(t, time.Second), ce.host, 1); err != nil || !proof.Matches(ce.host, 1) {
726 t.Fatalf("checked clean stop: %v", err)
727 }
728 if clean.StopUncertain(ce.host) {
729 t.Fatal("a complete stop observation with no live entry must not read as uncertain")
730 }
731}
732
733// TestFenceAndStopIfUnfencedRefusesAnyHeldFence: the layout pass shares the
734// one fence slot per host with the residence move, and FenceAndStop would
735// replace a lower generation. The exclusive variant decides under the same
736// lock that installs, so a fence of any generation, lower, equal or
737// higher, refuses it and stays exactly as it was, and nothing is stopped.
738func TestFenceAndStopIfUnfencedRefusesAnyHeldFence(t *testing.T) {
739 t.Parallel()
740 for _, held := range []uint64{5, 10, 20} {
741 t.Run(fmt.Sprint(held), func(t *testing.T) {
742 a, e, main, _, _ := maintenanceAwake(t)
743 a.mu.Lock()
744 a.fences = map[string]uint64{e.host: held}
745 a.mu.Unlock()
746 _, err := a.FenceAndStopIfUnfenced(maintenanceContext(t, time.Second), e.host, 10)
747 if !errors.Is(err, ErrFenceHeld) {
748 t.Fatalf("err = %v, want ErrFenceHeld", err)
749 }
750 a.mu.Lock()
751 got := a.fences[e.host]
752 a.mu.Unlock()
753 if got != held {
754 t.Fatalf("held fence is %d, want %d", got, held)
755 }
756 if main.calls.Load() != 0 || !a.Owns(e.host) {
757 t.Fatal("a refused exclusive fence stopped the host")
758 }
759 })
760 }
761}
762
763// TestFenceAndStopIfUnfencedIsFenceAndStopOnAnUnfencedHost: with no fence
764// held it installs, stops and proves exactly as FenceAndStop does.
765func TestFenceAndStopIfUnfencedIsFenceAndStopOnAnUnfencedHost(t *testing.T) {
766 t.Parallel()
767 a, e, _, _, _ := maintenanceAwake(t)
768 proof, err := a.FenceAndStopIfUnfenced(maintenanceContext(t, time.Second), e.host, 7)
769 if err != nil || !proof.Matches(e.host, 7) {
770 t.Fatalf("exclusive fence on an unfenced awake host: %v", err)
771 }
772 if !a.Fenced(e.host) || a.Owns(e.host) {
773 t.Fatal("not fenced and stopped")
774 }
775 // An entry-less host with no stop on record answers ErrStopUnproven
776 // with its fence installed, as FenceAndStop does.
777 if _, err := a.FenceAndStopIfUnfenced(maintenanceContext(t, time.Second), "idle.example", 3); !errors.Is(err, ErrStopUnproven) {
778 t.Fatalf("entry-less host: %v", err)
779 }
780 if !a.LiftFence("idle.example", 3) {
781 t.Fatal("the unproven exclusive fence was not installed at its generation")
782 }
783}
784