carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "errors"
6
7 "github.com/carlosframework/platform/internal/hostwrite"
8)
9
10var (
11 errSwapNotAwake = errors.New("swap: instance not awake")
12 errSwapLeaseLost = errors.New("swap: lease lost")
13 errSwapFenced = errors.New("swap: a stop-evidence operation holds the host")
14 errSwapShutdown = errors.New("swap: agent shutting down")
15 errSwapSuperseded = errors.New("swap: superseded by a newer attempt")
16 errSwapStopped = errors.New("swap: an intentional stop cancelled the attempt")
17)
18
19// selectWork picks the attempt's lifecycle work through hostwrite, never
20// hostguard directly (spec §7.4). Claims off gives the legacy work. Claims
21// on gives the claim-backed work, and a selector that cannot answer is a
22// refusal, never the legacy work.
23func (a *Service) selectWork(ctx context.Context, host string) (hostwrite.Work, error) {
24 work, err := a.cfg.HostWrites.Select(ctx, host, "spawn")
25 if err != nil {
26 return nil, err
27 }
28 if work == nil {
29 return nil, hostwrite.ErrUnavailable
30 }
31 return work, nil
32}
33
34// admit runs fn as a §7.4 re-check. Claims off (hostwrite.Legacy): fn
35// alone. fn takes a.mu itself, so the check is the entry lock against the
36// barrier, the lease and the local fences that maintenance and moves set.
37// Claims on: fn runs inside the selected work's With, so the claim and the
38// installation stamp are validated first. A refusal there is returned as
39// is and is never retried down the claims-off path.
40func (a *Service) admit(ctx context.Context, work hostwrite.Work, fn func() error) error {
41 if work == nil {
42 return hostwrite.ErrUnavailable
43 }
44 if hostwrite.Legacy(work) {
45 return fn()
46 }
47 return work.With(ctx, hostwrite.Identity{}, fn)
48}
49
50// swapChecksLocked is the four §7.4 checks on the entry the attempt targets:
51// the entry is still awake, the lease is still held, no stop-evidence
52// operation (the local fence) is in progress, and the attempt is still the
53// newest. The agent's own shutdown counts as a stop in progress. newest is
54// called with a.mu held. The caller holds a.mu.
55func (a *Service) swapChecksLocked(e *actEntry, at Attempt, newest func(string) uint64) error {
56 return a.attemptCurrentLocked(e, at, newest, false)
57}
58
59// attemptCurrentLocked is THE attempt gate (spec §6.3, §7.4): every exec
60// point and every completion path of every attempt asks it, through
61// swapChecksLocked, memberCurrentLocked, a hold's pinCheck, a claims-on
62// spawn's before hook and a socketless respawn's attemptGate. waking is a
63// hold's pinned wake, whose entry is still waking and holds no lease yet:
64// the entry must be the host's, and the agent, the fences and the revision
65// are checked. Otherwise the entry must be awake with its lease. The caller
66// holds a.mu.
67func (a *Service) attemptCurrentLocked(e *actEntry, at Attempt, newest func(string) uint64, waking bool) error {
68 if e == nil || a.entries[at.Host] != e {
69 return errSwapNotAwake
70 }
71 if err := a.ownedLocked(e, waking); err != nil {
72 return err
73 }
74 if newest(at.Host) != at.SchedRev {
75 return errSwapSuperseded
76 }
77 return nil
78}
79
80// ownedLocked is the gate's OWNERSHIP check, apart from supersession (spec
81// §7.4). Every exec asks it, an attempt's or not: a wake's and its
82// fallback's, a worker respawn's (a crash restart, an attempt, its fallback,
83// its restore) and a legacy sidecar's. Its refusals (errSwapNotAwake,
84// errSwapLeaseLost, errSwapFenced, errSwapShutdown) are all gateNoOwnership:
85// nothing spawns, a restore included. waking is an entry still waking,
86// whose lease the wake holds itself (doWake's lease, not e.rep): the entry
87// must be the host's, with no fence and no shutdown. The caller holds a.mu.
88func (a *Service) ownedLocked(e *actEntry, waking bool) error {
89 switch {
90 case e == nil || a.entries[e.host] != e:
91 return errSwapNotAwake
92 case !waking && e.state != actAwake: // swapsim:M5
93 return errSwapNotAwake
94 case !waking && (e.rep == nil || e.rep.Err() != nil):
95 return errSwapLeaseLost
96 case a.fences[e.host] != 0:
97 return errSwapFenced
98 case a.shuttingDown:
99 return errSwapShutdown
100 }
101 return nil
102}
103
104// memberCurrentLocked is the gate for an attempt with a member in the
105// operation set (an overlap's successor): the attempt is current, and its
106// member is still in the set and not cancelled by the barrier. The caller
107// holds a.mu.
108func (a *Service) memberCurrentLocked(e *actEntry, m *genMember, mctx context.Context, at Attempt, newest func(string) uint64) error {
109 if err := a.attemptCurrentLocked(e, at, newest, false); err != nil {
110 return err
111 }
112 if e.ops.members[m.gen.N] != m || mctx.Err() != nil {
113 return errSwapStopped
114 }
115 return nil
116}
117