carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "encoding/json"
6 "slices"
7 "sync"
8 "time"
9
10 "github.com/carlosframework/platform/internal/controlsock"
11 "github.com/carlosframework/platform/internal/hostbox"
12 "github.com/carlosframework/platform/internal/hostclaim"
13 execlease "github.com/carlosframework/platform/internal/hostexec"
14 "github.com/carlosframework/platform/internal/hostwrite"
15)
16
17func executionExpectation(c *hostclaim.Claim) execlease.ExecutionRequest {
18 return execlease.ExecutionRequest{Schema: 1, Host: c.Host, Incarnation: c.Holder.Incarnation, Stamp: c.Holder.Stamp, Session: c.Holder.Session, Op: c.Op.ID}
19}
20
21func (p *productionClaimLocal) executionTransport() func(context.Context, execlease.ExecutionRequest) (execlease.ExecutionResponse, error) {
22 if p.executionCall != nil {
23 return p.executionCall
24 }
25 return rootExecutionClient(controlsock.Path)
26}
27
28func (p *productionClaimLocal) prepareWith(ctx context.Context, actor hostbox.Actor, o hostclaim.Observed) error {
29 if p.legacyParticipation {
30 return p.prepareLegacyWith(ctx, actor, o)
31 }
32 c := o.Claim
33 if c == nil {
34 return execlease.ErrState
35 }
36 p.mu.Lock()
37 old, lifetime, closed, assembly := p.slots[o.Host], p.lifetime, p.closed, p.assembly
38 delete(p.serving, o.Host)
39 p.mu.Unlock()
40 if closed || lifetime == nil || assembly == nil {
41 return hostwrite.ErrUnavailable
42 }
43 if p.Resources.Mode != "agent" {
44 return p.checkRootParticipation(ctx, actor, c)
45 }
46 if c.Free() || c.Op.Kind == hostclaim.KindServing || !slices.Contains(c.Op.Participants, actor.Box) {
47 if old != nil {
48 select {
49 case <-old.Ready:
50 case <-ctx.Done():
51 return ctx.Err()
52 }
53 if old.Controller == nil {
54 return execlease.ErrLost
55 }
56 if err := old.Controller.Close(ctx); err != nil {
57 return err
58 }
59 p.mu.Lock()
60 if p.slots[o.Host] == old {
61 delete(p.slots, o.Host)
62 }
63 p.mu.Unlock()
64 }
65 if c.Free() || c.Op.Kind != hostclaim.KindServing {
66 return nil
67 }
68 q := executionExpectation(c)
69 q.Operation, q.ID = "executionView", "serving-view"
70 out, err := p.executionTransport()(ctx, q)
71 if err != nil {
72 return err
73 }
74 boot, err := execlease.BootID()
75 if err != nil {
76 return err
77 }
78 if out.Schema != 1 || out.Status != "serving" || out.Host != q.Host || out.Incarnation != q.Incarnation || out.Stamp != q.Stamp || out.Session != q.Session || out.Op != q.Op || out.BootID != boot || out.Token != "" || out.SafeUntil != 0 || out.LeaseETag != "" || !out.Expires.IsZero() {
79 return execlease.ErrLost
80 }
81 if err := execlease.ClearRootClientMirror(p.Resources.DataDir, q.Host); err != nil {
82 return err
83 }
84 p.mu.Lock()
85 defer p.mu.Unlock()
86 if p.closed || p.slots[o.Host] != nil {
87 return execlease.ErrLost
88 }
89 if p.serving == nil {
90 p.serving = make(map[string]execlease.ExecutionRequest)
91 }
92 p.serving[o.Host] = executionExpectation(c)
93 return nil
94 }
95 if c.Holder == nil {
96 return execlease.ErrState
97 }
98 if old != nil {
99 select {
100 case <-old.Ready:
101 case <-ctx.Done():
102 return ctx.Err()
103 }
104 if old.Err != nil {
105 return old.Err
106 }
107 client, ok := old.Controller.(*execlease.RootClient)
108 if !ok {
109 return execlease.ErrLost
110 }
111 return client.Refresh(ctx)
112 }
113 boot, err := execlease.BootID()
114 if err != nil {
115 return err
116 }
117 slot := &claimParticipation{Ready: make(chan struct{}), Captured: o}
118 binding := execlease.Binding{Box: actor.Box, Enrollment: actor.Enrollment, Host: o.Host, Incarnation: c.Holder.Incarnation, Op: c.Op.ID, BootID: boot}
119 p.mu.Lock()
120 if p.closed || p.slots[o.Host] != nil {
121 p.mu.Unlock()
122 return execlease.ErrControllerBusy
123 }
124 if p.slots == nil {
125 p.slots = make(map[string]*claimParticipation)
126 }
127 p.slots[o.Host] = slot
128 p.mu.Unlock()
129 var captureMu sync.Mutex
130 captured := o
131 slot.Controller, slot.Err = execlease.OpenRootClient(lifetime, execlease.RootClientConfig{
132 Binding: binding, Clock: execlease.SystemClock{}, Call: p.executionTransport(),
133 Project: func(s execlease.State) error { return execlease.ProjectRootClient(p.Resources.DataDir, s) },
134 Observe: func(ctx context.Context) (execlease.ExecutionRequest, error) {
135 fresh, err := hostclaim.Read(ctx, p.Store, o.Host)
136 if err != nil {
137 return execlease.ExecutionRequest{}, err
138 }
139 if fresh.Absent || fresh.Claim == nil || fresh.Claim.Holder == nil {
140 return execlease.ExecutionRequest{}, execlease.ErrLost
141 }
142 if fresh.Claim.Holder.Incarnation == binding.Incarnation && fresh.Claim.Op.ID == binding.Op {
143 captureMu.Lock()
144 captured = fresh
145 captureMu.Unlock()
146 }
147 return executionExpectation(fresh.Claim), nil
148 },
149 CloseAdmission: func() { p.cancelWork(o.Host) },
150 Cleanup: func(ctx context.Context) error {
151 captureMu.Lock()
152 current := captured
153 captureMu.Unlock()
154 return p.cleanup(ctx, current, binding)
155 },
156 })
157 if slot.Err == nil {
158 var state execlease.State
159 state, slot.Err = slot.Controller.Ready(ctx)
160 slot.Binding = state.Binding
161 }
162 close(slot.Ready)
163 return slot.Err
164}
165
166func (p *productionClaimLocal) Check(ctx context.Context, actor hostbox.Actor, c *hostclaim.Claim, selection hostbox.Selection) error {
167 if p.legacyParticipation {
168 return p.checkLegacy(ctx, actor, c, selection)
169 }
170 if ctx.Err() != nil {
171 return ctx.Err()
172 }
173 if c == nil || c.Holder == nil || actor.Box != selection.Box || actor.Region != selection.Region || c.Holder.Incarnation != selection.Captured.Incarnation || c.Holder.Stamp != selection.Captured.Stamp || slices.Contains(c.Lost, actor.Box) {
174 return hostbox.ErrChanged
175 }
176 p.mu.Lock()
177 slot, serving, closed := p.slots[c.Host], p.serving[c.Host], p.closed
178 p.mu.Unlock()
179 if closed {
180 return execlease.ErrLost
181 }
182 if p.Resources.Mode != "agent" {
183 return p.checkRootParticipation(ctx, actor, c)
184 }
185 if c.Op.Kind == hostclaim.KindServing {
186 if slot != nil || serving != executionExpectation(c) {
187 return execlease.ErrLost
188 }
189 return nil
190 }
191 if slot == nil {
192 return execlease.ErrInactive
193 }
194 select {
195 case <-slot.Ready:
196 default:
197 return execlease.ErrInactive
198 }
199 if slot.Err != nil {
200 return slot.Err
201 }
202 client, ok := slot.Controller.(*execlease.RootClient)
203 if !ok || slot.Binding.Box != actor.Box || slot.Binding.Enrollment != actor.Enrollment {
204 return execlease.ErrLost
205 }
206 return client.Check(ctx, executionExpectation(c))
207}
208
209// ackRefreshBefore is how close to its expiry an execution acknowledgement
210// may get before this box refreshes it. The console completes a create,
211// delete or purge only while every participant's acknowledgement is
212// unexpired (hostops FirstPhase: now < Expires - RegistrationSkew), and its
213// only retry is recovery, five minutes after the op started. The participant
214// loop runs every 30 s and the lease (execlease.TTL, 90 s) renews every 20 s,
215// so a just-written acknowledgement has at least 70 s left. Two-thirds of the
216// TTL (60 s) leaves the first pass after a write alone and refreshes on the
217// next one. A refresh is due at most 60 s before expiry and the next pass
218// comes 30 s later, so the acknowledgement stays usable as long as a pass
219// starts within 60 - 30 - 5 = 25 s of its tick.
220const ackRefreshBefore = 2 * execlease.TTL / 3
221
222func (p *productionClaimLocal) wallNow() time.Time {
223 if p.now != nil {
224 return p.now()
225 }
226 return time.Now()
227}
228
229func (p *productionClaimLocal) acknowledgeOperation(ctx context.Context, actor hostbox.Actor, o hostclaim.Observed) error {
230 c := o.Claim
231 if c == nil || c.Holder == nil || (c.Op.Kind != hostclaim.KindCreating && c.Op.Kind != hostclaim.KindDeleting && c.Op.Kind != hostclaim.KindPurging) || !slices.Contains(c.Op.Participants, actor.Box) {
232 return nil
233 }
234 if previous := c.Op.Journal["exec_ack."+actor.Box]; previous != "" {
235 return p.refreshAcknowledgement(ctx, actor, o, previous)
236 }
237 p.mu.Lock()
238 slot := p.slots[o.Host]
239 p.mu.Unlock()
240 if slot == nil {
241 return execlease.ErrInactive
242 }
243 select {
244 case <-slot.Ready:
245 case <-ctx.Done():
246 return ctx.Err()
247 }
248 if slot.Err != nil {
249 return slot.Err
250 }
251 if slot.Controller == nil {
252 return execlease.ErrInactive
253 }
254 state, err := slot.Controller.Ready(ctx)
255 if err != nil {
256 return err
257 }
258 if state.State != "active" || state.Host != o.Host || state.Incarnation != c.Holder.Incarnation || state.Op != c.Op.ID || state.Box != actor.Box || state.Enrollment != actor.Enrollment {
259 return execlease.ErrLost
260 }
261 ack := hostclaim.ExecutionAcknowledgement{Op: state.Op, Enrollment: state.Enrollment, BootID: state.BootID, ControllerID: state.ControllerID, LeaseETag: state.LeaseETag, Expires: state.Expires}
262 // Recorded before the write: an uncertain write that landed is still this
263 // controller's acknowledgement on the next pass.
264 p.mu.Lock()
265 slot.AckController = state.ControllerID
266 p.mu.Unlock()
267 _, err = hostclaim.Update(ctx, p.Store, o, hostclaim.AcknowledgeExecution(actor.Box, ack))
268 return err
269}
270
271// refreshAcknowledgement extends this controller's own acknowledgement for
272// the op while it waits on the console, once fewer than ackRefreshBefore
273// remain. It writes only from the controller's current Ready state, under the
274// same checks as the first acknowledgement, and never on behalf of another
275// boot or controller: an acknowledgement this slot did not write is left to
276// expire.
277//
278// The root broker rotates the controller token on every renewal, so the
279// current state's ControllerID changes every 20 s. The slot is what
280// identifies the same controller; the refreshed acknowledgement keeps the
281// ControllerID the first one bound and carries the current lease ETag and
282// expiry.
283func (p *productionClaimLocal) refreshAcknowledgement(ctx context.Context, actor hostbox.Actor, o hostclaim.Observed, previous string) error {
284 c := o.Claim
285 var prior hostclaim.ExecutionAcknowledgement
286 if json.Unmarshal([]byte(previous), &prior) != nil || prior.Op != c.Op.ID || prior.Enrollment != actor.Enrollment || prior.BootID == "" || prior.ControllerID == "" {
287 return nil
288 }
289 if !p.wallNow().Add(ackRefreshBefore).After(prior.Expires) {
290 return nil
291 }
292 p.mu.Lock()
293 slot := p.slots[o.Host]
294 p.mu.Unlock()
295 if slot == nil {
296 return nil
297 }
298 select {
299 case <-slot.Ready:
300 case <-ctx.Done():
301 return ctx.Err()
302 }
303 p.mu.Lock()
304 owned := slot.AckController
305 p.mu.Unlock()
306 if slot.Err != nil || slot.Controller == nil || owned == "" || owned != prior.ControllerID {
307 return nil
308 }
309 state, err := slot.Controller.Ready(ctx)
310 if err != nil {
311 return err
312 }
313 if state.State != "active" || state.Host != o.Host || state.Incarnation != c.Holder.Incarnation || state.Op != c.Op.ID || state.Box != actor.Box || state.Enrollment != actor.Enrollment {
314 return execlease.ErrLost
315 }
316 if state.BootID != prior.BootID || state.LeaseETag == "" || !state.Expires.After(prior.Expires) {
317 return nil
318 }
319 ack := hostclaim.ExecutionAcknowledgement{Op: prior.Op, Enrollment: prior.Enrollment, BootID: prior.BootID, ControllerID: prior.ControllerID, LeaseETag: state.LeaseETag, Expires: state.Expires}
320 _, err = hostclaim.Update(ctx, p.Store, o, hostclaim.AcknowledgeExecution(actor.Box, ack))
321 return err
322}
323