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 "github.com/carlosframework/platform/internal/park"
7 "sync/atomic"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostactors"
11 "github.com/carlosframework/platform/internal/hostclaim"
12 "github.com/carlosframework/platform/internal/provision"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// RegistrationMove is the admission registration kind of a claim-mode move.
17const RegistrationMove = "move"
18
19// RegisterMove mints the exact "move" registration for the serving claim
20// observation o: this host, this incarnation, this claim ETag and gen, at
21// the admission's current epoch. The handle is the only capability that can
22// begin the move (BeginClaimMove); knowing the registration id is not enough.
23//
24// The registration id is the move id: judgeRegistration keeps a start whose
25// claim operation carries its id ("an accepted start owns the host"), so the
26// registration outlives its TTL exactly as long as the move (or its C7
27// conversion, which keeps the op id) holds the claim. Any other id would be
28// drained 90 seconds in, and every later record write would be refused.
29func (a ProductionAuthority) RegisterMove(ctx context.Context, o hostclaim.Observed, appID, moveID string) (*StartHandle, error) {
30 if a.Control == nil || a.Now == nil || a.Store == nil {
31 return nil, ErrContract
32 }
33 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.ETag == "" || o.Claim.Holder.Family != "instance" || o.Claim.Op.Kind != hostclaim.KindServing {
34 return nil, ErrPending
35 }
36 if !provision.ValidMoveID(moveID) {
37 return nil, ErrIdentity
38 }
39 h := o.Claim.Holder
40 admission, err := ReadAdmission(ctx, a.Store, h.Account, h.App, appID)
41 if err != nil {
42 return nil, err
43 }
44 id := moveID
45 controller, err := operationToken()
46 if err != nil {
47 return nil, err
48 }
49 owner := RegistrationOwner{Console: a.ConsoleID, Enrollment: a.Enrollment, Controller: controller}
50 reg := Registration{Kind: RegistrationMove, Host: o.Host, Owner: owner, AppID: appID, Epoch: admission.Value.Epoch, Claim: SavedObservation(o), Incarnation: h.Incarnation, CreatedAt: h.CreatedAt}
51 return a.Control.RegisterStart(ctx, h.Account, h.App, appID, id, reg, a.Now())
52}
53
54// BeginClaimMove is the claim-mode request door's start CAS (K2 under claim
55// Plan 5 Task 7). It persists, beside the move's own "source", "target" and
56// "to", the canonical app id and the id of h's writer-minted "move"
57// registration (which is the move id, see RegisterMove), and both boxes'
58// enrollments from the signed roster. The source is the claim's executor;
59// no enrollment is caller input, and the target and region are bound to
60// the roster. The admission is reread inside the transition, so the last
61// admission read precedes the one claim CAS, which itself runs under h's
62// lifecycle grant (the data boundary rereads admission again at the Put).
63func BeginClaimMove(ctx context.Context, s release.Store, d hostactors.Directory, h *StartHandle, o hostclaim.Observed, id, by, target, to string, now time.Time, homeNames ...string) (hostclaim.Observed, error) {
64 if h == nil || h.w == nil || s == nil || o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.ETag == "" || id == "" || id != h.id || now.IsZero() {
65 return hostclaim.Observed{}, ErrIdentity
66 }
67 home := ""
68 if len(homeNames) > 1 {
69 return hostclaim.Observed{}, ErrIdentity
70 }
71 if len(homeNames) == 1 {
72 home = homeNames[0]
73 }
74 if home != "" && park.PlacementRegion(home) != home {
75 return hostclaim.Observed{}, ErrIdentity
76 }
77 holder := o.Claim.Holder
78 if holder.Family != "instance" || holder.Account != h.account || holder.App != h.app {
79 return hostclaim.Observed{}, ErrIdentity
80 }
81 source := o.Claim.Op.Exec
82 roster, err := d.Read(ctx)
83 if err != nil {
84 return hostclaim.Observed{}, err
85 }
86 if !roster.Roster.MatchesHome(home) {
87 return hostclaim.Observed{}, ErrIdentity
88 }
89 src, sourceOK := roster.Roster.Boxes[source]
90 dst, targetOK := roster.Roster.Boxes[target]
91 if !sourceOK || !targetOK || source == target || src.State != "active" || dst.State != "active" || src.Region == "" || dst.Region == "" {
92 return hostclaim.Observed{}, ErrIdentity
93 }
94 // Lookup also refuses a box with a termination fence.
95 for box, enrollment := range map[string]string{source: src.Enrollment, target: dst.Enrollment} {
96 actor, err := d.Lookup(ctx, box)
97 if err != nil {
98 return hostclaim.Observed{}, err
99 }
100 if actor.Enrollment != enrollment {
101 return hostclaim.Observed{}, ErrIdentity
102 }
103 }
104 // The record is the region authority: the source's roster region must be
105 // the record's, which is what C7's conversion later checks against the
106 // fence binding.
107 rec, err := provision.GetRecord(ctx, s, holder.Account, holder.App, o.Host)
108 if err != nil {
109 return hostclaim.Observed{}, err
110 }
111 inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
112 if err != nil || inc != holder.Incarnation || !ClaimMoveRosterPlaces(roster.Roster, source, target, rec.Region, to, home) || rec.Move != nil {
113 return hostclaim.Observed{}, ErrIdentity
114 }
115 admitted := hostclaim.MoveAdmission{HomeRegion: home, AppID: h.appID, AdmissionID: h.id, SourceEnrollment: src.Enrollment, TargetEnrollment: dst.Enrollment}
116 return hostclaim.Update(h.Lifecycle(ctx), s, o, func(c *hostclaim.Claim) error {
117 if err := liveMoveRegistration(ctx, s, h, o); err != nil {
118 return err
119 }
120 if err := hostclaim.BeginAdmittedMove(id, by, source, target, park.PlacementRegion(to), now, admitted)(c); err != nil {
121 return err
122 }
123 if home != "" {
124 c.Op.Intent["home_region"] = home
125 }
126 return nil
127 })
128}
129
130// liveMoveRegistration: h's registration is still the exact open "move"
131// registration for this observation, at the current epoch, and live.
132func liveMoveRegistration(ctx context.Context, s release.Store, h *StartHandle, o hostclaim.Observed) error {
133 a, err := ReadAdmission(ctx, s, h.account, h.app, h.appID)
134 if err != nil {
135 return err
136 }
137 v := a.Value
138 r, ok := v.Registrations[h.id]
139 if !ok || r.Kind != RegistrationMove || r.Owner != h.owner || r.AppID != h.appID || r.Epoch != h.epoch || v.Epoch != h.epoch || v.State != "open" || v.Op != nil ||
140 r.Host != o.Host || r.Incarnation == "" || r.Incarnation != o.Claim.Holder.Incarnation || r.Claim != SavedObservation(o) || !RegistrationLive(r, h.w.now()) {
141 return ErrPending
142 }
143 return nil
144}
145
146// StartClaimMoveRecord journals the move on the record after BeginClaimMove,
147// as one issued effect of the same registration. The lifecycle record writer
148// binds the write to the moving claim's exact ETag and rereads admission.
149// Every record Put is If-Match on the record ETag it was decided from. When
150// one fails after it was issued, the effect is uncertain and
151// ResolveMoveStart, with this same handle, settles it.
152func StartClaimMoveRecord(ctx context.Context, s release.Store, h *StartHandle, moving hostclaim.Observed, p provision.MoveStart) (*provision.Record, error) {
153 if h == nil || h.w == nil || moving.Absent || moving.Claim == nil || moving.Claim.Holder == nil {
154 return nil, ErrIdentity
155 }
156 c := moving.Claim
157 if c.Op.Kind != hostclaim.KindMoving || c.Op.Phase != hostclaim.PhaseMoving || p.ID != c.Op.ID || p.Incarnation != c.Holder.Incarnation || !park.PlacementMatches(c.Op.Intent["home_region"], p.To, c.Op.Intent["to"]) ||
158 c.Op.Intent["admission_id"] != h.id || c.Op.Intent["app_id"] != h.appID {
159 return nil, ErrIdentity
160 }
161 var rec *provision.Record
162 var callErr error
163 _, err := runRecordEffect(ctx, h, Effect{ID: p.ID, Resource: c.Holder.Key, Action: moveStartAction, Inputs: map[string]string{"move_id": p.ID, "incarnation": p.Incarnation, "to": p.To}}, func(ctx context.Context) (string, error) {
164 // The handle keeps every If-Match ETag this start's record Puts
165 // carry, so ResolveMoveStart can fence one whose answer was lost.
166 puts := &moveStartPuts{id: p.ID, incarnation: p.Incarnation, key: c.Holder.Key, holder: *c.Holder}
167 h.moveStart.puts.Store(puts)
168 rec, callErr = setLifecycleRecordMove(ctx, moveStartRecorder{s: s, puts: puts, capture: func(ctx context.Context, seq uint64) error { return h.captureMoveStartBase(ctx, seq) }}, moving, provision.MoveStartTransition(p))
169 if callErr != nil {
170 return "", callErr
171 }
172 return p.ID, nil
173 })
174 if callErr != nil {
175 return nil, callErr
176 }
177 if err != nil {
178 return nil, err
179 }
180 if rec == nil {
181 // A replay of an effect already complete runs no call: read the result.
182 if rec, err = provision.GetRecord(ctx, s, c.Holder.Account, c.Holder.App, moving.Host); err != nil {
183 return nil, err
184 }
185 if rec.Move == nil || rec.Move.ID != p.ID || rec.Move.Incarnation != p.Incarnation {
186 return nil, ErrIdentity
187 }
188 }
189 return rec, nil
190}
191
192// runRecordEffect runs call as one issued effect of h's registration: the
193// data boundary admits the record Put only under that effect's permit. A
194// call refused before any write reached a raw sink settles "not-applied"
195// (as RunAppMutation does), so a refusal never leaves an uncertain effect
196// that blocks every later effect on the same record.
197func runRecordEffect(ctx context.Context, h *StartHandle, e Effect, call func(context.Context) (string, error)) (string, error) {
198 issued := new(atomic.Bool)
199 return h.Runner(h.w.now).Run(ctx, e, func(ctx context.Context) (string, error) {
200 outer, _ := ctx.Value(issuedMutationKey{}).([]*atomic.Bool)
201 ctx = context.WithValue(ctx, issuedMutationKey{}, append(append([]*atomic.Bool(nil), outer...), issued))
202 result, err := call(ctx)
203 if err != nil && !issued.Load() {
204 return "", errors.Join(ErrNotApplied, err)
205 }
206 return result, err
207 })
208}
209