package activator import ( "context" "errors" "github.com/carlosframework/platform/internal/hostwrite" ) var ( errSwapNotAwake = errors.New("swap: instance not awake") errSwapLeaseLost = errors.New("swap: lease lost") errSwapFenced = errors.New("swap: a stop-evidence operation holds the host") errSwapShutdown = errors.New("swap: agent shutting down") errSwapSuperseded = errors.New("swap: superseded by a newer attempt") errSwapStopped = errors.New("swap: an intentional stop cancelled the attempt") ) // selectWork picks the attempt's lifecycle work through hostwrite, never // hostguard directly (spec §7.4). Claims off gives the legacy work. Claims // on gives the claim-backed work, and a selector that cannot answer is a // refusal, never the legacy work. func (a *Service) selectWork(ctx context.Context, host string) (hostwrite.Work, error) { work, err := a.cfg.HostWrites.Select(ctx, host, "spawn") if err != nil { return nil, err } if work == nil { return nil, hostwrite.ErrUnavailable } return work, nil } // admit runs fn as a §7.4 re-check. Claims off (hostwrite.Legacy): fn // alone. fn takes a.mu itself, so the check is the entry lock against the // barrier, the lease and the local fences that maintenance and moves set. // Claims on: fn runs inside the selected work's With, so the claim and the // installation stamp are validated first. A refusal there is returned as // is and is never retried down the claims-off path. func (a *Service) admit(ctx context.Context, work hostwrite.Work, fn func() error) error { if work == nil { return hostwrite.ErrUnavailable } if hostwrite.Legacy(work) { return fn() } return work.With(ctx, hostwrite.Identity{}, fn) } // swapChecksLocked is the four §7.4 checks on the entry the attempt targets: // the entry is still awake, the lease is still held, no stop-evidence // operation (the local fence) is in progress, and the attempt is still the // newest. The agent's own shutdown counts as a stop in progress. newest is // called with a.mu held. The caller holds a.mu. func (a *Service) swapChecksLocked(e *actEntry, at Attempt, newest func(string) uint64) error { return a.attemptCurrentLocked(e, at, newest, false) } // attemptCurrentLocked is THE attempt gate (spec §6.3, §7.4): every exec // point and every completion path of every attempt asks it, through // swapChecksLocked, memberCurrentLocked, a hold's pinCheck, a claims-on // spawn's before hook and a socketless respawn's attemptGate. waking is a // hold's pinned wake, whose entry is still waking and holds no lease yet: // the entry must be the host's, and the agent, the fences and the revision // are checked. Otherwise the entry must be awake with its lease. The caller // holds a.mu. func (a *Service) attemptCurrentLocked(e *actEntry, at Attempt, newest func(string) uint64, waking bool) error { if e == nil || a.entries[at.Host] != e { return errSwapNotAwake } if err := a.ownedLocked(e, waking); err != nil { return err } if newest(at.Host) != at.SchedRev { return errSwapSuperseded } return nil } // ownedLocked is the gate's OWNERSHIP check, apart from supersession (spec // §7.4). Every exec asks it, an attempt's or not: a wake's and its // fallback's, a worker respawn's (a crash restart, an attempt, its fallback, // its restore) and a legacy sidecar's. Its refusals (errSwapNotAwake, // errSwapLeaseLost, errSwapFenced, errSwapShutdown) are all gateNoOwnership: // nothing spawns, a restore included. waking is an entry still waking, // whose lease the wake holds itself (doWake's lease, not e.rep): the entry // must be the host's, with no fence and no shutdown. The caller holds a.mu. func (a *Service) ownedLocked(e *actEntry, waking bool) error { switch { case e == nil || a.entries[e.host] != e: return errSwapNotAwake case !waking && e.state != actAwake: // swapsim:M5 return errSwapNotAwake case !waking && (e.rep == nil || e.rep.Err() != nil): return errSwapLeaseLost case a.fences[e.host] != 0: return errSwapFenced case a.shuttingDown: return errSwapShutdown } return nil } // memberCurrentLocked is the gate for an attempt with a member in the // operation set (an overlap's successor): the attempt is current, and its // member is still in the set and not cancelled by the barrier. The caller // holds a.mu. func (a *Service) memberCurrentLocked(e *actEntry, m *genMember, mctx context.Context, at Attempt, newest func(string) uint64) error { if err := a.attemptCurrentLocked(e, at, newest, false); err != nil { return err } if e.ops.members[m.gen.N] != m || mctx.Err() != nil { return errSwapStopped } return nil }