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 "strings"
8 "sync"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/cgroup"
12 "github.com/carlosframework/platform/internal/registry"
13)
14
15// prepHookBackend is fakeSwapBackend with hooks in the pinned wake's
16// preparation: Restore (under the wake's lease) and GenTier (the last step
17// before StartStaged).
18type prepHookBackend struct {
19 *fakeSwapBackend
20 onRestore func()
21 onTier func()
22}
23
24func (b *prepHookBackend) Restore(ctx context.Context, rt registry.Route) (bool, error) {
25 if b.onRestore != nil {
26 b.onRestore()
27 }
28 return b.fakeSwapBackend.Restore(ctx, rt)
29}
30
31func (b *prepHookBackend) GenTier(rt registry.Route, build string) cgroup.Limit {
32 if b.onTier != nil {
33 b.onTier()
34 }
35 return b.fakeSwapBackend.GenTier(rt, build)
36}
37
38// Final review F7: a hold's pinned wake checks the lease it actually holds,
39// and the attempt's checks, at the exec itself (immediately before
40// StartStaged), not before its preparation. A renewal that fails during
41// Restore or the rest of the preparation, or a fence set there, execs
42// nothing, and the attempt is abandoned (NoOwnership).
43func TestSwapHoldChecksItsLeaseAndAttemptAtTheExec(t *testing.T) {
44 t.Parallel()
45 lose := func(f *swapFixture) func() {
46 var once sync.Once
47 return func() {
48 once.Do(func() { f.b.lastRep().setErr(errors.New("lease renewal failed")) })
49 }
50 }
51 fence := func(f *swapFixture) func() {
52 var once sync.Once
53 return func() {
54 once.Do(func() {
55 f.a.mu.Lock()
56 if f.a.fences == nil {
57 f.a.fences = map[string]uint64{}
58 }
59 f.a.fences[f.host] = 9
60 f.a.mu.Unlock()
61 })
62 }
63 }
64 for _, tc := range []struct {
65 name string
66 arrange func(f *swapFixture, b *prepHookBackend)
67 want error
68 }{
69 {"the lease is lost during Restore", func(f *swapFixture, b *prepHookBackend) { b.onRestore = lose(f) }, errSwapLeaseLost},
70 {"the lease is lost during the preparation", func(f *swapFixture, b *prepHookBackend) { b.onTier = lose(f) }, errSwapLeaseLost},
71 {"a fence lands during the preparation", func(f *swapFixture, b *prepHookBackend) { b.onTier = fence(f) }, errSwapFenced},
72 } {
73 t.Run(tc.name, func(t *testing.T) {
74 f := newSwapFixture(t)
75 b := &prepHookBackend{fakeSwapBackend: f.b}
76 tc.arrange(f, b)
77 f.a.backend = b
78 res := f.a.Swap(context.Background(), f.attempt(ModeHold), f.newest)
79 if res.Outcome != SwapAbandoned || !strings.Contains(res.Reason, tc.want.Error()) {
80 t.Fatalf("Swap = %+v, want abandoned: %v", res, tc.want)
81 }
82 if gens, _, _ := f.b.calls(); gens != 0 {
83 t.Fatalf("StartStaged calls = %d: the pinned build was exec'd without its ownership", gens)
84 }
85 })
86 }
87}
88
89// Final review F10: a pinned wake's execution evidence is the spawn's
90// result, a returned process or an unproven-start sentinel, never having
91// entered StartStaged. A StartStaged that fails before its exec (a socket
92// directory, a token, a placement, cmd.Start) returns no process: nothing
93// of the build ran, so the hold is abandoned, not down. One that returns an
94// unproven-start sentinel, or a process that then fails its health check,
95// executed: the hold is down.
96func TestSwapHoldExecEvidenceIsTheSpawnResult(t *testing.T) {
97 t.Parallel()
98 for _, tc := range []struct {
99 name string
100 arrange func(f *swapFixture)
101 want SwapOutcome
102 }{
103 {"StartStaged fails before its exec", func(f *swapFixture) {
104 f.b.gmu.Lock()
105 f.b.genErr = errors.New("socket dir: permission denied")
106 f.b.gmu.Unlock()
107 }, SwapAbandoned},
108 {"StartStaged answers an unproven start", func(f *swapFixture) {
109 f.b.gmu.Lock()
110 f.b.genErr = fmt.Errorf("start g: %w", errGenUnproven)
111 f.b.gmu.Unlock()
112 }, SwapDown},
113 {"the build ran and failed its health check", func(f *swapFixture) {
114 f.a.genHealth = func(context.Context, Gen) error { return errors.New("unhealthy: HTTP 500") }
115 }, SwapDown},
116 } {
117 t.Run(tc.name, func(t *testing.T) {
118 f := newSwapFixture(t)
119 tc.arrange(f)
120 res := f.a.Swap(context.Background(), f.attempt(ModeHold), f.newest)
121 if res.Outcome != tc.want {
122 t.Fatalf("Swap = %+v, want %s", res, tc.want)
123 }
124 if gens, _, _ := f.b.calls(); gens != 1 {
125 t.Fatalf("StartStaged calls = %d, want 1", gens)
126 }
127 })
128 }
129}
130