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 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "os"
9 "reflect"
10 "time"
11
12 "github.com/carlosframework/platform/internal/hostclaim"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// PurgeRequestKey is the box-written automatic purge request. The console
17// consumes it only through DiscardPurgeRequest or RemovePurgeJournal.
18func PurgeRequestKey(account, app string) (string, error) {
19 if _, err := AdmissionKey(account, app); err != nil {
20 return "", err
21 }
22 return "apps/" + account + "/" + app + "/purge-request.json", nil
23}
24
25func validNewJournal(j PurgeJournal) error {
26 if j.V != 1 || j.ID == "" || j.AppID == "" || j.Phase != "reserved" || j.Hosts == nil {
27 return ErrIdentity
28 }
29 if j.Tombstone.Key != release.AppTombstoneKey(j.Account, j.App) || j.Tombstone.ETag == "" || !json.Valid(j.Tombstone.Body) {
30 return ErrIdentity
31 }
32 return nil
33}
34
35// ReservePurge registers the purge reservation, then writes the journal
36// create-only. The journal is the durable intent BeginPurge transfers to the
37// exclusive operation; losing that CAS leaves only an admitted reservation
38// with no teardown authority.
39func (w *ControlWriter) ReservePurge(ctx context.Context, j PurgeJournal, owner RegistrationOwner, now time.Time) (string, error) {
40 if err := w.ready(); err != nil {
41 return "", err
42 }
43 if !validRegistrationOwner(owner) {
44 return "", ErrIdentity
45 }
46 key, err := PurgeKey(j.Account, j.App)
47 if err != nil {
48 return "", err
49 }
50 a, err := w.read(ctx, j.Account, j.App, j.AppID)
51 if err != nil {
52 return "", err
53 }
54 if j.AdmissionEpoch == 0 {
55 j.AdmissionEpoch = a.Value.Epoch
56 }
57 if err = validNewJournal(j); err != nil || j.AdmissionEpoch != a.Value.Epoch {
58 return "", ErrIdentity
59 }
60 if existing, tag, gerr := w.raw.Get(ctx, key); gerr == nil {
61 var seen struct {
62 ID string `json:"id"`
63 AppID string `json:"app_id"`
64 }
65 if json.Unmarshal(existing, &seen) == nil && seen.ID == j.ID && seen.AppID == j.AppID && tag != "" {
66 return tag, nil
67 }
68 // Another purge's journal (or a malformed one) holds the name.
69 return "", ErrPending
70 } else if !errors.Is(gerr, os.ErrNotExist) {
71 return "", gerr
72 }
73 body, tag, err := w.raw.Get(ctx, j.Tombstone.Key)
74 if err != nil {
75 return "", err
76 }
77 if tag != j.Tombstone.ETag || !bytes.Equal(body, j.Tombstone.Body) {
78 return "", ErrPending
79 }
80 if a.Value.State != "open" || a.Value.Op != nil {
81 return "", ErrPending
82 }
83 if _, ok := a.Value.Registrations[j.ID]; !ok {
84 reg := Registration{Kind: registrationPurge, Owner: owner, AppID: j.AppID, Epoch: a.Value.Epoch}
85 if a, err = w.updateAdmission(ctx, a, func(v *AppAdmission) error { return registerStart(v, j.ID, reg, now) }); err != nil {
86 return "", err
87 }
88 }
89 reg := a.Value.Registrations[j.ID]
90 if reg.Kind != registrationPurge || reg.Owner != owner || !RegistrationLive(reg, now) {
91 return "", ErrPending
92 }
93 raw, err := json.Marshal(j)
94 if err != nil {
95 return "", err
96 }
97 return w.raw.Put(ctx, key, raw, "")
98}
99
100// BeginPurge transfers the reserved journal into the exclusive purging
101// operation, removing its own reservation registration in the same CAS.
102// Replays return a handle for the existing operation.
103func (w *ControlWriter) BeginPurge(ctx context.Context, account, app, appID, id string, owner RegistrationOwner) (*OperationHandle, error) {
104 if !validRegistrationOwner(owner) {
105 return nil, ErrIdentity
106 }
107 j, _, err := ReadPurgeJournal(ctx, w.raw, account, app, id, appID)
108 if err != nil {
109 return nil, err
110 }
111 a, err := w.read(ctx, account, app, appID)
112 if err != nil {
113 return nil, err
114 }
115 if op := a.Value.Op; op != nil {
116 if a.Value.State == "closed" && op.ID == id && op.Kind == OpPurging {
117 return &OperationHandle{w: w, account: account, app: app, appID: appID, id: id, kind: OpPurging, owner: owner}, nil
118 }
119 return nil, ErrPending
120 }
121 if j.Phase != "reserved" || j.AdmissionEpoch != a.Value.Epoch {
122 return nil, ErrPending
123 }
124 if _, err = w.updateAdmission(ctx, a, func(v *AppAdmission) error {
125 op := AppOperation{ID: id, Kind: OpPurging, AppID: appID, Phase: "draining", Intent: map[string]string{"account": account, "app": app}}
126 if err := exclusiveAdmission(v, op); err != nil {
127 return err
128 }
129 if r, ok := v.Registrations[id]; ok && r.Kind == registrationPurge {
130 delete(v.Registrations, id)
131 }
132 return nil
133 }); err != nil {
134 return nil, err
135 }
136 return &OperationHandle{w: w, account: account, app: app, appID: appID, id: id, kind: OpPurging, owner: owner}, nil
137}
138
139// SavePurgeJournal is a validated replacement, not a generic write:
140// identity is immutable, the phase never moves backwards, cleanup flags only
141// set, host entries are never removed, and the journal's own purging
142// operation must still own admission.
143func (w *ControlWriter) SavePurgeJournal(ctx context.Context, next PurgeJournal, tag string) (string, error) {
144 if err := w.ready(); err != nil {
145 return "", err
146 }
147 if tag == "" {
148 return "", ErrIdentity
149 }
150 key, err := PurgeKey(next.Account, next.App)
151 if err != nil {
152 return "", err
153 }
154 cur, curTag, err := ReadPurgeJournal(ctx, w.raw, next.Account, next.App, next.ID, next.AppID)
155 if err != nil {
156 return "", err
157 }
158 if curTag != tag {
159 return "", release.ErrCASConflict
160 }
161 if next.V != cur.V || next.RequestedBy != cur.RequestedBy || next.RequestedAt != cur.RequestedAt || next.Reason != cur.Reason ||
162 !reflect.DeepEqual(next.Tombstone, cur.Tombstone) || next.AdmissionEpoch != cur.AdmissionEpoch {
163 return "", ErrIdentity
164 }
165 if phaseIndex(next.Phase) < phaseIndex(cur.Phase) {
166 return "", ErrIdentity
167 }
168 for id, reg := range next.IngestRegistrations {
169 if !validIngestRegistration(id, reg, next.AppID, next.AdmissionEpoch) {
170 return "", ErrIdentity
171 }
172 }
173 for id, reg := range cur.IngestRegistrations {
174 if next.IngestRegistrations[id] != reg {
175 return "", ErrIdentity
176 }
177 }
178 for host := range cur.Hosts {
179 if _, ok := next.Hosts[host]; !ok {
180 return "", ErrIdentity
181 }
182 }
183 c, n := cur.Cleanup, next.Cleanup
184 if (c.Tombstone && !n.Tombstone) || (c.AppRow && !n.AppRow) || (c.Fences && !n.Fences) || (c.Admission && !n.Admission) ||
185 (c.Sweep.Verified && !n.Sweep.Verified) || n.Sweep.Batch < c.Sweep.Batch {
186 return "", ErrIdentity
187 }
188 a, err := w.read(ctx, next.Account, next.App, next.AppID)
189 if err != nil {
190 return "", err
191 }
192 if a.Value.State != "closed" || a.Value.Op == nil || a.Value.Op.ID != next.ID || a.Value.Op.Kind != OpPurging {
193 return "", ErrPending
194 }
195 body, err := json.Marshal(next)
196 if err != nil {
197 return "", err
198 }
199 MarkAppMutationIssued(ctx)
200 return w.raw.Put(ctx, key, body, tag)
201}
202
203// DrainPurgeRegistrations journals each saved registration into the purge
204// journal, fences or witnesses its host, and only then removes it.
205func (w *ControlWriter) DrainPurgeRegistrations(ctx context.Context, account, app, appID, id string, now time.Time, recoverCreate func(context.Context, string) error) error {
206 a, err := w.read(ctx, account, app, appID)
207 if err != nil {
208 return err
209 }
210 if a.Value.State != "closed" || a.Value.Op == nil || a.Value.Op.ID != id || a.Value.Op.Kind != OpPurging {
211 return ErrPending
212 }
213 for regID, registration := range a.Value.Registrations {
214 if registration.Kind == RegistrationIngest {
215 j, tag, err := ReadPurgeJournal(ctx, w.raw, account, app, id, appID)
216 if err != nil {
217 return err
218 }
219 if j.Phase != "reserved" {
220 return ErrPending
221 }
222 if j.IngestRegistrations == nil {
223 j.IngestRegistrations = map[string]Registration{}
224 }
225 if old, ok := j.IngestRegistrations[regID]; ok && old != registration {
226 return ErrIdentity
227 }
228 j.IngestRegistrations[regID] = registration
229 if _, err = w.SavePurgeJournal(ctx, j, tag); err != nil {
230 return err
231 }
232 out, _, err := w.judgeRegistration(ctx, &a.Value, account, app, regID, registration, now, nil)
233 if err != nil {
234 return err
235 }
236 if out != drainRemove {
237 return ErrPending
238 }
239 } else if registration.Kind == registrationPurge {
240 // A different purge's reservation: drained once it lapses.
241 if !RegistrationDrained(registration, now) {
242 return ErrPending
243 }
244 } else {
245 j, tag, err := ReadPurgeJournal(ctx, w.raw, account, app, id, appID)
246 if err != nil {
247 return err
248 }
249 if j.Phase != "reserved" {
250 return ErrPending
251 }
252 h := j.Hosts[registration.Host]
253 if h.Registrations == nil {
254 h.Registrations = map[string]json.RawMessage{}
255 }
256 body, err := json.Marshal(registration)
257 if err != nil {
258 return err
259 }
260 h.Registrations[regID] = body
261 j.Hosts[registration.Host] = h
262 if _, err = w.SavePurgeJournal(ctx, j, tag); err != nil {
263 return err
264 }
265 obs, err := hostclaim.Read(ctx, w.raw, registration.Host)
266 if err != nil {
267 return err
268 }
269 if !obs.Absent && obs.Claim.Op.ID == regID {
270 if obs.Claim.Op.Kind == hostclaim.KindCreating && recoverCreate != nil {
271 if err = recoverCreate(ctx, registration.Host); err != nil && !errors.Is(err, ErrPending) {
272 return err
273 }
274 }
275 return ErrPending
276 }
277 if !RegistrationDrained(registration, now) {
278 return ErrPending
279 }
280 changed := obs.Absent != registration.Claim.Absent || (!obs.Absent && (obs.ETag != registration.Claim.ETag || obs.Claim.Gen != registration.Claim.Gen))
281 switch {
282 case registration.Kind == RegistrationWakeRetryReset:
283 case obs.Absent || obs.Claim.Free():
284 if obs, err = hostclaim.Update(ctx, w.raw, obs, FencePurge(id, now)); err != nil {
285 return err
286 }
287 h.FenceState, h.FenceGen, h.FenceETag = "held", obs.Claim.Gen, obs.ETag
288 case obs.Claim.Holder == nil && obs.Claim.Op.Kind == hostclaim.KindPurging && obs.Claim.Op.ID == id:
289 h.FenceState, h.FenceGen, h.FenceETag = "held", obs.Claim.Gen, obs.ETag
290 default:
291 if !changed {
292 return ErrPending
293 }
294 // A foreign revision proves only that the delayed CAS cannot
295 // succeed; it grants no authority over that holder.
296 h.FenceState, h.FenceGen, h.FenceETag = "changed", obs.Claim.Gen, obs.ETag
297 }
298 if j, tag, err = ReadPurgeJournal(ctx, w.raw, account, app, id, appID); err != nil {
299 return err
300 }
301 if j.Phase != "reserved" {
302 return ErrPending
303 }
304 j.Hosts[registration.Host] = h
305 if _, err = w.SavePurgeJournal(ctx, j, tag); err != nil {
306 return err
307 }
308 }
309 // Retried: the mutate re-checks the op and the exact saved registration.
310 if _, err = w.retryAdmission(ctx, account, app, appID, AdmissionObserved{}, func(v *AppAdmission) error {
311 if v.Op == nil || v.Op.ID != id || v.Op.Kind != OpPurging || v.State != "closed" {
312 return ErrPending
313 }
314 saved, ok := v.Registrations[regID]
315 if !ok {
316 return nil
317 }
318 if saved != registration || !effectsSettled(v, regID) {
319 return ErrPending
320 }
321 delete(v.Registrations, regID)
322 return nil
323 }); err != nil {
324 return err
325 }
326 }
327 fresh, err := w.read(ctx, account, app, appID)
328 if err != nil {
329 return err
330 }
331 if len(fresh.Value.Registrations) != 0 || !effectsSettled(&fresh.Value, "") {
332 return ErrPending
333 }
334 return nil
335}
336
337// CompletePurge closes the incarnation: last_purge is set exactly here.
338func (h *OperationHandle) CompletePurge(ctx context.Context) error {
339 if h.kind != OpPurging {
340 return ErrIdentity
341 }
342 j, _, err := ReadPurgeJournal(ctx, h.w.raw, h.account, h.app, h.id, h.appID)
343 if err != nil {
344 return err
345 }
346 if j.Phase != "done" || !j.Cleanup.Fences || !j.Cleanup.Tombstone || !j.Cleanup.AppRow || !j.Cleanup.Sweep.Verified {
347 return ErrPending
348 }
349 a, err := h.w.read(ctx, h.account, h.app, h.appID)
350 if err != nil {
351 return err
352 }
353 if lp := a.Value.LastPurge; a.Value.Op == nil && lp != nil && lp.ID == h.id && lp.AppID == h.appID {
354 return nil
355 }
356 if err = h.mine(&a.Value); err != nil {
357 return err
358 }
359 _, err = h.w.retryAdmission(ctx, h.account, h.app, h.appID, a, func(v *AppAdmission) error {
360 if err := h.mine(v); err != nil {
361 return err
362 }
363 if len(v.Registrations) != 0 || !effectsSettled(v, "") {
364 return ErrPending
365 }
366 v.State = "closed"
367 v.LastPurge = &PurgeMark{ID: h.id, AppID: h.appID}
368 v.Op = nil
369 return nil
370 })
371 return err
372}
373
374// RemovePurgeJournal deletes the journal last, and with it a box purge
375// request that named the same retired incarnation.
376func (w *ControlWriter) RemovePurgeJournal(ctx context.Context, account, app, id string) error {
377 a, err := ReadCanonicalAdmission(ctx, w.raw, account, app)
378 if err != nil {
379 return err
380 }
381 if !CompletedAdmission(a.Value) || a.Value.LastPurge.ID != id {
382 return ErrPending
383 }
384 j, tag, err := ReadPurgeJournal(ctx, w.raw, account, app, id, a.Value.AppID)
385 if errors.Is(err, os.ErrNotExist) {
386 _, err = w.DiscardPurgeRequest(ctx, account, app)
387 return err
388 }
389 if err != nil {
390 return err
391 }
392 if j.Phase != "done" || !j.Cleanup.Fences {
393 return ErrPending
394 }
395 if _, err = w.DiscardPurgeRequest(ctx, account, app); err != nil {
396 return err
397 }
398 key, err := PurgeKey(account, app)
399 if err != nil {
400 return err
401 }
402 return release.DeleteIfMatch(ctx, w.raw, key, tag)
403}
404
405// DiscardPurgeRequest removes a box request only when it names a retired
406// incarnation; a request for the live app is left for SweepRequests.
407func (w *ControlWriter) DiscardPurgeRequest(ctx context.Context, account, app string) (bool, error) {
408 key, err := PurgeRequestKey(account, app)
409 if err != nil {
410 return false, err
411 }
412 body, tag, err := w.raw.Get(ctx, key)
413 if errors.Is(err, os.ErrNotExist) {
414 return false, nil
415 }
416 if err != nil {
417 return false, err
418 }
419 var request release.ClaimPurgeRequest
420 if err = json.Unmarshal(body, &request); err != nil {
421 return false, err
422 }
423 if request.V != 1 || request.Account != account || request.App != app || request.AppID == "" || tag == "" {
424 return false, ErrIdentity
425 }
426 a, err := ReadCanonicalAdmission(ctx, w.raw, account, app)
427 if err != nil {
428 return false, err
429 }
430 if a.Value.LastPurge == nil || a.Value.LastPurge.AppID != request.AppID {
431 return false, nil
432 }
433 if err = release.DeleteIfMatch(ctx, w.raw, key, tag); err != nil {
434 return false, err
435 }
436 return true, nil
437}
438