package host import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "os/exec" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" ) // OperationStart is one writer admitted under a move op kind on this // controller: the admitted observation, the start policy it was admitted // under, the selected inputs and the pre-launch record's controller tuple // (box, enrollment, boot, controller, session). The claim builds it once per // Prepare; an OperationAttempts implementation never constructs one. type OperationStart struct { Admitted hostclaim.Observed Policy activator.StartPolicy Inputs ClaimLaunchInputs Owner hostguard.FenceTarget } // OperationAttempts is the moves-owned attempt journal for a writer admitted // under landing (claim Plan 9 D2, moves ruling H2), and region HA's for a // probe admitted under relocating/probing (claim Plan 11 R1). It is the operation // analogue of activator.ServingRetries: the claim binds its methods into // activator.ClaimStart for the exact admitted session, and moves journals, // counts and settles the attempts in its own record. Every method must refuse // with hostbox.ErrChanged once the claim or the moves record no longer names // s.Admitted's op, phase step, session, stamp and generation. type OperationAttempts interface { // OpenProbe journals the attempt's first app-start time before execution // and returns that same time on every later start, including after a // controller restart. It never resets the opened time. Probes only. OpenProbe(ctx context.Context, s OperationStart, proposed time.Time) (time.Time, error) // CurrentAttempt returns the exact stable value last journalled by Reserve. // It never generates IDs; repeated reads between Reserve calls are identical. // CurrentAttempt is the reservation the role launcher journals for the // next start: ID and InvocationID unique per start, Session, Box, // Enrollment, BootID and ControllerID equal to s.Owner's, and // LaunchInputs equal to ClaimLaunchDigest(s.Inputs). CurrentAttempt(s OperationStart) hostclaim.WakeRetryAttempt // Reserve(0) validates the attempt journalled for this admission. Reserve // with n > 0 journals start n of the same admission before it runs. Reserve(ctx context.Context, s OperationStart, n int) error // Failed settles start n after its process was stopped and drained. Failed(ctx context.Context, s OperationStart, n int) error // Healthy accepts start n after readiness, replication and publication. // again re-runs the op kind's own readiness check against cmd, for the // probe's settle and second request (moves ยง7.7). Healthy(ctx context.Context, s OperationStart, n int, cmd *exec.Cmd, again func(context.Context) error) error } // ClaimProbeReadiness is the probe kind's readiness check (claim Plan 9 D3, // moves ruling H12). roleCgroup is the absolute cgroup v2 directory of the // started main role, resolved from its journalled reservation. Serving and a // landing wake after the commit keep ClaimWakeAssembly.Ready. type ClaimProbeReadiness func(ctx context.Context, cmd *exec.Cmd, rt registry.Route, in ClaimLaunchInputs, roleCgroup string) error // ClaimLaunchDigest names the launch inputs an attempt reservation binds: the // identity, executable, digest, arguments and environment, never the // admission. Serving retries and operation attempts use the same digest. func ClaimLaunchDigest(in ClaimLaunchInputs) (string, error) { raw, err := json.Marshal(struct { Identity hostwrite.Identity Executable, Digest string Args, Env []string }{in.Identity, in.Executable, in.ExecutableSHA256, in.Args, in.Env}) if err != nil { return "", err } digest := sha256.Sum256(raw) return hex.EncodeToString(digest[:]), nil } // claimOperationRefusal is main's typed refusal for every operation or probe // admission the bound moves C1 owners do not cover. func claimOperationRefusal() error { return &ClaimDependencyError{Contracts: []string{"moves C1 operation attempt journal and prepared-open adapter"}} } // ClaimOperationOwner is one op kind's attempt journal and probe readiness // check (claim Plan 11 R1). ProbeReady nil refuses that kind's probes. type ClaimOperationOwner struct { Attempts OperationAttempts ProbeReady ClaimProbeReadiness } // claimOperationRouter is the wake's OperationAttempts when more than one op // kind admits operation writers: landing to the moves journal, relocating to // region HA's. Each call goes to the admitted observation's kind; any other // kind is refused before any journal is touched. With region HA unbound the // wake keeps Plan 9's single moves journal and no router exists. type claimOperationRouter map[string]ClaimOperationOwner func (r claimOperationRouter) route(s OperationStart) (OperationAttempts, error) { if s.Admitted.Claim == nil { return nil, claimOperationRefusal() } owner, ok := r[s.Admitted.Claim.Op.Kind] if !ok || owner.Attempts == nil { return nil, claimOperationRefusal() } return owner.Attempts, nil } func (r claimOperationRouter) OpenProbe(ctx context.Context, s OperationStart, proposed time.Time) (time.Time, error) { ops, err := r.route(s) if err != nil { return time.Time{}, err } return ops.OpenProbe(ctx, s, proposed) } // CurrentAttempt of an unrouted kind is the zero attempt, which no role // launch accepts; Reserve refuses that kind first in every start. func (r claimOperationRouter) CurrentAttempt(s OperationStart) hostclaim.WakeRetryAttempt { ops, err := r.route(s) if err != nil { return hostclaim.WakeRetryAttempt{} } return ops.CurrentAttempt(s) } func (r claimOperationRouter) Reserve(ctx context.Context, s OperationStart, n int) error { ops, err := r.route(s) if err != nil { return err } return ops.Reserve(ctx, s, n) } func (r claimOperationRouter) Failed(ctx context.Context, s OperationStart, n int) error { ops, err := r.route(s) if err != nil { return err } return ops.Failed(ctx, s, n) } func (r claimOperationRouter) Healthy(ctx context.Context, s OperationStart, n int, cmd *exec.Cmd, again func(context.Context) error) error { ops, err := r.route(s) if err != nil { return err } return ops.Healthy(ctx, s, n, cmd, again) } // operationOwner is the attempt journal and probe readiness check bound for // o's op kind, or nil for a kind no operation owner covers. With Plan 9's // single moves journal only landing has one; with the router, each routed // kind has its own. func (w *ClaimWakeAssembly) operationOwner(o hostclaim.Observed) (OperationAttempts, ClaimProbeReadiness) { if w == nil || w.Operations == nil || o.Claim == nil { return nil, nil } kind := o.Claim.Op.Kind if r, ok := w.Operations.(claimOperationRouter); ok { owner := r[kind] if owner.Attempts == nil { return nil, nil } return owner.Attempts, owner.ProbeReady } if kind != hostclaim.KindLanding && !claimRecoveryKind(kind) { return nil, nil } return w.Operations, w.ProbeReady } // operationKind reports whether o's op kind admits writers through an // operation attempt journal bound on this wake. func (w *ClaimWakeAssembly) operationKind(o hostclaim.Observed) bool { ops, _ := w.operationOwner(o) return ops != nil } // claimSpawnAction is the action a wake attempt selects its launch work // under. A relocating attempt is always the target's probe (WakePhase admits // nothing else under relocating), and it selects "probe", the only action // hostbox.Policy gives relocating authority to, so an ordinary spawn never // gets probe authority. Every other kind keeps "spawn". func claimSpawnAction(c *hostclaim.Claim) string { if c != nil && c.Op.Kind == hostclaim.KindRelocating { return "probe" } return "spawn" } func (w *ClaimWakeAssembly) recoveryAttemptsReady() bool { for _, kind := range []string{hostclaim.KindRestoring, hostclaim.KindReverting} { ops, ready := w.operationOwner(hostclaim.Observed{Claim: &hostclaim.Claim{Op: hostclaim.Op{Kind: kind}}}) provider, ok := ops.(interface{ RecoveryProbeAttemptsReady() bool }) if !ok || !provider.RecoveryProbeAttemptsReady() || ready == nil { return false } } return true }