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 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "github.com/carlosframework/platform/internal/park"
9 "strconv"
10 "time"
11
12 "github.com/carlosframework/platform/internal/hostclaim"
13 "github.com/carlosframework/platform/internal/provision"
14 "github.com/carlosframework/platform/internal/release"
15)
16
17// movingIdentities digests the claim's operation and holder. The record
18// decision binds both, so a changed operation can never reuse it, while a
19// chore acknowledgement (which changes neither) can.
20func movingIdentities(c *hostclaim.Claim) (string, string, error) {
21 if c == nil || c.Holder == nil {
22 return "", "", ErrIdentity
23 }
24 h, err := json.Marshal(c.Holder)
25 if err != nil {
26 return "", "", err
27 }
28 op, err := json.Marshal(c.Op)
29 if err != nil {
30 return "", "", err
31 }
32 hs, ops := sha256.Sum256(h), sha256.Sum256(op)
33 return hex.EncodeToString(ops[:]), hex.EncodeToString(hs[:]), nil
34}
35
36// moveAdmission rereads the app admission the move started under (Task 7).
37// Accepted work drains under closed admission and expiry does not revoke it,
38// so only identity is compared: the exact "move" registration, named by the
39// move's own op id, for this host and incarnation, at the admission's epoch,
40// under the claim's app id.
41func moveAdmission(ctx context.Context, s release.Store, host string, c *hostclaim.Claim) error {
42 if c == nil || c.Holder == nil {
43 return ErrIdentity
44 }
45 appID, id := c.Op.Intent["app_id"], c.Op.Intent["admission_id"]
46 if appID == "" || id == "" || id != c.Op.ID {
47 return ErrIdentity
48 }
49 a, err := ReadAdmission(ctx, s, c.Holder.Account, c.Holder.App, appID)
50 if err != nil {
51 return err
52 }
53 r, ok := a.Value.Registrations[id]
54 if !ok || r.Kind != RegistrationMove || r.AppID != appID || r.Epoch != a.Value.Epoch || r.Host != host || r.Incarnation != c.Holder.Incarnation {
55 return ErrPending
56 }
57 return nil
58}
59
60// movingRecordDecision is the PreFlipGuard over the authoritative record's
61// durable decision (provision.MoveRelocatingDecision).
62type oneBoxRegionAuthority interface {
63 CheckOneBoxRegion(context.Context, string, string) error
64}
65
66type movingRecordDecision struct {
67 homeRegion string
68 placement oneBoxRegionAuthority
69 store release.Store
70}
71
72var _ hostclaim.PreFlipGuard = movingRecordDecision{}
73
74// CheckMovingBeforeFlip reads the move's own intent while the claim is
75// moving ("source", "to") and the conversion's once relocating
76// ("source_box", "region", "move_to_region"). The region it returns is the
77// verified roster identity for the record placement; the home code stays
78// empty in the decision. ConvertMovingToRelocating compares the returned
79// identity with the unmodified fence binding.
80// ConvertMovingToRelocating calls it again inside its transition, so the
81// admission reread here is the last read before the one claim CAS.
82func (g movingRecordDecision) CheckMovingBeforeFlip(ctx context.Context, o hostclaim.Observed, opID string, ref hostclaim.FenceRef) (string, error) {
83 if o.Claim == nil || o.Claim.Holder == nil || o.Absent || o.ETag == "" || opID == "" || o.Claim.Op.ID != opID {
84 return "", ErrIdentity
85 }
86 c, h := o.Claim, o.Claim.Holder
87 if !operationHomeMatches(g.homeRegion, c) {
88 return "", ErrIdentity
89 }
90 if err := moveAdmission(ctx, g.store, o.Host, c); err != nil {
91 return "", err
92 }
93 rec, etag, err := provision.GetRecordETag(ctx, g.store, h.Account, h.App, o.Host)
94 if err != nil {
95 return "", err
96 }
97 inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
98 if err != nil || etag == "" || inc != h.Incarnation || rec.Move == nil || rec.Move.ID != c.Op.ID || rec.Move.Incarnation != h.Incarnation || rec.Move.State != provision.MoveRelocating || rec.Move.Relocation == nil {
99 return "", ErrIdentity
100 }
101 d := rec.Move.Relocation
102 moving, holder, err := movingIdentities(c)
103 if err != nil {
104 return "", err
105 }
106 if holder != d.HolderIdentity {
107 return "", ErrIdentity
108 }
109 var source, region, to string
110 switch c.Op.Kind {
111 case hostclaim.KindMoving:
112 if d.MovingIdentity != moving || c.Gen < d.ClaimGen {
113 return "", hostclaim.ErrConflict
114 }
115 region = park.PlacementRegion(rec.Region)
116 if rec.Region == "" && c.Op.Intent["home_region"] != "" {
117 region = c.Op.Intent["home_region"]
118 }
119 source, to = c.Op.Intent["source"], c.Op.Intent["to"]
120 case hostclaim.KindRelocating:
121 convertedGen, err := strconv.ParseUint(c.Op.Journal["conversion_claim_gen"], 10, 64)
122 raw, e := json.Marshal(d)
123 if e != nil {
124 return "", e
125 }
126 sum := sha256.Sum256(raw)
127 if err != nil || convertedGen < d.ClaimGen || c.Op.Journal["conversion_claim_etag"] == "" || c.Op.Journal["conversion_decision_sha256"] != hex.EncodeToString(sum[:]) {
128 return "", hostclaim.ErrConflict
129 }
130 source, region, to = c.Op.Intent["source_box"], c.Op.Intent["region"], c.Op.Intent["move_to_region"]
131 default:
132 return "", hostclaim.ErrWrongState
133 }
134 raw, err := json.Marshal(ref)
135 if err != nil {
136 return "", err
137 }
138 if source == "" || region == "" || d.DecidedAt.IsZero() || d.Fence != string(raw) || d.Enrollment != ref.Enrollment || d.Enrollment != c.Op.Intent["source_enrollment"] || d.SourceBox != source ||
139 !park.PlacementMatches(c.Op.Intent["home_region"], d.Region, region) || rec.Region != d.Region || rec.Move.From != d.Region || !park.PlacementMatches(c.Op.Intent["home_region"], rec.Move.To, to) || rec.Move.FlippedAt != nil || rec.Move.CommittedAt != nil || rec.Move.Rollback != nil {
140 return "", ErrIdentity
141 }
142 switch d.PreviousState {
143 case provision.MovePreparing, provision.MoveReady, provision.MoveCutting:
144 default:
145 return "", ErrIdentity
146 }
147 if g.placement == nil {
148 return "", ErrContract
149 }
150 if err := g.placement.CheckOneBoxRegion(ctx, source, region); err != nil {
151 return "", err
152 }
153 return region, nil
154}
155
156// ConversionDecisionDigest binds the converted claim to the exact record
157// decision it was converted under.
158func (g movingRecordDecision) ConversionDecisionDigest(ctx context.Context, o hostclaim.Observed, id string, ref hostclaim.FenceRef) (string, error) {
159 if _, err := g.CheckMovingBeforeFlip(ctx, o, id, ref); err != nil {
160 return "", err
161 }
162 h := o.Claim.Holder
163 rec, err := provision.GetRecord(ctx, g.store, h.Account, h.App, o.Host)
164 if err != nil {
165 return "", err
166 }
167 if rec.Move == nil || rec.Move.Relocation == nil {
168 return "", ErrIdentity
169 }
170 raw, err := json.Marshal(rec.Move.Relocation)
171 if err != nil {
172 return "", err
173 }
174 digest := sha256.Sum256(raw)
175 return hex.EncodeToString(digest[:]), nil
176}
177
178// heldMoveRegistration: mh is the writer-minted handle of the move's own
179// registration (its id is the op id and admission_id, its app id the
180// claim's), still owned by mh at mh's epoch and live by the writer clock.
181// The data boundary checks the same again at each write (the issued effect
182// at the record Put, the lifecycle grant at the claim CAS); this check makes
183// an unbound call refuse before any of them. Admission may be closed:
184// accepted work drains under closed admission.
185func heldMoveRegistration(ctx context.Context, s release.Store, mh *StartHandle, host string, c *hostclaim.Claim) error {
186 if mh == nil || mh.w == nil || c == nil || c.Holder == nil || mh.account != c.Holder.Account || mh.app != c.Holder.App || mh.id != c.Op.ID || mh.appID != c.Op.Intent["app_id"] {
187 return ErrIdentity
188 }
189 if err := moveAdmission(ctx, s, host, c); err != nil {
190 return err
191 }
192 a, err := ReadAdmission(ctx, s, mh.account, mh.app, mh.appID)
193 if err != nil {
194 return err
195 }
196 r := a.Value.Registrations[mh.id]
197 if r.Owner != mh.owner || r.Epoch != mh.epoch || a.Value.Epoch != mh.epoch || !RegistrationLive(r, mh.w.now()) {
198 return ErrPending
199 }
200 return nil
201}
202
203// RecoverTerminatedMovingHost is called only with claim mode enabled, by the
204// Moves coordinator holding mh, the handle RegisterMove minted for this move
205// and kept live (Renew) since (no caller is wired yet). It persists the
206// record decision as one issued effect of that registration, then converts
207// the claim in one CAS under its lifecycle grant. No relocation
208// installation or execution is activated, including on replay.
209//
210// While moving, the source is the claim's "source"; once relocating, its
211// "source_box". The region is never claim input: it is the authoritative
212// record's, and must equal the fence's roster binding.
213func RecoverTerminatedMovingHost(ctx context.Context, s release.Store, mh *StartHandle, authority hostclaim.FenceAuthority, host string, ref hostclaim.FenceRef, now time.Time) error {
214 o, err := hostclaim.Read(ctx, s, host)
215 if err != nil {
216 return err
217 }
218 if o.Absent || o.Claim.Holder == nil {
219 return ErrIdentity
220 }
221 c, h := o.Claim, o.Claim.Holder
222 if err := heldMoveRegistration(ctx, s, mh, host, c); err != nil {
223 return err
224 }
225 fence, err := hostclaim.VerifyFence(ctx, s, authority, ref)
226 if err != nil {
227 return err
228 }
229 placement, ok := authority.(oneBoxRegionAuthority)
230 if !ok {
231 return ErrContract
232 }
233 if !operationHomeMatches(mh.w.homeRegion, c) {
234 return ErrIdentity
235 }
236 guard := movingRecordDecision{store: s, placement: placement, homeRegion: mh.w.homeRegion}
237 if c.Op.Kind == hostclaim.KindRelocating {
238 source := c.Op.Intent["source_box"]
239 if fence.Box() != source || fence.Region() != c.Op.Intent["region"] {
240 return hostclaim.ErrFence
241 }
242 if _, err := guard.CheckMovingBeforeFlip(ctx, o, c.Op.ID, ref); err != nil {
243 return err
244 }
245 return hostclaim.VerifyOperationFence(ctx, s, authority, c.Op, source)
246 }
247 if c.Op.Kind != hostclaim.KindMoving || c.Op.Intent["source_enrollment"] != ref.Enrollment {
248 return hostclaim.ErrWrongState
249 }
250 source := c.Op.Intent["source"]
251 if fence.Box() != source {
252 return hostclaim.ErrFence
253 }
254 rec, err := provision.GetRecord(ctx, s, h.Account, h.App, host)
255 if err != nil {
256 return err
257 }
258 region := rec.Region
259 if !park.PlacementMatches(c.Op.Intent["home_region"], region, fence.Region()) || rec.Move == nil || rec.Move.From != region {
260 return hostclaim.ErrFence
261 }
262 if err := placement.CheckOneBoxRegion(ctx, source, fence.Region()); err != nil {
263 return err
264 }
265 raw, err := json.Marshal(ref)
266 if err != nil {
267 return err
268 }
269 movingIdentity, holderIdentity, err := movingIdentities(c)
270 if err != nil {
271 return err
272 }
273 // A replay whose decision is already durable for this exact operation
274 // and holder writes nothing more (a crash after the record Put leaves
275 // that effect uncertain, and it would block a second one).
276 if _, undecided := guard.CheckMovingBeforeFlip(ctx, o, c.Op.ID, ref); undecided != nil {
277 moveGuard := provision.MoveGuard{ID: c.Op.ID, Incarnation: h.Incarnation}
278 effect, err := operationToken()
279 if err != nil {
280 return err
281 }
282 if _, err = runRecordEffect(ctx, mh, Effect{ID: effect, Resource: h.Key, Action: "move-relocating-decision", Inputs: map[string]string{"move_id": c.Op.ID, "claim_etag": o.ETag, "fence_sha256": ref.SHA256}}, func(ctx context.Context) (string, error) {
283 _, err := setLifecycleRecordMove(ctx, s, o, func(rec *provision.Record) error {
284 // Compare the claim before deciding the record. MoveRelocatingDecision
285 // rechecks the fresh record's region against the bound region.
286 fresh, err := hostclaim.Read(ctx, s, host)
287 if err != nil {
288 return err
289 }
290 if fresh.Absent || fresh.ETag != o.ETag {
291 return hostclaim.ErrConflict
292 }
293 return provision.MoveRelocatingDecision(moveGuard, source, ref.Enrollment, region, string(raw), o.Claim.Gen, o.ETag, movingIdentity, holderIdentity, now)(rec)
294 })
295 if err != nil {
296 return "", err
297 }
298 return c.Op.ID, nil
299 }); err != nil {
300 return err
301 }
302 }
303 fresh, err := hostclaim.Read(ctx, s, host)
304 if err != nil {
305 return err
306 }
307 if fresh.Absent || fresh.Claim.Op.Kind != hostclaim.KindMoving {
308 return hostclaim.ErrWrongState
309 }
310 freshMoving, freshHolder, err := movingIdentities(fresh.Claim)
311 if err != nil {
312 return err
313 }
314 if freshMoving != movingIdentity || freshHolder != holderIdentity {
315 return hostclaim.ErrConflict
316 }
317 _, err = hostclaim.ConvertMovingToRelocating(mh.Lifecycle(ctx), s, fresh, c.Op.ID, ref, authority, guard)
318 return err
319}
320