carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package hostexec
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "github.com/carlosframework/platform/internal/hostfloor"
10 "os"
11 "path/filepath"
12 "testing"
13 "time"
14)
15
16func TestPlan9PendingRecovery(t *testing.T) {
17 for _, tc := range []struct {
18 name string
19 boot string
20 start string
21 probeErr error
22 state string
23 legacy bool
24 busy bool
25 ok bool
26 }{
27 {name: "dead", boot: "boot", probeErr: os.ErrNotExist, state: "pending", ok: true},
28 {name: "reused-pid", boot: "boot", start: "456", state: "pending", ok: true},
29 {name: "new-boot", boot: "next", start: "123", state: "pending", ok: true},
30 {name: "live", boot: "boot", start: "123", state: "pending"},
31 {name: "unreadable", boot: "boot", probeErr: os.ErrPermission, state: "pending"},
32 {name: "active", boot: "next", state: "active"},
33 {name: "legacy", boot: "next", state: "pending", legacy: true},
34 {name: "lock-held", boot: "next", state: "pending", busy: true},
35 } {
36 t.Run(tc.name, func(t *testing.T) {
37 cfg, _, _, _, _ := fixture(t)
38 d, err := openLocal(cfg.Guard.DataDir)
39 if err != nil {
40 t.Fatal(err)
41 }
42 defer d.close()
43 s := State{V: 1, Binding: cfg.Binding, State: tc.state, Expires: time.Now().Add(TTL), SafeUntil: 99, LeaseETag: "tag", Process: ControllerProcess{KernelBootID: "boot", PID: 123, Start: "123"}}
44 s.ControllerID = "dead-controller"
45 if tc.legacy {
46 s.Process = ControllerProcess{}
47 }
48 if err = d.write(s); err != nil {
49 t.Fatal(err)
50 }
51 if tc.busy {
52 unlock, e := tryAcquire(cfg.LockDir, s.Host)
53 if e != nil {
54 t.Fatal(e)
55 }
56 defer unlock()
57 }
58 archive := filepath.Join(t.TempDir(), "settled")
59 err = recoverPending(context.Background(), cfg.Guard, cfg.LockDir, archive, func() (string, error) { return tc.boot, nil }, func(int) (string, error) { return tc.start, tc.probeErr })
60 if tc.ok {
61 if err != nil {
62 t.Fatal(err)
63 }
64 if _, err = d.read(s.Host); !errors.Is(err, os.ErrNotExist) {
65 t.Fatalf("pending retained: %v", err)
66 }
67 entries, err := os.ReadDir(archive)
68 if err != nil || len(entries) != 1 {
69 t.Fatalf("missing durable evidence: %v %v", entries, err)
70 }
71 // Model the crash after archive fsync but before the pending unlink.
72 if err = d.write(s); err != nil {
73 t.Fatal(err)
74 }
75 if err = recoverPending(context.Background(), cfg.Guard, cfg.LockDir, archive, func() (string, error) { return tc.boot, nil }, func(int) (string, error) { return tc.start, tc.probeErr }); err != nil {
76 t.Fatal(err)
77 }
78 } else {
79 if err == nil {
80 t.Fatal("unproven pending controller settled")
81 }
82 got, e := d.read(s.Host)
83 if e != nil || got.ControllerID != s.ControllerID {
84 t.Fatalf("refusal changed record: %+v %v", got, e)
85 }
86 }
87 })
88 }
89}
90
91func TestPlan9PendingCapturesKernelIdentity(t *testing.T) {
92 p, err := currentControllerProcess()
93 if err != nil {
94 t.Fatal(err)
95 }
96 boot, err := BootID()
97 if err != nil {
98 t.Fatal(err)
99 }
100 start, err := controllerProcessStart(os.Getpid())
101 if err != nil {
102 t.Fatal(err)
103 }
104 if p.PID != os.Getpid() || p.Start != start || p.KernelBootID != boot {
105 t.Fatalf("wrong process: %+v", p)
106 }
107 if err := proveControllerDead(p, boot, controllerProcessStart); err == nil {
108 t.Fatal("current process considered dead")
109 }
110}
111
112func TestPlan9FailedOpenAbortsOnlyJoinedPending(t *testing.T) {
113 for _, failedCleanup := range []bool{false, true} {
114 t.Run(fmt.Sprint(failedCleanup), func(t *testing.T) {
115 cfg, _, _, _, _ := fixture(t)
116 cfg.Clock = SystemClock{} // Persisted JSON strips monotonic time metadata.
117 cfg.UpdateGuardDir = t.TempDir()
118 cfg.UpdateJournal = filepath.Join(t.TempDir(), "journal.json")
119 cfg.verifyRunning = func(context.Context) error { return hostfloor.ErrFloor }
120 if failedCleanup {
121 cfg.Cleanup = func(context.Context) error { return errors.New("cleanup uncertain") }
122 }
123 c, err := Open(t.Context(), cfg)
124 if c != nil || !errors.Is(err, hostfloor.ErrFloor) {
125 t.Fatal(c, err)
126 }
127 d, err := openLocal(cfg.Guard.DataDir)
128 if err != nil {
129 t.Fatal(err)
130 }
131 defer d.close()
132 state, err := d.read(cfg.Binding.Host)
133 if failedCleanup {
134 if err != nil || state.State != "pending" {
135 t.Fatal(state, err)
136 }
137 } else if !errors.Is(err, os.ErrNotExist) {
138 t.Fatalf("joined aborted Open retained live-process pending: %+v %v", state, err)
139 }
140 })
141 }
142}
143func TestPlan9PendingRecoveryDoesNotCreateLocks(t *testing.T) {
144 cfg, _, _, _, _ := fixture(t)
145 d, err := openLocal(cfg.Guard.DataDir)
146 if err != nil {
147 t.Fatal(err)
148 }
149 defer d.close()
150 s := State{V: 1, Binding: cfg.Binding, State: "pending", Expires: time.Now().Add(TTL), SafeUntil: 99, Process: ControllerProcess{KernelBootID: "old", PID: 123, Start: "123"}}
151 s.ControllerID = "dead"
152 if err = d.write(s); err != nil {
153 t.Fatal(err)
154 }
155 lockDir := filepath.Join(t.TempDir(), "absent")
156 if err = recoverPending(t.Context(), cfg.Guard, lockDir, filepath.Join(t.TempDir(), "settled"), func() (string, error) { return "next", nil }, controllerProcessStart); err != nil {
157 t.Fatal(err)
158 }
159 if _, err = os.Stat(lockDir); !errors.Is(err, os.ErrNotExist) {
160 t.Fatal("root created runtime locks", err)
161 }
162}
163