carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostbox
2
3import (
4 "context"
5 "slices"
6 "time"
7
8 "github.com/carlosframework/platform/internal/hostclaim"
9 "github.com/carlosframework/platform/internal/hostguard"
10 "github.com/carlosframework/platform/internal/hostwrite"
11 "github.com/carlosframework/platform/internal/park"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15type RecordSelection struct {
16 Host, Key, ETag, Incarnation, Family string
17 Layout string
18 Paths []string
19 DryRun bool
20}
21type Reconciler struct {
22 PrepareInventory func(context.Context, RecordSelection, hostguard.Captured) error
23 Prepare func(context.Context, hostclaim.Observed) error
24 HomeRegion string
25 Claims release.Store
26 Guard hostguard.Guard
27 Local park.Local
28 Writes hostwrite.Config
29 Box, Region string
30 Now func() time.Time
31 // CheckRecord directly checks the selected record ETag and ownership acquisition.
32 CheckRecord func(context.Context, RecordSelection) error
33 // CheckLocal includes complete census and journal recovery before initialization.
34 CheckLocal func(context.Context, hostclaim.Observed, RecordSelection) error
35 // CheckHeld is the hold-only path's local check, in CheckLocal's place
36 // there: this box's own existing installation for the row it would
37 // hold, never the claim holder's stamp (Task 10b, Astra round 1).
38 // Production: EmptyInstallationCensus.CheckHeld, which
39 // ReconcileAdapter.Install sets from its census. nil refuses the hold.
40 CheckHeld func(context.Context, hostclaim.Observed, RecordSelection) error
41 // Adopt is Plan 5's #232 backfill coordinator, never a second adoption rule.
42 Adopt func(context.Context, RecordSelection) (hostclaim.Observed, error)
43 // FenceClosed reports whether this box's admission fence for the host
44 // is closed on the claim given (region HA §5.3: the source in
45 // releasing is ready until then). It is asked only where the row
46 // decision depends on it (rowDecision). It is called under the host lock
47 // and must not take it (hostguard.Guard.AdmissionFenced). nil: never
48 // closed, today's behaviour. An error keeps rows and mints none (C6).
49 // Production: AdmissionFenceClosed, region HA on only (Task 10b).
50 FenceClosed func(context.Context, hostclaim.Observed) (bool, error)
51 // HoldRow keeps an existing row held for an operation without
52 // installation authority (ruling 17). Production: RegistryHoldRow,
53 // region HA on only (Task 10b).
54 HoldRow func(ctx context.Context, host, holdOp string) error
55}
56
57// relocationAware is region HA on: Task 10b sets FenceClosed and HoldRow
58// only with CARLOS_REGION_HA (host's relocationSourceRows). With neither,
59// reconcile is today's: the serving executor alone (Serving), and no
60// hold-only path, the completed relocation's catch-up hold included.
61func (r Reconciler) relocationAware() bool { return r.FenceClosed != nil || r.HoldRow != nil }
62
63// relocates is the row decision's scope: a relocating claim with region HA
64// on. Every other operation, moving included, keeps main's path (Serving),
65// HA on or off: moves owns its holds, in provision's pass. The one
66// exception is a serving claim's completed relocation, whose catch-up hold
67// (completedHold) is decided apart from this scope.
68func (r Reconciler) relocates(c *hostclaim.Claim) bool {
69 return r.relocationAware() && c != nil && c.Op.Kind == hostclaim.KindRelocating
70}
71
72// completedHold is the catch-up hold (L18 joint round, finding 3): the
73// source's fence closed in releasing, but its next reconcile ran only once
74// the target had completed, so the claim serves on another box and
75// rowDecision's scope (relocates) no longer reaches it, and main's Serving
76// would refuse with the row left ready, which the edge serves, until the
77// park-local retires it. Completion waits on no acknowledgement of the
78// source's hold (ruling 80), so reconcile catches up here: the row is held
79// for the completed op, a RowRetiring hold, until the park-local retires
80// it (Task 10b decision 3: nothing un-holds).
81//
82// In scope only with region HA on, and only on positive evidence on the
83// fresh claim: a serving op on another box (not this one, which serves
84// through Serving) that journals a completed relocation (not completed on
85// this box) and a park-local this box owes for exactly that op, which
86// CompleteRelocation queues for the source and every installed box but
87// the target. A box on lost is left to its park-local (decision 3). A
88// park-local for another op, or none, is out of scope and main's path
89// runs. A completion record that cannot be read is its error, never "no
90// completion": the row is kept and nothing is minted. The path is hold
91// only (Install, checkOn): no activation, no inventory, no execution.
92func (r Reconciler) completedHold(c *hostclaim.Claim) (RowDecision, bool, error) {
93 if !r.relocationAware() || c == nil || c.Holder == nil || r.Box == "" || c.Op.Kind != hostclaim.KindServing || c.Op.Exec == r.Box || slices.Contains(c.Lost, r.Box) {
94 return RowDecision{}, false, nil
95 }
96 done, ok, err := hostclaim.ReadCompletedRelocation(c)
97 if err != nil {
98 return RowDecision{}, false, err
99 }
100 if !ok || done.Stop.Box == r.Box {
101 return RowDecision{}, false, nil
102 }
103 for _, o := range c.Obligations {
104 if o.Kind == "park-local" && o.Box == r.Box && o.Op == done.Op {
105 return RowDecision{Case: RowRetiring, HoldOp: done.Op}, true, nil
106 }
107 }
108 return RowDecision{}, false, nil
109}
110
111func (r Reconciler) fenceClosed(ctx context.Context, obs hostclaim.Observed) (bool, error) {
112 if r.FenceClosed == nil {
113 return false, nil
114 }
115 return r.FenceClosed(ctx, obs)
116}
117
118// rowDecision is §5.3's decision for this box on a fresh read. The fence
119// observer is asked only when the decision depends on its answer, which
120// is the source in releasing alone: every other relocating box, a target
121// before it has a local record included, decides without it. closed is
122// the observer's answer, false when it was not asked.
123func (r Reconciler) rowDecision(ctx context.Context, obs hostclaim.Observed) (d RowDecision, closed bool, err error) {
124 open := RelocationRow(obs.Claim, r.Box, true, false)
125 shut := RelocationRow(obs.Claim, r.Box, true, true)
126 if open == shut {
127 return open, false, nil
128 }
129 closed, err = r.fenceClosed(ctx, obs)
130 if err != nil {
131 return RowDecision{}, false, err
132 }
133 if closed {
134 return shut, true, nil
135 }
136 return open, false, nil
137}
138
139type activationReader struct {
140 r Reconciler
141 selected RecordSelection
142}
143
144func (a activationReader) Read(ctx context.Context, host string) (hostclaim.Observed, error) {
145 if host != a.selected.Host {
146 return hostclaim.Observed{}, ErrChanged
147 }
148 obs, err := hostclaim.Read(ctx, a.r.Claims, host)
149 if err != nil {
150 return obs, err
151 }
152 if err := a.r.check(ctx, obs, a.selected); err != nil {
153 return hostclaim.Observed{}, err
154 }
155 return obs, nil
156}
157
158// decided is Install's one row decision for a relocating claim and the
159// path it chose: holdOnly is a kept row's hold write, with no execution.
160type decided struct {
161 row RowDecision
162 holdOnly bool
163}
164
165func (r Reconciler) check(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error {
166 return r.checkOn(ctx, obs, selected, nil)
167}
168
169// checkOn is check on the row decision dec when the caller already made it
170// (Install), or on its own fresh one (dec nil). The hold-only write is not
171// execution, so the region's quiet window does not gate it; every other
172// path, the activation reader's and inventory's included, is gated.
173func (r Reconciler) checkOn(ctx context.Context, obs hostclaim.Observed, selected RecordSelection, dec *decided) error {
174 if r.Claims == nil || r.CheckRecord == nil || r.CheckLocal == nil || r.Now == nil || r.Box == "" || r.Region == "" || selected.ETag == "" || obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil {
175 return ErrUnsupported
176 }
177 h := obs.Claim.Holder
178 if obs.Host != selected.Host || h.Key != selected.Key || h.Incarnation != selected.Incarnation || h.Family != selected.Family || h.Stamp == 0 {
179 return ErrChanged
180 }
181 holdOnly := false
182 caught, catchUp, err := r.completedHold(obs.Claim)
183 if err != nil {
184 return err
185 }
186 switch {
187 case catchUp:
188 // The catch-up hold is hold only, on Install's own decision: the
189 // activation reader, the inventory callback and any caller
190 // without that decision are refused before anything is written.
191 if dec == nil || !dec.holdOnly || dec.row != caught {
192 return ErrChanged
193 }
194 holdOnly = true
195 case !r.relocates(obs.Claim):
196 if err := Serving(obs.Claim, Selection{Box: r.Box}); err != nil {
197 return err
198 }
199 default:
200 d := RowDecision{}
201 if dec != nil {
202 d, holdOnly = dec.row, dec.holdOnly
203 } else {
204 var err error
205 if d, _, err = r.rowDecision(ctx, obs); err != nil {
206 return err
207 }
208 }
209 switch d.Case {
210 case RowReady, RowHeld:
211 default:
212 return ErrChanged
213 }
214 if !holdOnly {
215 // Claim Plan 11's policy gate admits no "reconcile" action under
216 // a relocating claim (R1: only op.exec's probe), so the
217 // activation path would end refused at work.With after it had
218 // activated the guard and written the inventory. Refuse here,
219 // before anything is written; the dry run, the activation reader
220 // and the inventory callback all agree. The hold-only write
221 // takes no policy and is unchanged.
222 return ErrChanged
223 }
224 }
225 if until := park.QuietUntil(obs.Claim.QuietAfter, r.HomeRegion, r.Region); !holdOnly && !until.IsZero() && !r.Now().After(until) {
226 return ErrChanged
227 }
228 if err := r.CheckRecord(ctx, selected); err != nil {
229 return err
230 }
231 if holdOnly {
232 // Holding is not installing (Task 10b, Astra round 1): the source
233 // at installing or later is held against its own installation,
234 // which CheckHeld checks; CheckLocal would ask for the holder's
235 // stamp, the target's, and refuse for good.
236 if r.CheckHeld == nil {
237 return ErrUnsupported
238 }
239 return r.CheckHeld(ctx, obs, selected)
240 }
241 return r.CheckLocal(ctx, obs, selected)
242}
243func (r Reconciler) Install(ctx context.Context, selected RecordSelection, write func() error) error {
244 return r.installWithWork(ctx, selected, func(work hostwrite.Work) error { return work.With(ctx, hostwrite.Identity{}, write) })
245}
246func (r Reconciler) installWithWork(ctx context.Context, selected RecordSelection, write func(hostwrite.Work) error) error {
247 if !r.Writes.Enabled {
248 if selected.DryRun {
249 return nil
250 }
251 work, err := r.Writes.Select(ctx, selected.Host, "reconcile")
252 if err != nil {
253 return err
254 }
255 return write(work)
256 }
257 if write == nil || r.Claims == nil {
258 return ErrUnsupported
259 }
260 obs, err := hostclaim.Read(ctx, r.Claims, selected.Host)
261 if err != nil {
262 return err
263 }
264 if obs.Absent {
265 if selected.DryRun {
266 return ErrUnsupported
267 }
268 if r.Adopt == nil {
269 return ErrUnsupported
270 }
271 obs, err = r.Adopt(ctx, selected)
272 if err != nil {
273 return err
274 }
275 }
276 var dec *decided
277 if r.relocates(obs.Claim) {
278 // One answer from the observer, for the check, the decision and
279 // the path, asked only when the decision depends on it.
280 d, closed, err := r.rowDecision(ctx, obs)
281 if err != nil {
282 return err
283 }
284 // No installation of ours to activate, or one whose admission
285 // fence is closed (the source in releasing, still op.exec, which
286 // With refuses): a kept row's hold only, under the flock. A
287 // target before installing has no row and mints none (ruling 17).
288 dec = &decided{row: d, holdOnly: d.Case == RowHeld && (obs.Claim.Op.Exec != r.Box || closed)}
289 } else if d, ok, err := r.completedHold(obs.Claim); err != nil {
290 return err
291 } else if ok {
292 // A completed relocation's catch-up hold: always hold only.
293 dec = &decided{row: d, holdOnly: true}
294 }
295 if err := r.checkOn(ctx, obs, selected, dec); err != nil {
296 return err
297 }
298 if selected.DryRun {
299 return nil
300 }
301 if dec != nil && dec.holdOnly {
302 return r.holdOnly(ctx, selected, dec.row)
303 }
304 if r.Prepare != nil {
305 if err := r.Prepare(ctx, obs); err != nil {
306 return err
307 }
308 }
309 // Activate's participant callback is not an admission validator for executors.
310 // The reader validates EVERY fresh read while Activate holds hostguard.
311 guard := r.Guard
312 guard.Claims = activationReader{r: r, selected: selected}
313 if err := guard.Activate(ctx, selected.Host, r.Box, nil); err != nil {
314 return err
315 }
316 captured, err := guard.Capture(selected.Host)
317 if err != nil {
318 return err
319 }
320 if r.PrepareInventory != nil {
321 if err := r.PrepareInventory(ctx, selected, captured); err != nil {
322 return err
323 }
324 }
325 selector, ok := r.Writes.Selector.(interface {
326 SelectAt(context.Context, string, string, hostguard.Captured) (hostwrite.Work, error)
327 })
328 if !ok {
329 return ErrUnsupported
330 }
331 work, err := selector.SelectAt(ctx, selected.Host, "reconcile", captured)
332 if err != nil {
333 return err
334 }
335 work = installationWork(work)
336 local := r.Local
337 local.Guard = guard
338 check := func(c *hostclaim.Claim) error {
339 return r.check(ctx, hostclaim.Observed{Host: selected.Host, Claim: c}, selected)
340 }
341 if err := local.RecordInventory(ctx, selected.Host, captured, selected.Layout, selected.Paths, check); err != nil {
342 return err
343 }
344 return write(work)
345}
346
347// holdOnly keeps an existing row held for the operation, with no
348// installation authority: the write is conditional on the row's observed
349// hold (registry.SetHoldIf, through RegistryHoldRow), under the host's
350// flock. The decision is remade on a fresh read under that lock.
351// ErrChanged when there is no row: nothing is minted.
352func (r Reconciler) holdOnly(ctx context.Context, selected RecordSelection, d RowDecision) error {
353 if r.HoldRow == nil {
354 return ErrUnsupported
355 }
356 return r.Guard.WithLocal(ctx, selected.Host, func() error {
357 fresh, err := hostclaim.Read(ctx, r.Claims, selected.Host)
358 if err != nil {
359 return err
360 }
361 if fresh.Absent || fresh.Claim == nil || fresh.Claim.Holder == nil || fresh.Claim.Holder.Incarnation != selected.Incarnation {
362 return ErrChanged
363 }
364 // The decision is remade in whichever scope the fresh read is in:
365 // the catch-up decision (RowRetiring) and rowDecision's held one
366 // (RowHeld) never compare equal, so a scope change is ErrChanged.
367 again, catchUp, err := r.completedHold(fresh.Claim)
368 switch {
369 case err != nil:
370 return err
371 case catchUp:
372 case r.relocates(fresh.Claim):
373 if again, _, err = r.rowDecision(ctx, fresh); err != nil {
374 return err
375 }
376 default:
377 return ErrChanged
378 }
379 if again != d {
380 return ErrChanged
381 }
382 if err := r.checkOn(ctx, fresh, selected, &decided{row: d, holdOnly: true}); err != nil {
383 return err
384 }
385 return r.HoldRow(ctx, selected.Host, d.HoldOp)
386 })
387}
388