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 "database/sql"
6 "errors"
7 "os"
8 "strings"
9
10 "github.com/carlosframework/platform/internal/controlsock"
11 "github.com/carlosframework/platform/internal/hostbox"
12 "github.com/carlosframework/platform/internal/hostclaim"
13 "github.com/carlosframework/platform/internal/hostguard"
14 "github.com/carlosframework/platform/internal/hostwrite"
15 "github.com/carlosframework/platform/internal/park"
16 "github.com/carlosframework/platform/internal/regionname"
17 "github.com/carlosframework/platform/internal/registry"
18)
19
20// claimStopLocal is this controller's verified identity when it asks for a stop.
21type claimStopLocal struct {
22 Actor hostbox.Actor
23 BootID string
24 ControllerID string
25 PID int
26 ProcessStart string
27}
28
29// claimLaunchOwner is the tuple that journaled a launch. PID and ProcessStart
30// are zero when no record was found; root then refuses the stop.
31type claimLaunchOwner struct {
32 BootID, ControllerID string
33 DirectoryRevision string
34 RosterRevision uint64
35 PID int
36 ProcessStart string
37}
38
39// local reports whether this very process wrote the record. The same IDs in a
40// restarted process are not local: its producers ended with the old process.
41func (o claimLaunchOwner) local(l claimStopLocal) bool {
42 if o.BootID != l.BootID || o.ControllerID != l.ControllerID {
43 return false
44 }
45 return (o.PID == 0 && o.ProcessStart == "") || (o.PID == l.PID && o.ProcessStart == l.ProcessStart)
46}
47
48func ownerOf(in hostbox.LaunchInventory) claimLaunchOwner {
49 return claimLaunchOwner{BootID: in.Target.BootID, ControllerID: in.Target.ControllerID, DirectoryRevision: in.Target.DirectoryRevision, RosterRevision: in.Target.RosterRevision, PID: in.ControllerPID, ProcessStart: in.ProcessStart}
50}
51
52// recoveryStopDrain refuses. A recovered controller's producers ended with it,
53// and this controller's producers are never offered as that controller's drain.
54type recoveryStopDrain struct{}
55
56func (recoveryStopDrain) DrainClaim(context.Context, StopRequest, string) (DrainAcknowledgement, error) {
57 return DrainAcknowledgement{}, ErrClaimStopIdentity
58}
59
60func (a ClaimAssembly) bindClaimWakeStop() {
61 a.Wake.Stop = func(ctx context.Context, captured hostclaim.Observed) error {
62 if err := a.stopCapturedSession(ctx, captured); err != nil {
63 return err
64 }
65 pending := a.Wake.get(captured.Host)
66 if pending != nil {
67 current, _ := pending.snapshot()
68 // Root has already stopped this exact session. Its pending
69 // checkpoint may have advanced since capture; either observation
70 // order identifies the same stopped lease, never a new session.
71 reaches := a.stopReaches()
72 if sameStopWriter(current, captured, reaches) || sameStopWriter(captured, current, reaches) {
73 return a.Wake.release(ctx, captured.Host, pending)
74 }
75 }
76 return nil
77 }
78 a.Wake.Retries.StopAttempt = a.stopRetryAttempt
79}
80
81func (a ClaimAssembly) localStopIdentity(ctx context.Context) (claimStopLocal, error) {
82 if a.stopLocal != nil {
83 return a.stopLocal(ctx)
84 }
85 if a.Controllers == nil {
86 return claimStopLocal{}, hostwrite.ErrUnavailable
87 }
88 actor, err := a.Identity.Verify(ctx)
89 if err != nil {
90 return claimStopLocal{}, err
91 }
92 controller, err := a.Controllers.VerifiedController(ctx, os.Getpid())
93 if err != nil {
94 return claimStopLocal{}, err
95 }
96 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
97 if err != nil {
98 return claimStopLocal{}, err
99 }
100 start, err := claimConstructionProcessStart()
101 if err != nil {
102 return claimStopLocal{}, err
103 }
104 local := claimStopLocal{Actor: actor, BootID: strings.TrimSpace(string(boot)), ControllerID: controller, PID: os.Getpid(), ProcessStart: start}
105 if local.BootID == "" || local.ControllerID == "" || local.ProcessStart == "" {
106 return claimStopLocal{}, hostwrite.ErrUnavailable
107 }
108 return local, nil
109}
110
111// stopRetryAttempt stops the exact tuple persisted in a serving retry attempt.
112// After a controller restart or reboot that tuple is not this controller's.
113// It is requested in recovery mode, and root proves its controller dead.
114func (a ClaimAssembly) stopRetryAttempt(ctx context.Context, o hostclaim.Observed, attempt hostclaim.WakeRetryAttempt) error {
115 if o.Claim == nil || o.Claim.Holder == nil || attempt.ID == "" || attempt.BootID == "" || attempt.ControllerID == "" {
116 return hostbox.ErrChanged
117 }
118 if a.Wake == nil || a.Launches == nil {
119 return hostwrite.ErrUnavailable
120 }
121 if attempt.Session == 0 {
122 // An unbound attempt cannot reach Prepare, so it never launched. Its
123 // holder session is stopped from that session's own launch records.
124 if a.Wake.Stop == nil {
125 return hostwrite.ErrUnavailable
126 }
127 return a.Wake.Stop(ctx, o)
128 }
129 h := o.Claim.Holder
130 if h.Session != attempt.Session {
131 return hostbox.ErrChanged
132 }
133 local, err := a.localStopIdentity(ctx)
134 if err != nil {
135 return err
136 }
137 if attempt.Box != local.Actor.Box || attempt.Enrollment != local.Actor.Enrollment {
138 return hostbox.ErrChanged
139 }
140 owner := claimLaunchOwner{BootID: attempt.BootID, ControllerID: attempt.ControllerID}
141 recorded, err := a.Launches.Read(ctx, hostguard.FenceTarget{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: attempt.Session, Box: attempt.Box, Enrollment: attempt.Enrollment, BootID: attempt.BootID, ControllerID: attempt.ControllerID})
142 switch {
143 case err == nil:
144 owner = ownerOf(recorded)
145 // This process already stopped and settled the attempt's own launch
146 // before failing it; fencing the session would end its later retries.
147 if owner.local(local) && attemptSettled(recorded, attempt) {
148 return nil
149 }
150 case errors.Is(err, sql.ErrNoRows):
151 // Keep the persisted tuple. Root refuses it without a launch record;
152 // this controller's tuple is never substituted.
153 default:
154 return err
155 }
156 _, err = a.collectCapturedStop(ctx, o, local, owner, false)
157 return err
158}
159
160func attemptSettled(in hostbox.LaunchInventory, attempt hostclaim.WakeRetryAttempt) bool {
161 for _, r := range in.Reservations {
162 if r.ID == attempt.ID+":main" && r.InvocationID == attempt.InvocationID && r.Inputs == attempt.LaunchInputs && r.State == "settled" {
163 return true
164 }
165 }
166 return false
167}
168
169// stopCapturedSession stops each journaled launch of the captured session. A
170// record left by an earlier controller or boot keeps its original tuple.
171func (a ClaimAssembly) stopCapturedSession(ctx context.Context, captured hostclaim.Observed) error {
172 if captured.Claim == nil || captured.Claim.Holder == nil {
173 return hostbox.ErrChanged
174 }
175 h := captured.Claim.Holder
176 local, err := a.localStopIdentity(ctx)
177 if err != nil {
178 return err
179 }
180 if a.stopReaches() != nil && claimInitialSourceRecovery(captured, local.Actor.Box) {
181 if err := (claimCrossBoxHandover{Guard: a.Guard, Evidence: a.Claims, Box: local.Actor.Box}).sourceRecoveryEvidence(ctx, captured); err != nil {
182 return err
183 }
184 }
185 if h.Session == 0 {
186 // No writer was ever admitted, so nothing was launched or journaled.
187 // Root's positive no-process witness (R4) is the stop: this
188 // controller's tuple, no drain.
189 _, err = a.collectCapturedStop(ctx, captured, local, claimLaunchOwner{BootID: local.BootID, ControllerID: local.ControllerID}, false)
190 return err
191 }
192 if a.Launches == nil {
193 return hostwrite.ErrUnavailable
194 }
195 launches, err := a.Launches.SessionLaunches(ctx, captured.Host, h.Incarnation, h.Stamp, h.Session, local.Actor.Box, local.Actor.Enrollment)
196 if err != nil {
197 return err
198 }
199 if len(launches) == 0 {
200 // Nothing is journaled. Request this controller's tuple; root refuses it
201 // unless its own records prove the stop. Absence is never emptiness.
202 _, err = a.collectCapturedStop(ctx, captured, local, claimLaunchOwner{BootID: local.BootID, ControllerID: local.ControllerID}, true)
203 return err
204 }
205 for _, in := range launches {
206 if _, err := a.collectCapturedStop(ctx, captured, local, ownerOf(in), true); err != nil {
207 return err
208 }
209 }
210 return nil
211}
212
213// collectRetirementStop stops the one launch tuple represented by a retiring
214// route. Retirement can carry only one fence receipt, so multiple journal
215// owners are ambiguous and must be refused before any of them is affected.
216func (a ClaimAssembly) collectRetirementStop(ctx context.Context, row registry.Route) (hostguard.FenceTarget, error) {
217 if a.Launches == nil {
218 return hostguard.FenceTarget{}, hostwrite.ErrUnavailable
219 }
220 local, err := a.localStopIdentity(ctx)
221 if err != nil {
222 return hostguard.FenceTarget{}, err
223 }
224 captured, err := hostclaim.Read(ctx, a.Claims, row.Host)
225 if err != nil {
226 return hostguard.FenceTarget{}, err
227 }
228 if captured.Claim == nil || captured.Claim.Holder == nil {
229 return hostguard.FenceTarget{}, hostbox.ErrChanged
230 }
231 claim, holder := captured.Claim, captured.Claim.Holder
232 if holder.Incarnation != row.ClaimIncarnation || holder.Stamp != row.ClaimStamp || claim.Op.Exec != local.Actor.Box {
233 return hostguard.FenceTarget{}, hostbox.ErrChanged
234 }
235 obligation := ""
236 for _, ob := range claim.Obligations {
237 if ob.Kind == "park-local" && ob.Box == local.Actor.Box && ob.Owner == "box:"+local.Actor.Box && ob.Op == claim.Op.ID {
238 if obligation != "" {
239 return hostguard.FenceTarget{}, hostbox.ErrChanged
240 }
241 obligation = ob.ID
242 }
243 }
244 if obligation == "" {
245 return hostguard.FenceTarget{}, hostbox.ErrChanged
246 }
247 launches, err := a.Launches.SessionLaunches(ctx, captured.Host, holder.Incarnation, holder.Stamp, holder.Session, local.Actor.Box, local.Actor.Enrollment)
248 if err != nil {
249 return hostguard.FenceTarget{}, err
250 }
251 if len(launches) != 1 {
252 return hostguard.FenceTarget{}, hostbox.ErrChanged
253 }
254 result, err := a.collectCapturedStop(ctx, captured, local, ownerOf(launches[0]), false)
255 if err != nil {
256 return hostguard.FenceTarget{}, err
257 }
258 if verifyRetirementReceipt(ctx, hostguard.FenceReceipt{Target: result.Target, Bytes: result.Bytes, EvidenceAt: result.EvidenceAt}) != nil {
259 return hostguard.FenceTarget{}, ErrClaimStopReceipt
260 }
261 return result.Target, nil
262}
263
264func (a ClaimAssembly) collectCapturedStop(ctx context.Context, captured hostclaim.Observed, local claimStopLocal, owner claimLaunchOwner, writerSession bool) (StopResult, error) {
265 if captured.Claim == nil || captured.Claim.Holder == nil {
266 return StopResult{}, hostbox.ErrChanged
267 }
268 zero := captured.Claim.Holder.Session == 0
269 if zero && !owner.local(local) {
270 // A session-zero witness is only ever this controller's own request.
271 return StopResult{}, ErrClaimStopIdentity
272 }
273 if owner.BootID == "" || owner.ControllerID == "" {
274 return StopResult{}, hostwrite.ErrUnavailable
275 }
276 observed, err := hostclaim.Read(ctx, a.Claims, captured.Host)
277 if err != nil {
278 return StopResult{}, err
279 }
280 if !sameStopWriter(captured, observed, a.stopReaches()) {
281 return StopResult{}, hostbox.ErrChanged
282 }
283 actor := local.Actor
284 claim, holder := observed.Claim, observed.Claim.Holder
285 home, err := park.VerifiedHome(actor.FleetHomeRegion, actor.HomeRegion, actor.Region, claim.Op.Intent["home_region"])
286 if err != nil {
287 return StopResult{}, hostbox.ErrChanged
288 }
289 obligation := ""
290 if id, ok := a.operationWriterStop(claim, actor); (writerSession || zero) && ok {
291 obligation = id
292 } else if claim.Op.Kind != hostclaim.KindServing {
293 for _, ob := range claim.Obligations {
294 park := ob.Kind == "park-local" && ob.Owner == "box:"+actor.Box
295 release := ob.Kind == "relocation-release" && ob.ID == "relocation-release" && ob.Park == nil && regionname.SameRegion(home, ob.Region, actor.Region) && strings.HasPrefix(ob.Owner, "region:") && regionname.SameRegion(home, strings.TrimPrefix(ob.Owner, "region:"), actor.Region)
296 if (park || release) && ob.Box == actor.Box && ob.Op == claim.Op.ID {
297 if obligation != "" {
298 return StopResult{}, hostbox.ErrChanged
299 }
300 obligation = ob.ID
301 }
302 }
303 if obligation == "" {
304 return StopResult{}, hostbox.ErrChanged
305 }
306 }
307 recovering := !owner.local(local)
308 directoryRevision := owner.DirectoryRevision
309 if directoryRevision == "" {
310 // No record was found. Root still requires exact journal evidence.
311 directoryRevision = actor.Binding
312 }
313 // The receipt numeral: the launch record's roster counter, or this
314 // actor's when no record (or a record from before roster binding) names one.
315 rosterRevision := owner.RosterRevision
316 if rosterRevision == 0 {
317 rosterRevision = actor.Revision
318 }
319 req := StopRequest{Region: actor.Region, Recover: recovering, Host: observed.Host, Incarnation: holder.Incarnation, InstallationStamp: holder.Stamp, WriterSession: holder.Session, OperationID: claim.Op.ID, Phase: claim.Op.Phase, ObligationID: obligation, Box: actor.Box, Enrollment: actor.Enrollment, BootID: owner.BootID, ControllerID: owner.ControllerID, ExpectedClaimETag: observed.ETag, ExpectedDirectoryRevision: directoryRevision, RosterRevision: rosterRevision}
320 target := hostguard.FenceTarget{Region: req.Region, Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp, Session: req.WriterSession, Op: req.OperationID, Phase: req.Phase, ObligationID: req.ObligationID, Box: req.Box}
321 if zero && claim.Op.Kind == hostclaim.KindServing {
322 // A never-admitted holder may be a new creator or an adopted legacy
323 // writer on this box. Root independently proves that no process remains.
324 if claim.Op.Exec != actor.Box || (holder.WriterBox != "" && holder.WriterBox != actor.Box) || len(holder.Admissions) != 0 {
325 return StopResult{}, hostbox.ErrChanged
326 }
327 } else if !hostguard.StopAuthorityMatches(claim, target) {
328 return StopResult{}, hostbox.ErrChanged
329 }
330 var drain StopDrainer = recoveryStopDrain{}
331 if !recovering && !zero {
332 producers, err := a.Wake.Producers(ctx, observed)
333 if err != nil {
334 return StopResult{}, err
335 }
336 drain = producers
337 }
338 var result StopResult
339 if a.collectStop != nil {
340 result, err = a.collectStop(ctx, req, drain)
341 } else {
342 result, err = CollectClaimStop(ctx, controlsock.Path, req, drain)
343 }
344 if err != nil {
345 return StopResult{}, err
346 }
347 return validateTransportStop(req, result)
348}
349
350// sameStopWriter reports whether b, observed at or after a, names a's writer
351// session for a stop or a lease release. reaches compares the phases; nil
352// requires the same phase, as main did.
353func sameStopWriter(a, b hostclaim.Observed, reaches func(kind, from, to string) bool) bool {
354 return claimSameWriter(a, b, reaches) && a.Claim.Holder.WriterBox == b.Claim.Holder.WriterBox && a.Claim.Holder.Location == b.Claim.Holder.Location
355}
356
357// stopReaches is the teardown's phase predicate once the moves C1 session
358// table is bound (Wake.Continue). Stopping a session grants it nothing, so it
359// is not M9's adjacency: a probe admitted at probing whose op reached
360// committed unseen is still stopped and released as itself (R1). A step back
361// never matches. Write authority keeps Wake.Continue.
362func (a ClaimAssembly) stopReaches() func(kind, from, to string) bool {
363 if a.Wake == nil || a.Wake.Continue == nil {
364 return nil
365 }
366 return hostclaim.SessionReaches
367}
368
369// operationWriterStop is the stop authority for this box's admitted operation
370// writer, at probing, probed or committed (claim Plan 9 D4): before a
371// restarted probe or a cold wake after the open is admitted, the predecessor
372// is that writer, and no park-local chore exists or is needed for it. The
373// request keeps the op and phase and names the writer session instead of a
374// chore; root's full stop and receipt are unchanged, and the cold handover
375// then finds that receipt for the predecessor session. Bound only with the
376// moves C1 integration (Wake.Continue: CARLOS_HOST_CLAIM and
377// CARLOS_MOVES_PHASE1); otherwise main's park-local requirement stands.
378func (a ClaimAssembly) operationWriterStop(c *hostclaim.Claim, actor hostbox.Actor) (string, bool) {
379 if a.stopReaches() == nil {
380 return "", false
381 }
382 if c != nil && c.Op.Kind == hostclaim.KindRelocating {
383 return hostguard.RelocationWriterStop(c, actor.Box, actor.Region)
384 }
385 if c != nil && claimRecoveryKind(c.Op.Kind) {
386 return hostguard.RecoveryWriterStop(c, actor.Box, actor.Region)
387 }
388 return hostguard.LandingWriterStop(c, actor.Box, actor.Region)
389}
390