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 host
4
5import (
6 "context"
7 "crypto/sha256"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "strconv"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/hostclaim"
17 execlease "github.com/carlosframework/platform/internal/hostexec"
18 "github.com/carlosframework/platform/internal/hostguard"
19)
20
21// probeStopped moves the participation box's relocation to a passed and
22// stopped probe: dst's session 12 admitted into P1 at the install stamp,
23// the guard activated there, and root's writer-session stop of session 12
24// completed with its receipt in the guard journal (ruling 49). It returns
25// the probing observation, the stop's fence target and root's receipt.
26func (b *participationBox) probeStopped() (hostclaim.Observed, hostguard.FenceTarget, []byte) {
27 b.t.Helper()
28 ctx := b.t.Context()
29 probing := b.update(func(c *hostclaim.Claim) error {
30 stamp := c.Gen + 1
31 c.Holder.Stamp, c.Holder.Session, c.Holder.Generation, c.Holder.WriterBox = stamp, 12, "P1", "dst"
32 c.Holder.Admissions = append(c.Holder.Admissions, hostclaim.AdmissionRecord{Session: 12, Box: "dst", Generation: "P1", Predecessor: "g1", Mode: "cold", Region: "ie"})
33 probe, _ := json.Marshal(hostclaim.Probe{Box: "dst", Session: 12, At: time.Date(2026, 10, 7, 3, 0, 0, 0, time.UTC)})
34 c.Op.Phase = hostclaim.PhaseRelocatingProbing
35 c.Op.Journal = map[string]string{"install_stamp": strconv.FormatUint(stamp, 10), "prepared": "P1", "prepared_opened": "P1", "probe": string(probe)}
36 return nil
37 })
38 if err := b.guard.Activate(ctx, participationHost, "dst", nil); err != nil {
39 b.t.Fatal(err)
40 }
41 h := probing.Claim.Holder
42 target := hostguard.FenceTarget{FenceID: "fence-1", Region: "ie", Host: participationHost, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: 12, Op: "rel-1", Phase: hostclaim.PhaseRelocatingProbing,
43 ObligationID: hostguard.WriterStopObligation(12), Box: "dst", Enrollment: b.actor.Enrollment, BootID: "boot", ControllerID: "ctl", ClaimETag: probing.ETag, DirectoryRevision: "4", RosterRevision: 4}
44 // The probe's launch is in the box's launch journal, as the wake
45 // journals it: main's ordinary cleanup would stop that session again.
46 if err := b.launches.Prelaunch(ctx, hostbox.LaunchInventory{ControllerPID: 4101, ProcessStart: "start-1", Target: target,
47 Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/" + claimContainmentName(target), Device: 1, Inode: 12}}, NoState: true}); err != nil {
48 b.t.Fatal(err)
49 }
50 if _, err := b.guard.BeginFence(ctx, target); err != nil {
51 b.t.Fatal(err)
52 }
53 at := time.Date(2026, 10, 7, 3, 1, 0, 0, time.UTC)
54 receipt := mustMarshalStopReceipt(StopReceipt{V: 1, ID: "r1", Kind: StopReceiptStopped, Region: "ie", ClaimETag: probing.ETag, ObligationID: target.ObligationID, FenceID: "fence-1", Host: participationHost,
55 Incarnation: h.Incarnation, InstallationStamp: h.Stamp, WriterSession: 12, OperationID: "rel-1", Phase: hostclaim.PhaseRelocatingProbing, Box: "dst", Enrollment: b.actor.Enrollment,
56 BootID: "boot", ControllerID: "ctl", DirectoryRevision: testReceiptNumeral(target), Binding: "4", EvidenceAt: at})
57 if err := b.guard.CompleteFence(ctx, target, receipt, at, func(context.Context, hostguard.FenceTarget, []byte, time.Time) error { return nil }); err != nil {
58 b.t.Fatal(err)
59 }
60 return probing, target, receipt
61}
62
63// serve is CompleteRelocation's serving CAS as the box sees it: kind
64// serving on dst, the holder still the stopped probe session, and the
65// serving op recording the stop.
66func (b *participationBox) serve(receipt []byte) hostclaim.Observed {
67 b.t.Helper()
68 sum := sha256.Sum256(receipt)
69 return b.update(func(c *hostclaim.Claim) error {
70 done, _ := json.Marshal(hostclaim.CompletedRelocation{Op: "rel-1", Stop: hostclaim.ProbeStop{Box: "dst", Session: 12, Receipt: "fence-1", ReceiptSHA256: hex.EncodeToString(sum[:])},
71 Stamp: c.Holder.Stamp, Phase: hostclaim.PhaseRelocatingProbing, Generation: c.Holder.Generation})
72 c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "dst", Participants: []string{"dst"}, Journal: map[string]string{hostclaim.JournalCompleted: string(done)}}
73 return nil
74 })
75}
76
77func (b *participationBox) servingCheck(o hostclaim.Observed) error {
78 h := o.Claim.Holder
79 return b.p.Check(b.t.Context(), b.actor, o.Claim, hostbox.Selection{Host: participationHost, Box: "dst", Region: "ie", Captured: hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}})
80}
81
82// Rev 3 finding 5, rulings 56 and 66, C10: completion without an agent
83// restart. While the participation slot is open the serving Check denies
84// (the wake cannot admit); DischargeCompleted closes the slot through main's
85// Prepare, whose cleanup is C10's discharge by the slot's binding and the
86// probe's receipt (no stop asked of root), ClearStale removes the record,
87// the serving Check passes, and N6's handover finds the probe's receipt.
88func TestRelocationCompletionDischargesParticipationWithoutRestart(t *testing.T) {
89 t.Parallel()
90 ctx := t.Context()
91 b := newParticipationBox(t)
92 if err := b.p.Prepare(ctx, b.read()); err != nil { // acknowledged at installing
93 t.Fatal(err)
94 }
95 _, target, receipt := b.probeStopped()
96 serving := b.serve(receipt)
97 if err := b.servingCheck(serving); err == nil {
98 t.Fatal("the serving wake was admissible with the participation open")
99 }
100 if err := b.p.DischargeCompleted(ctx, serving); err != nil {
101 t.Fatalf("discharge: %v", err)
102 }
103 if b.stops != 0 {
104 t.Fatalf("the discharge asked root for %d stops", b.stops)
105 }
106 if len(b.p.slots) != 0 || len(b.records()) != 0 {
107 t.Fatalf("slots %d records %v", len(b.p.slots), b.records())
108 }
109 if err := b.servingCheck(b.read()); err != nil {
110 t.Fatalf("the serving Check after the discharge: %v", err)
111 }
112 h := relocationCompletedHandover{Guard: b.guard, Box: "dst"}
113 if err := h.Before(ctx, b.read()); err != nil {
114 t.Fatalf("N6 before admission: %v", err)
115 }
116 if r, err := h.receipt(ctx, b.read()); err != nil || r.Target != target {
117 t.Fatalf("N6's prior: %+v %v", r.Target, err)
118 }
119}
120
121// Rev 4 finding 3, ruling 66: the agent restarts after the serving CAS,
122// with the participation's .hostexec record left behind. The new process's
123// DischargeCompleted reaches clearUntracked, C10 discharges by the
124// record's binding (no stop asked, the probe's fence untouched),
125// ClearStale removes the record, and the first serving wake's handover
126// from the probe's receipt is accepted by the guard.
127func TestRelocationRestartAfterServingCASRecoversParticipation(t *testing.T) {
128 t.Parallel()
129 ctx := t.Context()
130 b := newParticipationBox(t)
131 if err := b.p.Prepare(ctx, b.read()); err != nil { // acknowledged at installing
132 t.Fatal(err)
133 }
134 slot := b.p.slots[participationHost]
135 _, target, receipt := b.probeStopped()
136 serving := b.serve(receipt)
137 // The crash: the process ends and its controller with it; the record
138 // stays. The restarted agent has no slot.
139 _ = slot.Controller.Close(ctx)
140 b.p.mu.Lock()
141 b.p.slots = map[string]*claimParticipation{}
142 b.p.mu.Unlock()
143 if len(b.records()) != 1 {
144 t.Fatalf("records after the crash: %v", b.records())
145 }
146 if err := b.p.DischargeCompleted(ctx, serving); err != nil {
147 t.Fatalf("discharge after the restart: %v", err)
148 }
149 if b.stops != 0 || len(b.records()) != 0 {
150 t.Fatalf("stops %d records %v", b.stops, b.records())
151 }
152 h := serving.Claim.Holder
153 got, err := b.guard.CompletedWriterStops(ctx, participationHost, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, "rel-1", "dst")
154 if err != nil || len(got) != 1 || got[0].Target != target {
155 t.Fatalf("the probe's receipt changed: %+v %v", got, err)
156 }
157 if err := b.servingCheck(b.read()); err != nil {
158 t.Fatalf("the serving Check: %v", err)
159 }
160 // The first serving wake: a cold admission at session and stamp 40;
161 // the guard's handover barrier over the probe's receipt is accepted.
162 admitted := b.update(func(c *hostclaim.Claim) error {
163 c.Holder.Stamp, c.Holder.Session, c.Holder.Generation = 40, 40, "g40"
164 c.Holder.Admissions = append(c.Holder.Admissions, hostclaim.AdmissionRecord{Session: 40, Box: "dst", Generation: "g40", Predecessor: "P1", Mode: "cold", Region: "ie"})
165 return nil
166 })
167 barrier := target
168 barrier.FenceID, barrier.Stamp, barrier.Session, barrier.Op, barrier.Phase, barrier.ObligationID, barrier.ClaimETag = "handover-1", 40, 40, "", "", "", admitted.ETag
169 if err := b.guard.BeginColdHandover(ctx, target, barrier, func(*hostclaim.Claim) error { return nil }); err != nil {
170 t.Fatalf("the serving handover from the probe's receipt: %v", err)
171 }
172}
173
174// Absence is never evidence: after the restart, a completion record that
175// is present but cannot be read is not "no completion". C10 ends the
176// cleanup with that error before anything else: main's cleanup, which
177// would ask root to stop the probe's journaled launch again (replacing
178// its fence), never runs, nothing is stopped, and the record stays for
179// the next pass. The probe's receipt is as it was.
180func TestRelocationRestartDischargeRefusesAnUnreadableCompletion(t *testing.T) {
181 t.Parallel()
182 ctx := t.Context()
183 b := newParticipationBox(t)
184 if err := b.p.Prepare(ctx, b.read()); err != nil { // acknowledged at installing
185 t.Fatal(err)
186 }
187 slot := b.p.slots[participationHost]
188 _, target, _ := b.probeStopped()
189 serving := b.update(func(c *hostclaim.Claim) error {
190 c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "dst", Participants: []string{"dst"}, Journal: map[string]string{hostclaim.JournalCompleted: "{not json"}}
191 return nil
192 })
193 _ = slot.Controller.Close(ctx)
194 b.p.mu.Lock()
195 b.p.slots = map[string]*claimParticipation{}
196 b.p.mu.Unlock()
197 if err := b.p.DischargeCompleted(ctx, serving); !errors.Is(err, hostclaim.ErrRelocation) {
198 t.Fatalf("an unreadable completion was not refused: %v", err)
199 }
200 if b.stops != 0 || len(b.records()) != 1 {
201 t.Fatalf("stops %d records %v", b.stops, b.records())
202 }
203 h := serving.Claim.Holder
204 got, err := b.guard.CompletedWriterStops(ctx, participationHost, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, "rel-1", "dst")
205 if err != nil || len(got) != 1 || got[0].Target != target {
206 t.Fatalf("the probe's receipt changed: %+v %v", got, err)
207 }
208}
209
210// C10's text: the discharge is by the persisted binding's exact op and
211// incarnation. A binding for another op, or another incarnation, never
212// discharges a completed relocation its participation was not part of:
213// C10 falls through and main's cleanup answers for that binding (here it
214// refuses: the box's fixture has no root to stop through), so the cleanup
215// never succeeds on its account. The participation's own binding does.
216func TestRelocationDischargeIsKeyedByTheBindingsOpAndIncarnation(t *testing.T) {
217 t.Parallel()
218 ctx := t.Context()
219 b := newParticipationBox(t)
220 if err := b.p.Prepare(ctx, b.read()); err != nil { // acknowledged at installing
221 t.Fatal(err)
222 }
223 slot := b.p.slots[participationHost]
224 _, _, receipt := b.probeStopped()
225 serving := b.serve(receipt)
226 _ = slot.Controller.Close(ctx)
227 own := slot.Config.Binding
228 for name, change := range map[string]func(*execlease.Binding){
229 "another op": func(x *execlease.Binding) { x.Op = "rel-0" },
230 "another incarnation": func(x *execlease.Binding) { x.Incarnation = "other" },
231 } {
232 binding := own
233 change(&binding)
234 if err := b.p.cleanup(ctx, serving, binding); err == nil {
235 t.Fatalf("%s: the completed relocation was discharged by a binding that is not its participation's", name)
236 }
237 }
238 if err := b.p.cleanup(ctx, serving, own); err != nil {
239 t.Fatalf("the participation's own binding: %v", err)
240 }
241 if b.stops != 0 {
242 t.Fatalf("root was asked for %d stops", b.stops)
243 }
244}
245