carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "net"
7 "os"
8 "path/filepath"
9 "strconv"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/cgroup"
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/hostclaim"
17 "github.com/carlosframework/platform/internal/hostguard"
18 "github.com/carlosframework/platform/internal/hostrelocate"
19 "github.com/carlosframework/platform/internal/provision"
20)
21
22// Finding 14: every journal field has its own JSON name and survives a
23// round trip; a journal for another host or op is refused.
24func TestRelocationAttemptJournalRoundTrips(t *testing.T) {
25 dd := t.TempDir()
26 j, err := readRelocationAttempts(dd, "shop.example", "op1")
27 if err != nil {
28 t.Fatal(err)
29 }
30 j.Opened["1"] = time.Date(2026, 10, 10, 9, 0, 0, 0, time.UTC)
31 j.Sessions["7"] = 1
32 j.Starts["1"] = relocationStart{Session: 7, Generation: "P1", Stamp: 9, State: probeReserved, Attempt: hostclaim.WakeRetryAttempt{ID: "a", InvocationID: "b", Session: 7, Box: "dst"}}
33 if err := j.write(dd); err != nil {
34 t.Fatal(err)
35 }
36 raw, _ := os.ReadFile(filepath.Join(dd, "relocate", "ops", "op1", "attempts.json"))
37 for _, key := range []string{`"host":"shop.example"`, `"op":"op1"`} {
38 if !strings.Contains(string(raw), key) {
39 t.Fatalf("%s missing from %s", key, raw)
40 }
41 }
42 got, err := readRelocationAttempts(dd, "shop.example", "op1")
43 if err != nil || got.Starts["1"] != j.Starts["1"] || got.Sessions["7"] != 1 || !got.Opened["1"].Equal(j.Opened["1"]) {
44 t.Fatalf("round trip: %+v %v", got, err)
45 }
46 if _, err := readRelocationAttempts(dd, "other.example", "op1"); !errors.Is(err, hostbox.ErrChanged) {
47 t.Fatalf("another host's journal was read: %v", err)
48 }
49}
50
51// Finding 7: root's reader returns the reserved start only for the exact
52// op at probing, the executor's attempt k, session, generation and stamp.
53func TestRootReadsTheRelocationAttemptJournal(t *testing.T) {
54 dd := t.TempDir()
55 j, _ := readRelocationAttempts(dd, "shop.example", "op1")
56 j.Sessions["7"] = 1
57 attempt := hostclaim.WakeRetryAttempt{ID: "a", InvocationID: "b", Session: 7, Box: "dst"}
58 j.Starts["1"] = relocationStart{Session: 7, Generation: "P1", Stamp: 9, State: probeReserved, Attempt: attempt}
59 if err := j.write(dd); err != nil {
60 t.Fatal(err)
61 }
62 claim := func() *hostclaim.Claim {
63 return &hostclaim.Claim{Host: "shop.example", Holder: &hostclaim.Holder{Incarnation: "inc", Stamp: 9, Session: 7, Generation: "P1", WriterBox: "dst"},
64 Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "op1", Phase: hostclaim.PhaseRelocatingProbing, Exec: "dst", Journal: map[string]string{"install_stamp": "9", "prepared": "P1", "probe_attempts.dst": "1"}}}
65 }
66 r := relocationAttemptReader{DataDir: dd}
67 got, err := r.ReadAttempt(context.Background(), hostclaim.Observed{Host: "shop.example", Claim: claim()})
68 if err != nil || got != attempt {
69 t.Fatalf("read %+v %v", got, err)
70 }
71 // Astra round 2 finding 4: a later cold attempt runs at its own
72 // admission's stamp; the reader takes the journaled start at that
73 // stamp (root's claimLaunchAuthority pin is claim ask C11).
74 j2, _ := readRelocationAttempts(dd, "shop.example", "op1")
75 j2.Sessions["14"] = 2
76 later := hostclaim.WakeRetryAttempt{ID: "c", InvocationID: "d", Session: 14, Box: "dst"}
77 j2.Starts["2"] = relocationStart{Session: 14, Generation: "g2", Stamp: 14, State: probeReserved, Attempt: later}
78 if err := j2.write(dd); err != nil {
79 t.Fatal(err)
80 }
81 second := claim()
82 second.Holder.Stamp, second.Holder.Session, second.Holder.Generation = 14, 14, "g2"
83 second.Op.Journal["probe_attempts.dst"] = "2"
84 second.Op.Journal["prepared_opened"] = "P1"
85 if got, err := r.ReadAttempt(context.Background(), hostclaim.Observed{Host: "shop.example", Claim: second}); err != nil || got != later {
86 t.Fatalf("second attempt: %+v %v", got, err)
87 }
88 for name, change := range map[string]func(*hostclaim.Claim){
89 "another session": func(c *hostclaim.Claim) { c.Holder.Session = 8 },
90 "another generation": func(c *hostclaim.Claim) { c.Holder.Generation = "g2" },
91 "another stamp": func(c *hostclaim.Claim) { c.Holder.Stamp = 10; c.Op.Journal["install_stamp"] = "10" },
92 "P at a stamp not the install stamp": func(c *hostclaim.Claim) { c.Op.Journal["install_stamp"] = "8" },
93 "a later attempt": func(c *hostclaim.Claim) { c.Op.Journal["probe_attempts.dst"] = "2" },
94 "installed": func(c *hostclaim.Claim) { c.Op.Phase = hostclaim.PhaseInstalled },
95 "stuck": func(c *hostclaim.Claim) { c.Op.Journal["stuck"] = "x" },
96 } {
97 c := claim()
98 change(c)
99 if _, err := r.ReadAttempt(context.Background(), hostclaim.Observed{Host: "shop.example", Claim: c}); err == nil {
100 t.Fatalf("%s: read", name)
101 }
102 }
103}
104
105// Finding 4: a relocation target's route is the nested layout, and the
106// files and env paths are the instance directory's.
107func TestRelocationRouteIsNestedAndPathsAgree(t *testing.T) {
108 dd := "/data"
109 rt := provision.RouteFor(provision.Record{Host: "shop.example", App: "shop", Account: "acct", Channel: "stable"}, dd, "", true, nil)
110 if rt.DBPath != provision.NestedDBPath(dd, "shop.example") || rt.Socket != provision.NestedSocket("shop.example") {
111 t.Fatalf("not nested: %+v", rt)
112 }
113 p := relocationPathsFor(dd, rt)
114 dir := provision.InstanceDir(dd, "shop.example")
115 if p.FilesDir != filepath.Join(dir, "shop.example") || p.EnvPath != filepath.Join(dir, "shop.example.env") {
116 t.Fatalf("paths %+v", p)
117 }
118 if _, err := claimIsolatedPaths(map[string]string{"data": dd}, "shop.example", ClaimLaunchInputs{DBPath: rt.DBPath, Limit: cgroup.Limit{MemoryMax: cgroup.TierDefault.MemoryMax}}); err != nil {
119 t.Fatalf("the isolated launch refuses the minted route: %v", err)
120 }
121}
122
123// Finding 7, ruling 48: root installs the relocation attempt reader only
124// with region HA on; its launch capability answers for kind relocating
125// only then.
126func TestRootInstallsTheRelocationReaderOnlyWithRegionHA(t *testing.T) {
127 journal, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db"))
128 if err != nil {
129 t.Fatal(err)
130 }
131 defer journal.Close()
132 inner := stopKernelFunc(func(context.Context, hostguard.FenceTarget, StopInventory, DrainAcknowledgement) (StopKernelEvidence, error) {
133 return StopKernelEvidence{}, nil
134 })
135 root := ClaimRootHTTP{Launches: journal, Stop: ClaimStopHTTP{Authenticate: func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, nil },
136 Coordinator: ClaimStopCoordinator{Kernel: inner, Guard: hostguard.Guard{DataDir: "/data", Claims: &launchClaims{}}}}}
137 actor := func(context.Context) (hostbox.Actor, error) { return hostbox.Actor{}, nil }
138 saved := claimLaunchEnv
139 t.Cleanup(func() { claimLaunchEnv = saved })
140 q := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchCapability, Kind: hostclaim.KindRelocating}
141 for _, on := range []bool{true, false} {
142 value := ""
143 if on {
144 value = "1"
145 }
146 claimLaunchEnv = func(k string) string {
147 if k == "CARLOS_REGION_HA" {
148 return value
149 }
150 return ""
151 }
152 wrapped, err := claimLaunchFor(root, actor, map[string]string{"opt": "/opt/carlos", "data": "/data"})
153 if err != nil {
154 t.Fatal(err)
155 }
156 reader, installed := wrapped.Launch.Attempts[hostclaim.KindRelocating].(relocationAttemptReader)
157 if installed != on || (on && reader.DataDir != "/data") || (!on && wrapped.Launch.Attempts != nil) {
158 t.Fatalf("region HA %v: attempts %+v", on, wrapped.Launch.Attempts)
159 }
160 f := newLaunchFixture(t, servingOp())
161 f.root.Attempts = wrapped.Launch.Attempts
162 _, err = f.root.Serve(t.Context(), f.peer, q)
163 if on && err != nil {
164 t.Fatalf("the relocating capability with region HA on: %v", err)
165 }
166 if !on && !errors.Is(err, hostbox.ErrUnsupported) {
167 t.Fatalf("the relocating capability with region HA off: %v", err)
168 }
169 }
170}
171
172// Absence is never evidence: root's reader answers nothing for an
173// operation whose journal is missing, unreadable or malformed, and the
174// journal's own settles and restarts refuse once it is gone. (A fresh
175// journal for a missing file is the writer's first state; the reader
176// refuses it because no session is bound, and this pins that direction.)
177func TestRootReaderRefusesAMissingOrUnreadableJournal(t *testing.T) {
178 ctx := context.Background()
179 claim := func() *hostclaim.Claim {
180 return &hostclaim.Claim{Host: "shop.example", Holder: &hostclaim.Holder{Incarnation: "inc", Stamp: 9, Session: 7, Generation: "P1", WriterBox: "dst"},
181 Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "op1", Phase: hostclaim.PhaseRelocatingProbing, Exec: "dst", Journal: map[string]string{"install_stamp": "9", "prepared": "P1", "probe_attempts.dst": "1"}}}
182 }
183 dd := t.TempDir()
184 r := relocationAttemptReader{DataDir: dd}
185 o := hostclaim.Observed{Host: "shop.example", Claim: claim()}
186 if _, err := r.ReadAttempt(ctx, o); !errors.Is(err, hostbox.ErrChanged) {
187 t.Fatalf("no journal: %v", err)
188 }
189 path, _ := relocationAttemptsPath(dd, "op1")
190 if err := os.MkdirAll(path, 0o700); err != nil {
191 t.Fatal(err)
192 }
193 if _, err := r.ReadAttempt(ctx, o); err == nil {
194 t.Fatal("a journal that cannot be read answered")
195 }
196 if err := os.Remove(path); err != nil {
197 t.Fatal(err)
198 }
199 if err := os.WriteFile(path, []byte(`{"v":1,"host":"shop.example","op":"op1"`), 0o600); err != nil {
200 t.Fatal(err)
201 }
202 if _, err := r.ReadAttempt(ctx, o); !errors.Is(err, hostbox.ErrChanged) {
203 t.Fatalf("a malformed journal: %v", err)
204 }
205 // A journal with its maps missing is malformed too, not empty.
206 if err := os.WriteFile(path, []byte(`{"v":1,"host":"shop.example","op":"op1"}`), 0o600); err != nil {
207 t.Fatal(err)
208 }
209 if _, err := readRelocationAttempts(dd, "shop.example", "op1"); !errors.Is(err, hostbox.ErrChanged) {
210 t.Fatalf("a journal without its maps: %v", err)
211 }
212}
213
214// Root takes a start only when the journal vouches for it twice: the
215// holder's session is bound to the record attempt k, and k's current
216// start is still reserved. A well-formed start under an unbound session,
217// a session bound to another attempt, or a start already settled failed
218// or healthy, answers nothing.
219func TestRootReaderNeedsABoundReservedStart(t *testing.T) {
220 ctx := context.Background()
221 attempt := hostclaim.WakeRetryAttempt{ID: "a", InvocationID: "b", Session: 7, Box: "dst"}
222 claim := &hostclaim.Claim{Host: "shop.example", Holder: &hostclaim.Holder{Incarnation: "inc", Stamp: 9, Session: 7, Generation: "P1", WriterBox: "dst"},
223 Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "op1", Phase: hostclaim.PhaseRelocatingProbing, Exec: "dst", Journal: map[string]string{"install_stamp": "9", "prepared": "P1", "probe_attempts.dst": "1"}}}
224 o := hostclaim.Observed{Host: "shop.example", Claim: claim}
225 for name, shape := range map[string]func(*relocationAttemptJournal){
226 "unbound session": func(j *relocationAttemptJournal) { delete(j.Sessions, "7") },
227 "session bound to another attempt": func(j *relocationAttemptJournal) { j.Sessions["7"] = 2 },
228 "a failed start": func(j *relocationAttemptJournal) { s := j.Starts["1"]; s.State = probeFailed; j.Starts["1"] = s },
229 "a healthy start": func(j *relocationAttemptJournal) { s := j.Starts["1"]; s.State = probeHealthy; j.Starts["1"] = s },
230 "a start under another session": func(j *relocationAttemptJournal) { s := j.Starts["1"]; s.Session = 8; j.Starts["1"] = s },
231 "a start at another stamp": func(j *relocationAttemptJournal) { s := j.Starts["1"]; s.Stamp = 10; j.Starts["1"] = s },
232 "an attempt for another session": func(j *relocationAttemptJournal) { s := j.Starts["1"]; s.Attempt.Session = 8; j.Starts["1"] = s },
233 "an attempt for another box": func(j *relocationAttemptJournal) { s := j.Starts["1"]; s.Attempt.Box = "src"; j.Starts["1"] = s },
234 "a journal for another op": func(j *relocationAttemptJournal) { j.Op = "op2" },
235 } {
236 dd := t.TempDir()
237 j, _ := readRelocationAttempts(dd, "shop.example", "op1")
238 j.Sessions["7"] = 1
239 j.Starts["1"] = relocationStart{Session: 7, Generation: "P1", Stamp: 9, State: probeReserved, Attempt: attempt}
240 shape(&j)
241 if err := j.write(dd); err != nil {
242 t.Fatal(err)
243 }
244 if got, err := (relocationAttemptReader{DataDir: dd}).ReadAttempt(ctx, o); !errors.Is(err, hostbox.ErrChanged) {
245 t.Fatalf("%s: read %+v %v", name, got, err)
246 }
247 }
248 // The record attempt is root's claim read, bounded by MaxProbeAttempts.
249 dd := t.TempDir()
250 j, _ := readRelocationAttempts(dd, "shop.example", "op1")
251 j.Sessions["7"] = hostclaim.MaxProbeAttempts + 1
252 j.Starts[strconv.Itoa(hostclaim.MaxProbeAttempts+1)] = relocationStart{Session: 7, Generation: "P1", Stamp: 9, State: probeReserved, Attempt: attempt}
253 if err := j.write(dd); err != nil {
254 t.Fatal(err)
255 }
256 claim.Op.Journal["probe_attempts.dst"] = strconv.Itoa(hostclaim.MaxProbeAttempts + 1)
257 if _, err := (relocationAttemptReader{DataDir: dd}).ReadAttempt(ctx, o); !errors.Is(err, hostbox.ErrChanged) {
258 t.Fatalf("an attempt past the bound: %v", err)
259 }
260}
261
262// Final review A2: the passed probe is recovered from the journal only as
263// the healthy start of the executor's record attempt bound to the holder's
264// session, generation and stamp; any other start is no probe; a missing or
265// unreadable journal after P's opening is a refusal, never "no probe".
266func TestRelocationPassedProbeReadsTheHealthyStartForTheAdmittedSession(t *testing.T) {
267 ctx := context.Background()
268 dd := t.TempDir()
269 claim := func() *hostclaim.Claim {
270 return &hostclaim.Claim{Host: "shop.example", Holder: &hostclaim.Holder{Incarnation: "inc", Stamp: 9, Session: 7, Generation: "P1", WriterBox: "dst"},
271 Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "op1", Phase: hostclaim.PhaseRelocatingProbing, Exec: "dst",
272 Journal: map[string]string{"install_stamp": "9", "prepared": "P1", "prepared_opened": "P1", "probe_attempts.dst": "1"}}}
273 }
274 passed := relocationPassedProbe(dd)
275 if _, _, err := passed(ctx, hostclaim.Observed{Host: "shop.example", Claim: claim()}); !errors.Is(err, hostrelocate.ErrRefused) {
276 t.Fatalf("a missing journal after P's opening: %v", err)
277 }
278 write := func(state string, change func(*relocationStart)) {
279 t.Helper()
280 j, _ := readRelocationAttempts(dd, "shop.example", "op1")
281 j.Sessions["7"] = 1
282 st := relocationStart{Session: 7, Generation: "P1", Stamp: 9, State: state, Attempt: hostclaim.WakeRetryAttempt{ID: "a", InvocationID: "b", Session: 7, Box: "dst"}}
283 if change != nil {
284 change(&st)
285 }
286 j.Starts["1"] = st
287 if err := j.write(dd); err != nil {
288 t.Fatal(err)
289 }
290 }
291 write(probeHealthy, nil)
292 if s, ok, err := passed(ctx, hostclaim.Observed{Host: "shop.example", Claim: claim()}); err != nil || !ok || s != 7 {
293 t.Fatalf("the healthy start: %d %v %v", s, ok, err)
294 }
295 for name, tc := range map[string]struct {
296 state string
297 change func(*relocationStart)
298 claim func(*hostclaim.Claim)
299 }{
300 "reserved": {probeReserved, nil, nil},
301 "failed": {probeFailed, nil, nil},
302 "another session": {probeHealthy, func(s *relocationStart) { s.Session = 8; s.Attempt.Session = 8 }, nil},
303 "another generation": {probeHealthy, func(s *relocationStart) { s.Generation = "g2" }, nil},
304 "another stamp": {probeHealthy, func(s *relocationStart) { s.Stamp = 10 }, nil},
305 "another box's attempt": {probeHealthy, func(s *relocationStart) { s.Attempt.Box = "src" }, nil},
306 "a later record attempt": {probeHealthy, nil, func(c *hostclaim.Claim) { c.Op.Journal["probe_attempts.dst"] = "2" }},
307 } {
308 write(tc.state, tc.change)
309 c := claim()
310 if tc.claim != nil {
311 tc.claim(c)
312 }
313 if s, ok, err := passed(ctx, hostclaim.Observed{Host: "shop.example", Claim: c}); err != nil || ok || s != 0 {
314 t.Fatalf("%s: %d %v %v", name, s, ok, err)
315 }
316 }
317 for name, change := range map[string]func(*hostclaim.Claim){
318 "P not opened": func(c *hostclaim.Claim) { delete(c.Op.Journal, "prepared_opened") },
319 "installed": func(c *hostclaim.Claim) { c.Op.Phase = hostclaim.PhaseInstalled },
320 "another writer": func(c *hostclaim.Claim) { c.Holder.WriterBox = "src" },
321 "no attempt count": func(c *hostclaim.Claim) { delete(c.Op.Journal, "probe_attempts.dst") },
322 } {
323 write(probeHealthy, nil)
324 c := claim()
325 change(c)
326 if _, ok, err := passed(ctx, hostclaim.Observed{Host: "shop.example", Claim: c}); err == nil || ok {
327 t.Fatalf("%s: %v %v", name, ok, err)
328 }
329 }
330 path, _ := relocationAttemptsPath(dd, "op1")
331 if err := os.WriteFile(path, []byte("{"), 0o600); err != nil {
332 t.Fatal(err)
333 }
334 if _, ok, err := passed(ctx, hostclaim.Observed{Host: "shop.example", Claim: claim()}); err == nil || ok {
335 t.Fatalf("an unreadable journal: %v %v", ok, err)
336 }
337}
338