carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostexec
2
3import (
4 "bytes"
5 "context"
6 "crypto/rand"
7 "crypto/sha256"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "os"
12 "strings"
13 "sync"
14 "time"
15
16 "github.com/carlosframework/platform/internal/release"
17 "github.com/carlosframework/platform/internal/rootparticipation"
18 "github.com/carlosframework/platform/internal/rootstate"
19)
20
21type ExecutionRequest = rootparticipation.ExecutionRequest
22type ExecutionAuthority = rootparticipation.ExecutionAuthority
23type ExecutionResponse = rootparticipation.ExecutionResponse
24type ExecutionReplay = rootparticipation.ExecutionReplay
25type ExecutionRecord = rootparticipation.ExecutionRecord
26
27type RootBrokerConfig struct {
28 Recovered func(context.Context) error
29 Transition func(context.Context, ExecutionRecord, ExecutionRequest, ExecutionAuthority) error
30 State rootstate.Store
31 Remote release.Store
32 Clock Clock
33 BootID, Identity string
34 Authority func(context.Context, ExecutionRequest, ControllerProcess) (ExecutionAuthority, error)
35 Alive func(context.Context, ControllerProcess) error
36 Cleanup func(context.Context, ExecutionRecord) error
37 Settle func(context.Context, ExecutionRecord) error
38 Exclude func(context.Context, func() error) error
39}
40
41type RootBroker struct {
42 cfg RootBrokerConfig
43 mu sync.Mutex
44 workers sync.WaitGroup
45 stopping bool
46 disposed bool
47 joined chan struct{}
48 closeGate chan struct{}
49 lifetimeCtx context.Context
50 cancel context.CancelFunc
51 closeErr error
52 denied map[string]bool
53 closed bool
54 calls map[string]chan struct{}
55 lifetime map[string]func()
56 live map[string]func()
57 retryAfter map[string]int64
58 retryDelay map[string]time.Duration
59}
60
61func NewRootBroker(cfg RootBrokerConfig) (*RootBroker, error) {
62 if cfg.State == nil || cfg.Remote == nil || cfg.Clock == nil || !validID(cfg.BootID) || !validID(cfg.Identity) || cfg.Authority == nil || cfg.Alive == nil || cfg.Cleanup == nil || cfg.Settle == nil || cfg.Exclude == nil {
63 return nil, ErrState
64 }
65 lifetimeCtx, cancel := context.WithCancel(context.Background())
66 return &RootBroker{cfg: cfg, lifetimeCtx: lifetimeCtx, cancel: cancel, closeGate: make(chan struct{}, 1), denied: make(map[string]bool), calls: make(map[string]chan struct{}), lifetime: make(map[string]func()), live: make(map[string]func()), retryAfter: make(map[string]int64), retryDelay: make(map[string]time.Duration)}, nil
67}
68
69func (b *RootBroker) enter(ctx context.Context) (context.Context, func(), error) {
70 b.mu.Lock()
71 if b.stopping {
72 b.mu.Unlock()
73 return nil, nil, ErrLost
74 }
75 b.workers.Add(1)
76 b.mu.Unlock()
77 work, cancel := context.WithCancel(ctx)
78 stop := context.AfterFunc(b.lifetimeCtx, cancel)
79 return work, func() { stop(); cancel(); b.workers.Done() }, nil
80}
81
82func executionDigest(q ExecutionRequest, p ControllerProcess) string {
83 b, _ := json.Marshal(struct {
84 Request ExecutionRequest
85 Peer ControllerProcess
86 }{q, p})
87 s := sha256.Sum256(b)
88 return hex.EncodeToString(s[:])
89}
90
91func (b *RootBroker) read(host string) (ExecutionRecord, error) {
92 b.mu.Lock()
93 defer b.mu.Unlock()
94 raw, err := b.cfg.State.Read(host + ".json")
95 if err != nil {
96 return ExecutionRecord{}, err
97 }
98 var r ExecutionRecord
99 if decode(raw, &r) != nil || r.Schema != 1 || r.Request.Host != host || !r.Request.Valid() || !validID(r.Token) || r.Authority.Migration == 0 || r.Replay == nil {
100 return ExecutionRecord{}, ErrState
101 }
102 switch r.Status {
103 case "pending", "active", "lost", "settled":
104 default:
105 return ExecutionRecord{}, ErrState
106 }
107 return r, nil
108}
109
110func (b *RootBroker) write(r ExecutionRecord) error {
111 b.mu.Lock()
112 defer b.mu.Unlock()
113 if r.Status == "active" && (b.closed || b.denied[r.Request.Host]) {
114 return ErrLost
115 }
116 if release := b.live[r.Request.Host]; release != nil {
117 release()
118 delete(b.live, r.Request.Host)
119 }
120 raw, err := json.Marshal(r)
121 if err != nil {
122 return err
123 }
124 if err := b.cfg.State.Write(r.Request.Host+".json", raw); err != nil {
125 return err
126 }
127 if r.Status == "active" {
128 release, err := rootstate.HoldLive(b.cfg.State, r.Request.Host+".live")
129 if err != nil {
130 return err
131 }
132 b.live[r.Request.Host] = release
133 }
134 return nil
135}
136
137func executionResponse(r ExecutionRecord) ExecutionResponse {
138 q := r.Request
139 return ExecutionResponse{Schema: 1, Host: q.Host, Incarnation: q.Incarnation, Stamp: q.Stamp, Session: q.Session, Op: q.Op, Token: r.Token, BootID: r.Peer.KernelBootID, Status: r.Status, SafeUntil: r.SafeUntil, LeaseETag: r.LeaseETag, Expires: r.Expires}
140}
141
142func (b *RootBroker) active(ctx context.Context, r ExecutionRecord) error {
143 b.mu.Lock()
144 denied := b.closed || b.denied[r.Request.Host]
145 b.mu.Unlock()
146 if denied || r.Status != "active" || r.Broker != b.cfg.Identity || r.Peer.KernelBootID != b.cfg.BootID {
147 return ErrLost
148 }
149 _, now, err := b.cfg.Clock.Now()
150 if err != nil {
151 return err
152 }
153 if now >= r.SafeUntil {
154 return ErrLost
155 }
156 return b.cfg.Alive(ctx, r.Peer)
157}
158
159func sameExecution(q ExecutionRequest, r ExecutionRecord) bool {
160 p := r.Request
161 return q.Host == p.Host && q.Incarnation == p.Incarnation && q.Stamp == p.Stamp && q.Session == p.Session && q.Op == p.Op
162}
163
164func (b *RootBroker) deny(ctx context.Context, r ExecutionRecord, cause error) error {
165 b.mu.Lock()
166 raw, readErr := b.cfg.State.Read(r.Request.Host + ".json")
167 var current ExecutionRecord
168 if readErr == nil && decode(raw, &current) == nil && ((current.Token != r.Token && current.Token != r.TransitionFrom) || current.Status == "settled") {
169 b.mu.Unlock()
170 return cause
171 }
172 if readErr == nil && current.Token == r.Token && current.RequestStart > r.RequestStart {
173 r = current
174 }
175 b.denied[r.Request.Host] = true
176 if release := b.live[r.Request.Host]; release != nil {
177 release()
178 delete(b.live, r.Request.Host)
179 }
180 r.Status = "lost"
181 raw, persist := json.Marshal(r)
182 if persist == nil {
183 persist = b.cfg.State.Write(r.Request.Host+".json", raw)
184 }
185 if persist != nil {
186 b.closed = true
187 for host, release := range b.live {
188 release()
189 delete(b.live, host)
190 }
191 }
192 b.mu.Unlock()
193 cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
194 defer cancel()
195 clean := b.cfg.Cleanup(cleanup, r)
196 return errors.Join(cause, persist, clean)
197}
198
199func (b *RootBroker) Execute(ctx context.Context, peer ControllerProcess, q ExecutionRequest) (ExecutionResponse, error) {
200 if !q.Valid() || peer.PID <= 0 || !validID(peer.Start) || peer.KernelBootID != b.cfg.BootID {
201 return ExecutionResponse{}, ErrState
202 }
203 ctx, leave, enterErr := b.enter(ctx)
204 if enterErr != nil {
205 return ExecutionResponse{}, enterErr
206 }
207 defer leave()
208 if q.Operation != "executionSettle" {
209 if err := b.cfg.Alive(ctx, peer); err != nil {
210 return ExecutionResponse{}, err
211 }
212 }
213 b.mu.Lock()
214 gate := b.calls[q.Host]
215 if gate == nil {
216 if len(b.calls) >= 4096 {
217 b.mu.Unlock()
218 return ExecutionResponse{}, ErrState
219 }
220 gate = make(chan struct{}, 1)
221 b.calls[q.Host] = gate
222 }
223 b.mu.Unlock()
224 select {
225 case gate <- struct{}{}:
226 case <-ctx.Done():
227 return ExecutionResponse{}, ctx.Err()
228 }
229 defer func() { <-gate }()
230 b.mu.Lock()
231 held := b.lifetime[q.Host] != nil
232 b.mu.Unlock()
233 if !held {
234 unlock, lockErr := b.cfg.State.Lock(ctx, q.Host+".lock")
235 if lockErr != nil {
236 return ExecutionResponse{}, lockErr
237 }
238 b.mu.Lock()
239 b.lifetime[q.Host] = unlock
240 b.mu.Unlock()
241 }
242 if err := ctx.Err(); err != nil {
243 return ExecutionResponse{}, err
244 }
245 // Settlement verifies retained obligations even after current admission ends.
246 var authority ExecutionAuthority
247 var err error
248 if q.Operation != "executionSettle" {
249 cutoff := int64(0)
250 if prior, readErr := b.read(q.Host); readErr == nil && prior.Status == "active" {
251 cutoff = prior.SafeUntil
252 }
253 err = b.retry(ctx, cutoff, func(call context.Context) error { var e error; authority, e = b.cfg.Authority(call, q, peer); return e })
254 if err == nil && (authority.Migration == 0 || (authority.Installation == "" && !authority.ParticipationOnly) || !validID(authority.Box) || !validID(authority.Enrollment) || authority.Account == "" || authority.App == "" || authority.ClaimRevision == "" || authority.RecordRevision == "" || authority.DirectoryRevision == "") {
255 err = ErrState
256 }
257 if err != nil {
258 if prior, readErr := b.read(q.Host); readErr == nil && prior.Peer == peer && (q.Token == prior.Token || q.ID == prior.Request.ID) && prior.Status != "settled" {
259 return ExecutionResponse{}, b.deny(ctx, prior, err)
260 }
261 return ExecutionResponse{}, err
262 }
263 if q.Operation == "executionView" && q.Token == "" {
264 return b.servingView(ctx, peer, q, authority)
265 }
266 if authority.Kind == "serving" {
267 return ExecutionResponse{}, ErrInactive
268 }
269 }
270 r, err := b.read(q.Host)
271 missing := errors.Is(err, os.ErrNotExist)
272 if err != nil && !missing {
273 return ExecutionResponse{}, err
274 }
275 digest := executionDigest(q, peer)
276 if !missing {
277 if replay, ok := r.Replay[q.ID]; ok {
278 if replay.Digest != digest {
279 return ExecutionResponse{}, ErrState
280 }
281 if q.Operation == "executionSettle" && replay.Response.Status == "settled" {
282 return replay.Response, nil
283 }
284 if !sameExecution(q, r) || replay.Response.Token != r.Token {
285 return ExecutionResponse{}, ErrLost
286 }
287 if err = b.active(ctx, r); err != nil {
288 return ExecutionResponse{}, b.deny(ctx, r, err)
289 }
290 if r.Peer != peer || r.Authority != authority {
291 return ExecutionResponse{}, b.deny(ctx, r, ErrLost)
292 }
293 return replay.Response, nil
294 }
295 }
296 if q.Operation == "executionBegin" {
297 b.mu.Lock()
298 closed := b.closed
299 b.mu.Unlock()
300 if closed || (!missing && (r.Status != "settled" || sameExecution(q, r))) {
301 return ExecutionResponse{}, ErrLost
302 }
303 var token [32]byte
304 if _, err = rand.Read(token[:]); err != nil {
305 return ExecutionResponse{}, err
306 }
307 history := make(map[string]ExecutionReplay)
308 for id, replay := range r.Replay {
309 if replay.Response.Status == "settled" && replay.Response.Token == r.Token {
310 history[id] = replay
311 }
312 }
313 b.mu.Lock()
314 delete(b.denied, q.Host)
315 delete(b.retryAfter, q.Host)
316 delete(b.retryDelay, q.Host)
317 b.mu.Unlock()
318 r = ExecutionRecord{Schema: 1, Request: q, Peer: peer, Authority: authority, Broker: b.cfg.Identity, Token: hex.EncodeToString(token[:]), Status: "pending", Replay: history}
319 wall, start, e := b.cfg.Clock.Now()
320 if e != nil {
321 return ExecutionResponse{}, e
322 }
323 r.PriorSafeUntil = r.SafeUntil
324 r.RequestStart = start
325 r.SafeUntil = start + int64(TTL-Margin)
326 r.Expires = wall.Add(TTL)
327 r.Replay[q.ID] = ExecutionReplay{Digest: digest}
328 if err = b.cfg.Exclude(ctx, func() error { return b.write(r) }); err != nil {
329 return ExecutionResponse{}, err
330 }
331 return b.acquire(ctx, peer, q, r, digest, true)
332 }
333 if missing || r.Peer != peer || q.Token != r.Token {
334 return ExecutionResponse{}, ErrLost
335 }
336 if !sameExecution(q, r) {
337 if q.Operation != "executionRenew" || b.cfg.Transition == nil || q.Host != r.Request.Host || q.Op != r.Request.Op || q.Incarnation != r.Request.Incarnation {
338 return ExecutionResponse{}, ErrLost
339 }
340 if err = b.active(ctx, r); err != nil {
341 return ExecutionResponse{}, b.deny(ctx, r, err)
342 }
343 if err = b.cfg.Transition(ctx, r, q, authority); err != nil {
344 return ExecutionResponse{}, b.deny(ctx, r, err)
345 }
346 var token [32]byte
347 if _, err = rand.Read(token[:]); err != nil {
348 return ExecutionResponse{}, err
349 }
350 r.Request = q
351 r.TransitionFrom = r.Token
352 r.Token = hex.EncodeToString(token[:])
353 r.Authority = authority
354 return b.acquire(ctx, peer, q, r, digest, false)
355 }
356 if q.Operation == "executionSettle" {
357 return b.settle(ctx, q, r, digest)
358 }
359 if err = b.active(ctx, r); err != nil {
360 return ExecutionResponse{}, b.deny(ctx, r, err)
361 }
362 if authority != r.Authority {
363 if !executionInstallationBinding(r.Authority, authority) {
364 if (q.Operation != "executionRenew" && q.Operation != "executionView") || b.cfg.Transition == nil || b.cfg.Transition(ctx, r, q, authority) != nil {
365 return ExecutionResponse{}, b.deny(ctx, r, ErrLost)
366 }
367 r.Authority = authority
368 if q.Operation == "executionView" {
369 if err = b.write(r); err != nil {
370 return ExecutionResponse{}, b.deny(ctx, r, err)
371 }
372 return executionResponse(r), nil
373 }
374 return b.acquire(ctx, peer, q, r, digest, false)
375 }
376 r.Authority = authority
377 if err = b.write(r); err != nil {
378 return ExecutionResponse{}, b.deny(ctx, r, err)
379 }
380 }
381 if q.Operation == "executionView" {
382 return executionResponse(r), nil
383 }
384 return b.acquire(ctx, peer, q, r, digest, false)
385}
386
387func (b *RootBroker) acquire(ctx context.Context, peer ControllerProcess, q ExecutionRequest, r ExecutionRecord, digest string, begin bool) (ExecutionResponse, error) {
388 key, err := Key(q.Host, r.Authority.Box)
389 if err != nil {
390 return ExecutionResponse{}, err
391 }
392 etag := r.LeaseETag
393 if begin {
394 raw, existing, e := b.cfg.Remote.Get(ctx, key)
395 if e == nil {
396 var remote Record
397 wall, _, clockErr := b.cfg.Clock.Now()
398 if decode(raw, &remote) != nil || remote.Expires.IsZero() || !wall.After(remote.Expires) || existing == "" || clockErr != nil {
399 return ExecutionResponse{}, b.deny(ctx, r, ErrLost)
400 }
401 etag = existing
402 } else if !errors.Is(e, os.ErrNotExist) {
403 return ExecutionResponse{}, b.deny(ctx, r, e)
404 }
405 } else {
406 if r.Token == q.Token {
407 var token [32]byte
408 if _, err := rand.Read(token[:]); err != nil {
409 return ExecutionResponse{}, err
410 }
411 r.TransitionFrom, r.Token = r.Token, hex.EncodeToString(token[:])
412 }
413 priorID := ""
414 var prior ExecutionReplay
415 for id, replay := range r.Replay {
416 if replay.Response.Status == "settled" && (priorID == "" || replay.Response.Expires.After(prior.Response.Expires) || replay.Response.Expires.Equal(prior.Response.Expires) && id > priorID) {
417 priorID, prior = id, replay
418 }
419 }
420 r.Replay = make(map[string]ExecutionReplay)
421 if priorID != "" {
422 r.Replay[priorID] = prior
423 }
424 wall, start, e := b.cfg.Clock.Now()
425 if e != nil {
426 return ExecutionResponse{}, b.deny(ctx, r, e)
427 }
428 r.PriorSafeUntil = r.SafeUntil
429 r.RequestStart = start
430 r.SafeUntil = start + int64(TTL-Margin)
431 r.Expires = wall.Add(TTL)
432 r.Status = "pending"
433 r.Replay[q.ID] = ExecutionReplay{Digest: digest}
434 if e = b.cfg.Exclude(ctx, func() error { return b.write(r) }); e != nil {
435 return ExecutionResponse{}, b.deny(ctx, r, e)
436 }
437 }
438 raw, _ := json.Marshal(Record{Op: q.Op, Expires: r.Expires})
439 var next string
440 cutoff := r.SafeUntil
441 if r.PriorSafeUntil > 0 && r.PriorSafeUntil < cutoff {
442 cutoff = r.PriorSafeUntil
443 }
444 uncertain := false
445 err = b.retry(ctx, cutoff, func(call context.Context) error {
446 if uncertain {
447 found, current, e := b.cfg.Remote.Get(call, key)
448 if e == nil && bytes.Equal(found, raw) && current != "" {
449 next = current
450 return nil
451 }
452 if e != nil && !errors.Is(e, os.ErrNotExist) {
453 return e
454 }
455 if e == nil && current != etag {
456 return ErrLost
457 }
458 }
459 var e error
460 next, e = b.cfg.Remote.Put(call, key, raw, etag)
461 if e != nil && !errors.Is(e, release.ErrCASConflict) {
462 uncertain = true
463 }
464 return e
465 })
466 if err != nil || next == "" {
467 return ExecutionResponse{}, b.deny(ctx, r, errors.Join(err, ErrLost))
468 }
469 r.LeaseETag = next
470 var authority ExecutionAuthority
471 err = b.retry(ctx, cutoff, func(call context.Context) error { var e error; authority, e = b.cfg.Authority(call, q, peer); return e })
472 if err != nil || authority != r.Authority {
473 return ExecutionResponse{}, b.deny(ctx, r, errors.Join(err, ErrLost))
474 }
475 _, now, err := b.cfg.Clock.Now()
476 if err != nil || now >= r.SafeUntil || (!begin && r.PriorSafeUntil > 0 && now >= r.PriorSafeUntil) || ctx.Err() != nil {
477 return ExecutionResponse{}, b.deny(ctx, r, errors.Join(err, ErrLost))
478 }
479 if err = b.cfg.Alive(ctx, peer); err != nil {
480 return ExecutionResponse{}, b.deny(ctx, r, err)
481 }
482 r.PriorSafeUntil = 0
483 r.Status = "active"
484 out := executionResponse(r)
485 r.Replay[q.ID] = ExecutionReplay{Digest: digest, Response: out}
486 if err = b.write(r); err != nil {
487 return ExecutionResponse{}, b.deny(ctx, r, err)
488 }
489 if err = b.active(ctx, r); err != nil {
490 return ExecutionResponse{}, b.deny(ctx, r, err)
491 }
492 return out, nil
493}
494
495func (b *RootBroker) settle(ctx context.Context, q ExecutionRequest, r ExecutionRecord, digest string) (ExecutionResponse, error) {
496 if r.Status == "settled" {
497 return executionResponse(r), nil
498 }
499 if err := b.deny(ctx, r, nil); err != nil {
500 return ExecutionResponse{}, err
501 }
502 r.Status = "lost"
503 key, err := Key(q.Host, r.Authority.Box)
504 if err != nil {
505 return ExecutionResponse{}, err
506 }
507 deletionUncertain := r.LeaseETag == ""
508 if !deletionUncertain {
509 err = release.DeleteIfMatch(ctx, b.cfg.Remote, key, r.LeaseETag)
510 if errors.Is(err, release.ErrCASConflict) {
511 deletionUncertain = true
512 } else if err != nil && !errors.Is(err, os.ErrNotExist) {
513 return ExecutionResponse{}, err
514 }
515 }
516 if deletionUncertain {
517 raw, currentETag, readErr := b.cfg.Remote.Get(ctx, key)
518 wall, _, clockErr := b.cfg.Clock.Now()
519 if clockErr != nil {
520 return ExecutionResponse{}, clockErr
521 }
522 if !wall.After(r.Expires) {
523 return ExecutionResponse{}, ErrLost
524 }
525 if readErr == nil {
526 var remote Record
527 if decode(raw, &remote) != nil || remote.Expires.IsZero() || !wall.After(remote.Expires) || currentETag == "" {
528 return ExecutionResponse{}, ErrLost
529 }
530 if err := release.DeleteIfMatch(ctx, b.cfg.Remote, key, currentETag); err != nil {
531 return ExecutionResponse{}, err
532 }
533 } else if !errors.Is(readErr, os.ErrNotExist) {
534 return ExecutionResponse{}, readErr
535 }
536 }
537 if err = b.cfg.Settle(ctx, r); err != nil {
538 return ExecutionResponse{}, err
539 }
540 r.Status = "settled"
541 out := executionResponse(r)
542 r.Replay = make(map[string]ExecutionReplay)
543 r.Replay[q.ID] = ExecutionReplay{Digest: digest, Response: out}
544 if err = b.write(r); err != nil {
545 return ExecutionResponse{}, err
546 }
547 return out, nil
548}
549
550func (b *RootBroker) Monitor(ctx context.Context) error {
551 ctx, leave, err := b.enter(ctx)
552 if err != nil {
553 return err
554 }
555 defer leave()
556 names, err := b.cfg.State.Names()
557 if err != nil {
558 return err
559 }
560 var errs []error
561 for _, name := range names {
562 if name == "census.json" || name == "update.json" || !strings.HasSuffix(name, ".json") {
563 continue
564 }
565 host := strings.TrimSuffix(name, ".json")
566 b.mu.Lock()
567 owned := b.lifetime[host] != nil
568 b.mu.Unlock()
569 if !owned {
570 continue
571 }
572 r, err := b.read(host)
573 if err != nil {
574 errs = append(errs, err)
575 continue
576 }
577 if r.Status == "settled" {
578 continue
579 }
580 if r.Status == "lost" {
581 _, now, clockErr := b.cfg.Clock.Now()
582 if clockErr != nil {
583 errs = append(errs, clockErr)
584 continue
585 }
586 b.mu.Lock()
587 next := b.retryAfter[host]
588 b.mu.Unlock()
589 if now < next {
590 continue
591 }
592 err := b.recoverSettlement(ctx, host)
593 b.mu.Lock()
594 if err != nil {
595 delay := b.retryDelay[host] * 2
596 if delay < time.Second {
597 delay = time.Second
598 }
599 if delay > 30*time.Second {
600 delay = 30 * time.Second
601 }
602 b.retryDelay[host], b.retryAfter[host] = delay, now+int64(delay)
603 } else {
604 delete(b.retryDelay, host)
605 delete(b.retryAfter, host)
606 }
607 b.mu.Unlock()
608 errs = append(errs, err)
609 continue
610 }
611 _, now, clockErr := b.cfg.Clock.Now()
612 bounded, cancel := context.WithTimeout(ctx, time.Second)
613 aliveErr := b.cfg.Alive(bounded, r.Peer)
614 cancel()
615 cutoff := r.SafeUntil
616 if r.Status == "pending" && r.PriorSafeUntil > 0 && r.PriorSafeUntil < cutoff {
617 cutoff = r.PriorSafeUntil
618 }
619 if clockErr != nil || aliveErr != nil || now >= cutoff || r.Broker != b.cfg.Identity || r.Peer.KernelBootID != b.cfg.BootID {
620 errs = append(errs, b.deny(ctx, r, ErrLost))
621 }
622 }
623 if b.cfg.Recovered != nil {
624 errs = append(errs, b.cfg.Recovered(ctx))
625 }
626 return errors.Join(errs...)
627}
628
629func executionInstallationBinding(old, next ExecutionAuthority) bool {
630 if !old.ParticipationOnly || (old.Installation != "" && (old.Installation != next.Installation || next.ParticipationOnly)) || next.Installation == "" {
631 return false
632 }
633 old.Installation = next.Installation
634 old.ParticipationOnly = next.ParticipationOnly
635 return old == next
636}
637
638func (b *RootBroker) Close(ctx context.Context) error {
639 b.mu.Lock()
640 b.closed = true
641 if !b.stopping {
642 b.stopping = true
643 b.cancel()
644 for host, unlock := range b.live {
645 unlock()
646 delete(b.live, host)
647 }
648 b.joined = make(chan struct{})
649 go func() { b.workers.Wait(); close(b.joined) }()
650 }
651 joined := b.joined
652 b.mu.Unlock()
653 select {
654 case <-joined:
655 case <-ctx.Done():
656 return ctx.Err()
657 }
658 select {
659 case b.closeGate <- struct{}{}:
660 case <-ctx.Done():
661 return ctx.Err()
662 }
663 defer func() { <-b.closeGate }()
664 if b.disposed {
665 return b.closeErr
666 }
667 names, err := b.cfg.State.Names()
668 errs := []error{err}
669 for _, name := range names {
670 if name == "census.json" || name == "update.json" || !strings.HasSuffix(name, ".json") {
671 continue
672 }
673 r, e := b.read(strings.TrimSuffix(name, ".json"))
674 if e != nil {
675 errs = append(errs, e)
676 continue
677 }
678 if r.Broker == b.cfg.Identity && r.Status != "settled" {
679 errs = append(errs, b.deny(ctx, r, nil))
680 }
681 }
682 b.mu.Lock()
683 for host, unlock := range b.lifetime {
684 unlock()
685 delete(b.lifetime, host)
686 }
687 b.mu.Unlock()
688 b.disposed = true
689 b.closeErr = errors.Join(errs...)
690 return b.closeErr
691}
692
693func (b *RootBroker) Recover(ctx context.Context) error {
694 ctx, leave, err := b.enter(ctx)
695 if err != nil {
696 return err
697 }
698 defer leave()
699 names, err := b.cfg.State.Names()
700 if err != nil {
701 return err
702 }
703 var errs []error
704 for _, name := range names {
705 if name == "census.json" || name == "update.json" || !strings.HasSuffix(name, ".json") {
706 continue
707 }
708 host := strings.TrimSuffix(name, ".json")
709 b.mu.Lock()
710 owned := b.lifetime[host] != nil
711 b.mu.Unlock()
712 if !owned {
713 unlock, e := b.cfg.State.Lock(ctx, host+".lock")
714 if e != nil {
715 return e
716 }
717 b.mu.Lock()
718 b.lifetime[host] = unlock
719 b.mu.Unlock()
720 }
721 r, e := b.read(host)
722 if e != nil {
723 errs = append(errs, e)
724 continue
725 }
726 if r.Status != "settled" {
727 errs = append(errs, b.recoverSettlement(ctx, host))
728 }
729 }
730 if b.cfg.Recovered != nil {
731 errs = append(errs, b.cfg.Recovered(ctx))
732 }
733 return errors.Join(errs...)
734}
735
736func (b *RootBroker) recoverSettlement(ctx context.Context, host string) error {
737 b.mu.Lock()
738 gate := b.calls[host]
739 if gate == nil {
740 gate = make(chan struct{}, 1)
741 b.calls[host] = gate
742 }
743 b.mu.Unlock()
744 select {
745 case gate <- struct{}{}:
746 default:
747 return nil
748 }
749 defer func() { <-gate }()
750 r, err := b.read(host)
751 if err != nil || r.Status == "settled" {
752 return err
753 }
754 q := r.Request
755 q.Operation, q.Token, q.ID = "executionSettle", r.Token, "recovery-"+r.Token
756 _, err = b.settle(ctx, q, r, executionDigest(q, r.Peer))
757 return err
758}
759
760func (b *RootBroker) Run(ctx context.Context) {
761 ctx, leave, err := b.enter(ctx)
762 if err != nil {
763 return
764 }
765 defer leave()
766 for {
767 select {
768 case <-ctx.Done():
769 return
770 case <-b.lifetimeCtx.Done():
771 return
772 case <-b.cfg.Clock.After(100 * time.Millisecond):
773 }
774 _ = b.Monitor(ctx)
775 }
776}
777
778func (b *RootBroker) retry(ctx context.Context, cutoff int64, fn func(context.Context) error) error {
779 _, now, err := b.cfg.Clock.Now()
780 if err != nil {
781 return err
782 }
783 slack := time.Second
784 if cutoff != 0 {
785 slack = time.Duration(cutoff - now)
786 if slack <= 0 {
787 return ErrLost
788 }
789 }
790 bounded, cancel := context.WithTimeout(ctx, slack)
791 defer cancel()
792 delay := 25 * time.Millisecond
793 for {
794 err = fn(bounded)
795 if err == nil || errors.Is(err, ErrState) || errors.Is(err, ErrInactive) || errors.Is(err, ErrLost) || errors.Is(err, ErrUnsupported) || errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrNotExist) || errors.Is(err, release.ErrCASConflict) {
796 return err
797 }
798 _, now, clockErr := b.cfg.Clock.Now()
799 if clockErr != nil || cutoff != 0 && now >= cutoff {
800 return errors.Join(err, clockErr, ErrLost)
801 }
802 timer := time.NewTimer(delay)
803 select {
804 case <-bounded.Done():
805 timer.Stop()
806 return errors.Join(err, bounded.Err())
807 case <-timer.C:
808 }
809 if delay < 500*time.Millisecond {
810 delay *= 2
811 }
812 }
813}
814