carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "time"
9
10 "github.com/carlosframework/platform/internal/domains"
11 "github.com/carlosframework/platform/internal/hostclaim"
12 "github.com/carlosframework/platform/internal/hostops"
13 "github.com/carlosframework/platform/internal/provision"
14 "github.com/carlosframework/platform/internal/release"
15)
16
17// retireClaimHostRecord runs one delete as an exclusive "deleting"
18// operation. The order is fixed:
19//
20// 1. Begin (or resume) the operation and drain its registrations.
21// 2. Under the operation's own lifecycle grant, and with no effect in
22// flight, CAS the host claim from serving to deleting. The production
23// authority refuses that CAS while any effect of the app is unsettled,
24// so it never runs inside an effect.
25// 3. Delete the record and park the claim inside one record effect: the
26// typed record delete needs that effect's permit.
27// 4. Free the claim once parking has no obligations left, release the
28// wake and ownership markers (one effect) and reopen (FinishDelete).
29//
30// From step 2 on the delete intent is durable and owned by this
31// operation: a step that is only waiting (parking, a box acknowledgement)
32// answers claimRetireAccepted, and recovery or a retry carries it on.
33func (s *Server) retireClaimHostRecord(ctx context.Context, ref provision.RetireRef) error {
34 c := s.cfg.HostClaimOps
35 if c == nil || c.Control == nil || s.claimOwner == (hostops.RegistrationOwner{}) {
36 return provision.ErrClaimRetirer
37 }
38 acct, err := s.store.AccountBySqid(ref.Account)
39 if err != nil {
40 return err
41 }
42 if acct == nil {
43 return hostops.ErrIdentity
44 }
45 appID, err := s.store.ClaimAppID(acct.ID, ref.App)
46 if err != nil {
47 return err
48 }
49 admission, err := hostops.ReadAdmission(ctx, s.cfg.Releases, ref.Account, ref.App, appID)
50 if err != nil {
51 return err
52 }
53 var h *hostops.OperationHandle
54 if op := admission.Value.Op; op == nil {
55 key, err := claimRetireRecordKey(ref)
56 if err != nil {
57 return err
58 }
59 obs, err := hostclaim.Read(ctx, s.cfg.Releases, ref.Host)
60 if err != nil {
61 return err
62 }
63 if obs.Absent {
64 // Nothing holds the host: "not there" is reached when no record
65 // exists either. A record without a claim waits for backfill.
66 if _, _, gerr := s.cfg.Releases.Get(ctx, key); errors.Is(gerr, os.ErrNotExist) {
67 return nil
68 } else if gerr != nil {
69 return gerr
70 }
71 return hostops.ErrPending
72 }
73 holder := obs.Claim.Holder
74 if holder == nil {
75 if obs.Claim.Free() {
76 return nil
77 }
78 return hostops.ErrPending
79 }
80 if !claimRetireHolderIs(holder, ref, key) {
81 return hostops.ErrIdentity
82 }
83 id, err := claimEffectID()
84 if err != nil {
85 return err
86 }
87 begin := hostops.AppOperation{ID: id, Kind: hostops.OpDeleting, AppID: appID, Phase: "draining", Intent: map[string]string{"host": ref.Host, "incarnation": holder.Incarnation, "acquisition": holder.Acquisition, "family": ref.Family, "name": ref.Name}}
88 if h, err = c.Control.BeginExclusive(ctx, ref.Account, ref.App, appID, begin, s.claimOwner); err != nil {
89 return err
90 }
91 if admission, err = hostops.ReadAdmission(ctx, s.cfg.Releases, ref.Account, ref.App, appID); err != nil {
92 return err
93 }
94 } else {
95 // Only this host's own delete is resumed. Any other exclusive
96 // operation (a foreign mutation, another host's delete, a purge or a
97 // placement) refuses here, before any effect.
98 if op.Kind != hostops.OpDeleting || op.Intent["host"] != ref.Host || (ref.Incarnation != "" && op.Intent["incarnation"] != ref.Incarnation) {
99 return hostops.ErrPending
100 }
101 if h, err = c.Control.ResumeExclusive(ctx, ref.Account, ref.App, appID, op.ID, hostops.OpDeleting, s.claimOwner); err != nil {
102 return err
103 }
104 }
105 op := admission.Value.Op
106 if op == nil || op.ID != h.ID() || op.Intent["host"] != ref.Host || (ref.Incarnation != "" && op.Intent["incarnation"] != ref.Incarnation) {
107 return hostops.ErrPending
108 }
109 ref.Incarnation = op.Intent["incarnation"]
110 if ref.Family == "" {
111 ref.Family, ref.Name = op.Intent["family"], op.Intent["name"]
112 }
113 key, err := claimRetireRecordKey(ref)
114 if err != nil {
115 return err
116 }
117 // Every claim write below carries this operation's lifecycle grant.
118 ctx = h.Lifecycle(ctx)
119 // The operation is durable from here: a step that is only waiting
120 // answers claimRetireAccepted with this operation's ID and phase.
121 if err = h.Drain(ctx, c.Now(), c.RecoverCreate); err != nil {
122 return claimRetireWaiting(h, "draining", ref.Host, err)
123 }
124 if err = s.advanceClaimRetire(ctx, c, h, ref, key); err != nil {
125 return err
126 }
127 // The claim is free: the incarnation is retired, and its move log
128 // outbox rows go with it (Plan 6 Task 3; the drainer's drop of a row
129 // whose claim is gone is the backstop).
130 if err = s.store.PurgeMoveLogIncarnation(ref.Host, ref.Incarnation); err != nil {
131 return claimRetireWaiting(h, "releasing", ref.Host, err)
132 }
133 if err = s.releaseClaimRetireMarkers(ctx, h, ref, appID, op.Intent["acquisition"]); err != nil {
134 return claimRetireWaiting(h, "releasing", ref.Host, err)
135 }
136 return claimRetireWaiting(h, "releasing", ref.Host, h.FinishDelete(ctx))
137}
138
139// advanceClaimRetire moves the host claim from wherever this operation
140// left it to free. nil means free; claimRetireAccepted means the owned
141// delete is waiting on boxes or parking.
142func (s *Server) advanceClaimRetire(ctx context.Context, c *hostops.Controller, h *hostops.OperationHandle, ref provision.RetireRef, key string) error {
143 runner := h.Runner(time.Now)
144 owned := false
145 // Each pass makes at most one transition, so four passes cover
146 // serving, deleting, parking and free.
147 for range 4 {
148 obs, err := hostclaim.Read(ctx, s.cfg.Releases, ref.Host)
149 if err != nil {
150 return err
151 }
152 if obs.Absent {
153 return hostops.ErrPending
154 }
155 if obs.Claim.Free() {
156 return nil
157 }
158 holder := obs.Claim.Holder
159 if holder == nil || !claimRetireHolderIs(holder, ref, key) || holder.Incarnation != ref.Incarnation {
160 return hostops.ErrIdentity
161 }
162 claimOp := obs.Claim.Op
163 switch claimOp.Kind {
164 case hostclaim.KindServing:
165 if owned {
166 return hostops.ErrPending
167 }
168 // Step 2, the durable delete intent. No effect is issued here.
169 if _, err = c.BeginRetire(ctx, key, ref.By, ref.Incarnation); err != nil {
170 return claimRetireWaiting(h, "starting", ref.Host, err)
171 }
172 case hostclaim.KindDeleting:
173 owned = true
174 refreshed, err := s.refreshClaimRetireRecord(ctx, c, obs, key)
175 if err != nil {
176 return claimRetireWaiting(h, claimOp.Kind, ref.Host, err)
177 }
178 if refreshed {
179 // Re-read the re-bound claim before deleting.
180 continue
181 }
182 effectID, err := claimEffectID()
183 if err != nil {
184 return err
185 }
186 // Step 3: the typed record delete runs under this effect's permit.
187 _, err = runner.Run(ctx, hostops.Effect{ID: effectID, Resource: "record:" + ref.Host, Action: "retire"}, func(ctx context.Context) (string, error) {
188 rerr := c.Retire(ctx, ref.Host, claimOp.ID)
189 if rerr == nil {
190 return "retired", nil
191 }
192 if errors.Is(rerr, hostops.ErrPending) {
193 fresh, ferr := hostclaim.Read(ctx, s.cfg.Releases, ref.Host)
194 if ferr == nil && !fresh.Absent && fresh.Claim.Op.ID == claimOp.ID && fresh.Claim.Op.Kind == hostclaim.KindParking {
195 // Retire answers pending after a successful park.
196 return "parked", nil
197 }
198 // The record was not deleted (boxes not ready yet).
199 return "", errors.Join(hostops.ErrNotApplied, rerr)
200 }
201 return "", rerr
202 })
203 if err != nil {
204 return claimRetireWaiting(h, claimOp.Kind, ref.Host, err)
205 }
206 case hostclaim.KindParking:
207 owned = true
208 if len(obs.Claim.Obligations) != 0 {
209 return &claimRetireAccepted{operation: h.ID(), phase: hostclaim.KindParking, host: ref.Host}
210 }
211 // A claim-only transition under the lifecycle grant.
212 if err = c.Free(ctx, ref.Host, claimOp.ID); err != nil {
213 return claimRetireWaiting(h, claimOp.Kind, ref.Host, err)
214 }
215 default:
216 // Purging, placing and moving claims belong to their own operations.
217 return hostops.ErrPending
218 }
219 }
220 return &claimRetireAccepted{operation: h.ID(), phase: hostclaim.KindDeleting, host: ref.Host}
221}
222
223// refreshClaimRetireRecord re-binds the deleting claim to a record whose
224// ETag moved since the claim was written. It runs outside any effect: the
225// production refresh refuses while an effect is unsettled.
226func (s *Server) refreshClaimRetireRecord(ctx context.Context, c *hostops.Controller, obs hostclaim.Observed, key string) (bool, error) {
227 rec, err := hostops.ReadRecord(ctx, s.cfg.Releases, key)
228 if errors.Is(err, os.ErrNotExist) {
229 return false, nil
230 }
231 if err != nil {
232 return false, err
233 }
234 if rec.ETag == obs.Claim.Op.Intent["etag"] {
235 return false, nil
236 }
237 refresher, ok := c.Authority.(interface {
238 RefreshDeleteRecord(context.Context, hostclaim.Observed, hostops.Record) error
239 })
240 if !ok {
241 return true, hostops.ErrPending
242 }
243 return true, refresher.RefreshDeleteRecord(ctx, obs, rec)
244}
245
246// releaseClaimRetireMarkers removes the sidecar wake object and the
247// ownership marker, both bound to this incarnation, as one effect. It runs
248// only after the claim is free.
249func (s *Server) releaseClaimRetireMarkers(ctx context.Context, h *hostops.OperationHandle, ref provision.RetireRef, appID, acquisition string) error {
250 cleanupID, err := claimEffectID()
251 if err != nil {
252 return err
253 }
254 _, err = h.Runner(time.Now).Run(ctx, hostops.Effect{ID: cleanupID, Resource: "ownership:" + ref.Host, Action: "cleanup"}, func(ctx context.Context) (string, error) {
255 if ref.Family == "sidecar" {
256 key := release.SidecarWakeKey(ref.Account, ref.App, ref.Name)
257 raw, etag, err := s.cfg.Releases.Get(ctx, key)
258 if err != nil && !errors.Is(err, os.ErrNotExist) {
259 return "", err
260 }
261 if err == nil {
262 var wake struct {
263 AppID string `json:"app_id"`
264 Incarnation string `json:"incarnation"`
265 RequestID string `json:"request_id"`
266 }
267 if json.Unmarshal(raw, &wake) != nil {
268 return "", hostops.ErrIdentity
269 }
270 if (wake.AppID != "" || wake.Incarnation != "" || wake.RequestID != "") && (wake.AppID != appID || wake.Incarnation != ref.Incarnation || wake.RequestID == "") {
271 return "", hostops.ErrIdentity
272 }
273 if err = release.DeleteIfMatch(ctx, s.cfg.Releases, key, etag); err != nil {
274 return "", err
275 }
276 }
277 }
278 key := domains.ClaimKey(ref.Host)
279 raw, etag, err := s.cfg.Releases.Get(ctx, key)
280 if err != nil && !errors.Is(err, os.ErrNotExist) {
281 return "", err
282 }
283 if err == nil {
284 var owner domains.Claim
285 if json.Unmarshal(raw, &owner) != nil {
286 return "", hostops.ErrIdentity
287 }
288 if owner.Account != ref.Account || owner.App != ref.App || owner.Acquisition != acquisition || owner.RecordIncarnation != ref.Incarnation {
289 return "", hostops.ErrIdentity
290 }
291 if err = release.DeleteIfMatch(ctx, s.cfg.Releases, key, etag); err != nil {
292 return "", err
293 }
294 }
295 return "joined", nil
296 })
297 return err
298}
299
300func (s *Server) retireInstanceRecord(ctx context.Context, account, app, host, by, incarnation string) error {
301 return s.retireHostRecord(ctx, provision.RetireRef{Family: "instance", Account: account, App: app, Host: host, By: by, Incarnation: incarnation})
302}
303
304func mustCreateIncarnation(account, app, host string, created time.Time) string {
305 inc, err := hostclaim.Incarnation(account, app, host, created)
306 if err != nil {
307 return "invalid-incarnation"
308 }
309 return inc
310}
311
312func (s *Server) deleteLegacySidecarWake(ctx context.Context, account, app, name string) error {
313 if s.cfg.HostClaim {
314 return nil
315 }
316 return s.cfg.Releases.Delete(ctx, release.SidecarWakeKey(account, app, name))
317}
318