carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "errors"
6 "os"
7 "slices"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostclaim"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// These callbacks are authority providers, not optional best-effort hints.
16// Production construction waits for the decisions named in this plan.
17type Authority interface {
18 // SelectExecutor chooses the new host's executor once, before the
19 // create CAS (claim P5-D2, region HA §5.2). For a non-production
20 // authority, selected false means no choice at create: Attempt runs
21 // today's BeginCreate and Executor names the box at completion.
22 // Production is ExecutorChooser.Choose behind CARLOS_REGION_HA,
23 // consulted inside StartCreate's own selection; with it off, claim
24 // Plan 5's selectExecutor chooses as before.
25 SelectExecutor(context.Context, hostclaim.Observed, Record) (sel hostclaim.ExecutorSelection, selected bool, err error)
26 Create(context.Context, hostclaim.Observed, Record, string) error
27 Executor(context.Context, hostclaim.Observed, Record) (string, error)
28 StartDelete(context.Context, hostclaim.Observed, Record) error
29 FirstPhase(context.Context, hostclaim.Observed) error
30 Inventory(context.Context, hostclaim.Observed) ([]string, []string, error)
31 Adoption(context.Context, Record) (region, box string, otherRegions, otherBoxes []string, err error)
32 PurgeRetirement(context.Context, hostclaim.Observed) error
33}
34
35type Controller struct {
36 Store release.Store
37 // Control is the admission engine's typed writer (Task 7). nil in the
38 // C5-only slice; every admission/journal transition requires it.
39 Control *ControlWriter
40 Authority Authority
41 Now func() time.Time
42 NewID func() string
43}
44
45func (c Controller) ready() error {
46 if c.Store == nil || c.Authority == nil || c.Now == nil || c.NewID == nil {
47 return ErrContract
48 }
49 return nil
50}
51
52// Attempt consumes the claim observation taken BEFORE app registration.
53// Create's caller owns registration renewal/removal. A failed CAS is never retried here.
54func (c Controller) Attempt(ctx context.Context, before hostclaim.Observed, r Record, id, by string) (hostclaim.Observed, error) {
55 if err := c.ready(); err != nil {
56 return hostclaim.Observed{}, err
57 }
58 if before.Host != r.Holder.Host || r.Holder.CreatedAt.IsZero() || id == "" {
59 return hostclaim.Observed{}, ErrIdentity
60 }
61 // With region HA on, the executor is selected once, before the CAS,
62 // and recorded in it; off, the create is today's. The production
63 // authority makes that one selection inside StartCreate.
64 production, isProduction := c.Authority.(productionCreateAuthority)
65 var sel hostclaim.ExecutorSelection
66 var selected bool
67 if !isProduction {
68 var err error
69 if sel, selected, err = c.Authority.SelectExecutor(ctx, before, r); err != nil {
70 return hostclaim.Observed{}, err
71 }
72 }
73 // Last pre-CAS read of registration, open admission and purge reservation.
74 if err := c.Authority.Create(ctx, before, r, id); err != nil {
75 return hostclaim.Observed{}, err
76 }
77 var obs hostclaim.Observed
78 var err error
79 switch {
80 case isProduction:
81 obs, err = production.StartCreate(ctx, before, r, id, by)
82 case selected:
83 obs, err = hostclaim.Update(ctx, c.Store, before, hostclaim.BeginCreateOn(r.Holder, r.Body, id, by, c.Now(), sel))
84 default:
85 obs, err = hostclaim.Update(ctx, c.Store, before, hostclaim.BeginCreate(r.Holder, r.Body, id, by, c.Now()))
86 }
87 if err != nil {
88 return hostclaim.Observed{}, err
89 }
90 if isProduction {
91 if err := production.RecordEffects(ctx, obs, r); err != nil {
92 return obs, err
93 }
94 return c.Complete(ctx, obs.Host, id)
95 }
96 // No raw record effect precedes the durable claim intent.
97 if _, _, err = provision.WriteRecord(provision.WithRecordSequence(ctx), c.Store, r.Holder.Key, []byte(`{}`), r.Body, ""); err != nil {
98 if !errors.Is(err, release.ErrCASConflict) {
99 return obs, err
100 }
101 existing, rerr := ReadRecord(ctx, c.Store, r.Holder.Key)
102 if rerr != nil {
103 return obs, rerr
104 }
105 if !matches(existing, obs.Claim.Holder) {
106 removed, rerr := RemoveOrphan(ctx, c.Store, existing)
107 if rerr != nil {
108 return obs, rerr
109 }
110 if !removed {
111 return obs, ErrPending
112 }
113 if _, _, err = provision.WriteRecord(provision.WithRecordSequence(ctx), c.Store, r.Holder.Key, []byte(`{}`), r.Body, ""); err != nil {
114 return obs, err
115 }
116 }
117 }
118 return c.Complete(ctx, obs.Host, id)
119}
120
121func (c Controller) Complete(ctx context.Context, host, id string) (hostclaim.Observed, error) {
122 if err := c.ready(); err != nil {
123 return hostclaim.Observed{}, err
124 }
125 obs, err := hostclaim.Read(ctx, c.Store, host)
126 if err != nil {
127 return obs, err
128 }
129 if obs.Absent || obs.Claim.Op.Kind != hostclaim.KindCreating || obs.Claim.Op.ID != id {
130 return obs, ErrPending
131 }
132 r, err := ReadRecord(ctx, c.Store, obs.Claim.Holder.Key)
133 if err != nil {
134 return obs, err
135 }
136 if !matches(r, obs.Claim.Holder) {
137 return obs, ErrIdentity
138 }
139 box, err := c.Authority.Executor(ctx, obs, r)
140 if err != nil {
141 return obs, err
142 }
143 return hostclaim.Update(ctx, c.Store, obs, hostclaim.CompleteCreate(id, box))
144}
145
146func (c Controller) RecoverCreate(ctx context.Context, host string) error {
147 if err := c.ready(); err != nil {
148 return err
149 }
150 obs, err := hostclaim.Read(ctx, c.Store, host)
151 if err != nil {
152 return err
153 }
154 if obs.Absent || obs.Claim.Op.Kind != hostclaim.KindCreating {
155 return nil
156 }
157 if obs.Claim.Op.StartedAt.IsZero() || !c.Now().After(obs.Claim.Op.StartedAt.Add(5*time.Minute)) {
158 return ErrPending
159 }
160 r, err := ReadRecord(ctx, c.Store, obs.Claim.Holder.Key)
161 if errors.Is(err, os.ErrNotExist) {
162 if _, production := c.Authority.(productionCreateAuthority); production {
163 // The five-minute age is inspection eligibility. An absent record
164 // cannot settle a delayed ownership, store, or record effect.
165 return ErrPending
166 }
167 _, err = hostclaim.Update(ctx, c.Store, obs, hostclaim.CancelCreate(obs.Claim.Op.ID, c.Now()))
168 return err
169 }
170 if err != nil {
171 return err
172 }
173 if !matches(r, obs.Claim.Holder) {
174 return ErrPending
175 }
176 _, err = c.Complete(ctx, host, obs.Claim.Op.ID)
177 return err
178}
179
180// Adopt is the shared IO wrapper around the one hostclaim.Adopt transition.
181func (c Controller) Adopt(ctx context.Context, r Record) (hostclaim.Observed, error) {
182 if err := c.ready(); err != nil {
183 return hostclaim.Observed{}, err
184 }
185 obs, err := hostclaim.Read(ctx, c.Store, r.Holder.Host)
186 if err != nil {
187 return obs, err
188 }
189 if !obs.Absent {
190 if matches(r, obs.Claim.Holder) {
191 return obs, nil
192 }
193 if obs.Claim.Holder == nil || obs.Claim.Holder.Acquisition != "" || r.Holder.Acquisition == "" {
194 return obs, ErrIdentity
195 }
196 // #232 also repairs already-claimed legacy holders while globally
197 // frozen. Adoption authority verifies the exact manifest/ownership.
198 prior := *obs.Claim.Holder
199 prior.Acquisition = r.Holder.Acquisition
200 if !matches(r, &prior) {
201 return obs, ErrIdentity
202 }
203 _, box, _, _, err := c.Authority.Adoption(ctx, r)
204 if err != nil {
205 return obs, err
206 }
207 if obs.Claim.Op.Kind != hostclaim.KindServing || obs.Claim.Op.Exec != box || !slices.Contains(obs.Claim.Touched.Boxes, box) {
208 return obs, ErrIdentity
209 }
210 fresh, err := ReadRecord(ctx, c.Store, r.Holder.Key)
211 if err != nil {
212 return obs, err
213 }
214 if fresh.ETag != r.ETag {
215 return obs, ErrPending
216 }
217 return hostclaim.Update(ctx, c.Store, obs, func(next *hostclaim.Claim) error {
218 if next.Holder == nil || next.Holder.Acquisition != "" {
219 return ErrPending
220 }
221 next.Holder.Acquisition = r.Holder.Acquisition
222 return nil
223 })
224 }
225 region, box, regions, boxes, err := c.Authority.Adoption(ctx, r)
226 if err != nil {
227 return obs, err
228 }
229 // Re-read the exact record after discovery; never adopt a stale listing body.
230 fresh, err := ReadRecord(ctx, c.Store, r.Holder.Key)
231 if err != nil {
232 return obs, err
233 }
234 if fresh.ETag != r.ETag {
235 return obs, ErrPending
236 }
237 n := fresh.Holder
238 n.Region = region
239 return hostclaim.Update(ctx, c.Store, obs, hostclaim.Adopt(n, box, regions, boxes, c.NewID(), c.NewID))
240}
241
242// BeginRetire only starts the claim operation. Its successful return does not
243// mean the record is gone or the host is free; the caller must run/retry Retire.
244func (c Controller) BeginRetire(ctx context.Context, key, by, expectedInc string) (hostclaim.Observed, error) {
245 if err := c.ready(); err != nil {
246 return hostclaim.Observed{}, err
247 }
248 r, err := ReadRecord(ctx, c.Store, key)
249 if err != nil {
250 return hostclaim.Observed{}, err
251 }
252 inc, ierr := r.Incarnation()
253 if ierr != nil {
254 return hostclaim.Observed{}, ierr
255 }
256 if expectedInc == "" || inc != expectedInc {
257 return hostclaim.Observed{}, ErrIdentity
258 }
259 obs, err := hostclaim.Read(ctx, c.Store, r.Holder.Host)
260 if err != nil {
261 return obs, err
262 }
263 if obs.Absent {
264 return obs, ErrPending
265 }
266 if !matches(r, obs.Claim.Holder) {
267 return obs, ErrIdentity
268 }
269 if err := c.Authority.StartDelete(ctx, obs, r); err != nil {
270 return obs, err
271 }
272 regions, boxes, err := c.Authority.Inventory(ctx, obs)
273 if err != nil {
274 return obs, err
275 }
276 if production, ok := c.Authority.(interface {
277 StartDeleteClaim(context.Context, hostclaim.Observed, Record, string) (hostclaim.Observed, error)
278 }); ok {
279 return production.StartDeleteClaim(ctx, obs, r, by)
280 }
281 return hostclaim.Update(ctx, c.Store, obs, hostclaim.BeginDelete(c.NewID(), by, r.ETag, c.Now(), regions, boxes, ""))
282}
283
284func (c Controller) Retire(ctx context.Context, host, id string) error {
285 if err := c.ready(); err != nil {
286 return err
287 }
288 obs, err := hostclaim.Read(ctx, c.Store, host)
289 if err != nil {
290 return err
291 }
292 if obs.Absent || obs.Claim.Op.ID != id {
293 return ErrPending
294 }
295 op := obs.Claim.Op
296 if op.Kind != hostclaim.KindDeleting && op.Kind != hostclaim.KindPurging {
297 return ErrPending
298 }
299 if obs.Claim.Holder == nil || op.Intent["key"] != obs.Claim.Holder.Key || op.Intent["etag"] == "" {
300 return ErrIdentity
301 }
302 if err := c.Authority.FirstPhase(ctx, obs); err != nil {
303 return err
304 }
305 if op.Kind == hostclaim.KindPurging {
306 if err := c.Authority.PurgeRetirement(ctx, obs); err != nil {
307 return err
308 }
309 }
310 regions, boxes, err := c.Authority.Inventory(ctx, obs)
311 if err != nil {
312 return err
313 }
314 // Validate record identity before deleting even the stored version.
315 r, rerr := ReadRecord(ctx, c.Store, op.Intent["key"])
316 if rerr != nil && !errors.Is(rerr, os.ErrNotExist) {
317 return rerr
318 }
319 if rerr == nil {
320 if !matches(r, obs.Claim.Holder) {
321 return ErrIdentity
322 }
323 if r.ETag != op.Intent["etag"] {
324 if authority, ok := c.Authority.(interface {
325 RefreshDeleteRecord(context.Context, hostclaim.Observed, Record) error
326 }); ok {
327 return authority.RefreshDeleteRecord(ctx, obs, r)
328 }
329 return ErrPending
330 }
331 if err := release.DeleteIfMatch(ctx, c.Store, r.Holder.Key, r.ETag); err != nil {
332 return err
333 }
334 }
335 // A success/412 alone never certifies absence. Re-read before parking.
336 _, _, err = c.Store.Get(ctx, op.Intent["key"])
337 if !errors.Is(err, os.ErrNotExist) {
338 if err != nil {
339 return err
340 }
341 return ErrPending
342 }
343 // The old observation binds the entire sequence to this operation.
344 transition := hostclaim.ToParking(id, regions, boxes, c.NewID)
345 if _, production := c.Authority.(productionCreateAuthority); production {
346 transition = hostclaim.ToScopedParking(id, regions, boxes, c.NewID)
347 }
348 _, err = hostclaim.Update(ctx, c.Store, obs, transition)
349 if err != nil {
350 return err
351 }
352 return ErrPending
353}
354
355func (c Controller) Free(ctx context.Context, host, id string) error {
356 if err := c.ready(); err != nil {
357 return err
358 }
359 obs, err := hostclaim.Read(ctx, c.Store, host)
360 if err != nil {
361 return err
362 }
363 if obs.Absent {
364 return ErrPending
365 }
366 _, err = hostclaim.Update(ctx, c.Store, obs, hostclaim.FreeAfterPark(id))
367 return err
368}
369