| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "encoding/json" |
| 8 | "os/exec" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/activator" |
| 12 | "github.com/carlosframework/platform/internal/hostclaim" |
| 13 | "github.com/carlosframework/platform/internal/hostguard" |
| 14 | "github.com/carlosframework/platform/internal/hostwrite" |
| 15 | "github.com/carlosframework/platform/internal/registry" |
| 16 | ) |
| 17 | |
| 18 | // OperationStart is one writer admitted under a move op kind on this |
| 19 | // controller: the admitted observation, the start policy it was admitted |
| 20 | // under, the selected inputs and the pre-launch record's controller tuple |
| 21 | // (box, enrollment, boot, controller, session). The claim builds it once per |
| 22 | // Prepare; an OperationAttempts implementation never constructs one. |
| 23 | type OperationStart struct { |
| 24 | Admitted hostclaim.Observed |
| 25 | Policy activator.StartPolicy |
| 26 | Inputs ClaimLaunchInputs |
| 27 | Owner hostguard.FenceTarget |
| 28 | } |
| 29 | |
| 30 | // OperationAttempts is the moves-owned attempt journal for a writer admitted |
| 31 | // under landing (claim Plan 9 D2, moves ruling H2), and region HA's for a |
| 32 | // probe admitted under relocating/probing (claim Plan 11 R1). It is the operation |
| 33 | // analogue of activator.ServingRetries: the claim binds its methods into |
| 34 | // activator.ClaimStart for the exact admitted session, and moves journals, |
| 35 | // counts and settles the attempts in its own record. Every method must refuse |
| 36 | // with hostbox.ErrChanged once the claim or the moves record no longer names |
| 37 | // s.Admitted's op, phase step, session, stamp and generation. |
| 38 | type OperationAttempts interface { |
| 39 | // OpenProbe journals the attempt's first app-start time before execution |
| 40 | // and returns that same time on every later start, including after a |
| 41 | // controller restart. It never resets the opened time. Probes only. |
| 42 | OpenProbe(ctx context.Context, s OperationStart, proposed time.Time) (time.Time, error) |
| 43 | // CurrentAttempt returns the exact stable value last journalled by Reserve. |
| 44 | // It never generates IDs; repeated reads between Reserve calls are identical. |
| 45 | // CurrentAttempt is the reservation the role launcher journals for the |
| 46 | // next start: ID and InvocationID unique per start, Session, Box, |
| 47 | // Enrollment, BootID and ControllerID equal to s.Owner's, and |
| 48 | // LaunchInputs equal to ClaimLaunchDigest(s.Inputs). |
| 49 | CurrentAttempt(s OperationStart) hostclaim.WakeRetryAttempt |
| 50 | // Reserve(0) validates the attempt journalled for this admission. Reserve |
| 51 | // with n > 0 journals start n of the same admission before it runs. |
| 52 | Reserve(ctx context.Context, s OperationStart, n int) error |
| 53 | // Failed settles start n after its process was stopped and drained. |
| 54 | Failed(ctx context.Context, s OperationStart, n int) error |
| 55 | // Healthy accepts start n after readiness, replication and publication. |
| 56 | // again re-runs the op kind's own readiness check against cmd, for the |
| 57 | // probe's settle and second request (moves §7.7). |
| 58 | Healthy(ctx context.Context, s OperationStart, n int, cmd *exec.Cmd, again func(context.Context) error) error |
| 59 | } |
| 60 | |
| 61 | // ClaimProbeReadiness is the probe kind's readiness check (claim Plan 9 D3, |
| 62 | // moves ruling H12). roleCgroup is the absolute cgroup v2 directory of the |
| 63 | // started main role, resolved from its journalled reservation. Serving and a |
| 64 | // landing wake after the commit keep ClaimWakeAssembly.Ready. |
| 65 | type ClaimProbeReadiness func(ctx context.Context, cmd *exec.Cmd, rt registry.Route, in ClaimLaunchInputs, roleCgroup string) error |
| 66 | |
| 67 | // ClaimLaunchDigest names the launch inputs an attempt reservation binds: the |
| 68 | // identity, executable, digest, arguments and environment, never the |
| 69 | // admission. Serving retries and operation attempts use the same digest. |
| 70 | func ClaimLaunchDigest(in ClaimLaunchInputs) (string, error) { |
| 71 | raw, err := json.Marshal(struct { |
| 72 | Identity hostwrite.Identity |
| 73 | Executable, Digest string |
| 74 | Args, Env []string |
| 75 | }{in.Identity, in.Executable, in.ExecutableSHA256, in.Args, in.Env}) |
| 76 | if err != nil { |
| 77 | return "", err |
| 78 | } |
| 79 | digest := sha256.Sum256(raw) |
| 80 | return hex.EncodeToString(digest[:]), nil |
| 81 | } |
| 82 | |
| 83 | // claimOperationRefusal is main's typed refusal for every operation or probe |
| 84 | // admission the bound moves C1 owners do not cover. |
| 85 | func claimOperationRefusal() error { |
| 86 | return &ClaimDependencyError{Contracts: []string{"moves C1 operation attempt journal and prepared-open adapter"}} |
| 87 | } |
| 88 | |
| 89 | // ClaimOperationOwner is one op kind's attempt journal and probe readiness |
| 90 | // check (claim Plan 11 R1). ProbeReady nil refuses that kind's probes. |
| 91 | type ClaimOperationOwner struct { |
| 92 | Attempts OperationAttempts |
| 93 | ProbeReady ClaimProbeReadiness |
| 94 | } |
| 95 | |
| 96 | // claimOperationRouter is the wake's OperationAttempts when more than one op |
| 97 | // kind admits operation writers: landing to the moves journal, relocating to |
| 98 | // region HA's. Each call goes to the admitted observation's kind; any other |
| 99 | // kind is refused before any journal is touched. With region HA unbound the |
| 100 | // wake keeps Plan 9's single moves journal and no router exists. |
| 101 | type claimOperationRouter map[string]ClaimOperationOwner |
| 102 | |
| 103 | func (r claimOperationRouter) route(s OperationStart) (OperationAttempts, error) { |
| 104 | if s.Admitted.Claim == nil { |
| 105 | return nil, claimOperationRefusal() |
| 106 | } |
| 107 | owner, ok := r[s.Admitted.Claim.Op.Kind] |
| 108 | if !ok || owner.Attempts == nil { |
| 109 | return nil, claimOperationRefusal() |
| 110 | } |
| 111 | return owner.Attempts, nil |
| 112 | } |
| 113 | |
| 114 | func (r claimOperationRouter) OpenProbe(ctx context.Context, s OperationStart, proposed time.Time) (time.Time, error) { |
| 115 | ops, err := r.route(s) |
| 116 | if err != nil { |
| 117 | return time.Time{}, err |
| 118 | } |
| 119 | return ops.OpenProbe(ctx, s, proposed) |
| 120 | } |
| 121 | |
| 122 | // CurrentAttempt of an unrouted kind is the zero attempt, which no role |
| 123 | // launch accepts; Reserve refuses that kind first in every start. |
| 124 | func (r claimOperationRouter) CurrentAttempt(s OperationStart) hostclaim.WakeRetryAttempt { |
| 125 | ops, err := r.route(s) |
| 126 | if err != nil { |
| 127 | return hostclaim.WakeRetryAttempt{} |
| 128 | } |
| 129 | return ops.CurrentAttempt(s) |
| 130 | } |
| 131 | |
| 132 | func (r claimOperationRouter) Reserve(ctx context.Context, s OperationStart, n int) error { |
| 133 | ops, err := r.route(s) |
| 134 | if err != nil { |
| 135 | return err |
| 136 | } |
| 137 | return ops.Reserve(ctx, s, n) |
| 138 | } |
| 139 | |
| 140 | func (r claimOperationRouter) Failed(ctx context.Context, s OperationStart, n int) error { |
| 141 | ops, err := r.route(s) |
| 142 | if err != nil { |
| 143 | return err |
| 144 | } |
| 145 | return ops.Failed(ctx, s, n) |
| 146 | } |
| 147 | |
| 148 | func (r claimOperationRouter) Healthy(ctx context.Context, s OperationStart, n int, cmd *exec.Cmd, again func(context.Context) error) error { |
| 149 | ops, err := r.route(s) |
| 150 | if err != nil { |
| 151 | return err |
| 152 | } |
| 153 | return ops.Healthy(ctx, s, n, cmd, again) |
| 154 | } |
| 155 | |
| 156 | // operationOwner is the attempt journal and probe readiness check bound for |
| 157 | // o's op kind, or nil for a kind no operation owner covers. With Plan 9's |
| 158 | // single moves journal only landing has one; with the router, each routed |
| 159 | // kind has its own. |
| 160 | func (w *ClaimWakeAssembly) operationOwner(o hostclaim.Observed) (OperationAttempts, ClaimProbeReadiness) { |
| 161 | if w == nil || w.Operations == nil || o.Claim == nil { |
| 162 | return nil, nil |
| 163 | } |
| 164 | kind := o.Claim.Op.Kind |
| 165 | if r, ok := w.Operations.(claimOperationRouter); ok { |
| 166 | owner := r[kind] |
| 167 | if owner.Attempts == nil { |
| 168 | return nil, nil |
| 169 | } |
| 170 | return owner.Attempts, owner.ProbeReady |
| 171 | } |
| 172 | if kind != hostclaim.KindLanding && !claimRecoveryKind(kind) { |
| 173 | return nil, nil |
| 174 | } |
| 175 | return w.Operations, w.ProbeReady |
| 176 | } |
| 177 | |
| 178 | // operationKind reports whether o's op kind admits writers through an |
| 179 | // operation attempt journal bound on this wake. |
| 180 | func (w *ClaimWakeAssembly) operationKind(o hostclaim.Observed) bool { |
| 181 | ops, _ := w.operationOwner(o) |
| 182 | return ops != nil |
| 183 | } |
| 184 | |
| 185 | // claimSpawnAction is the action a wake attempt selects its launch work |
| 186 | // under. A relocating attempt is always the target's probe (WakePhase admits |
| 187 | // nothing else under relocating), and it selects "probe", the only action |
| 188 | // hostbox.Policy gives relocating authority to, so an ordinary spawn never |
| 189 | // gets probe authority. Every other kind keeps "spawn". |
| 190 | func claimSpawnAction(c *hostclaim.Claim) string { |
| 191 | if c != nil && c.Op.Kind == hostclaim.KindRelocating { |
| 192 | return "probe" |
| 193 | } |
| 194 | return "spawn" |
| 195 | } |
| 196 | |
| 197 | func (w *ClaimWakeAssembly) recoveryAttemptsReady() bool { |
| 198 | for _, kind := range []string{hostclaim.KindRestoring, hostclaim.KindReverting} { |
| 199 | ops, ready := w.operationOwner(hostclaim.Observed{Claim: &hostclaim.Claim{Op: hostclaim.Op{Kind: kind}}}) |
| 200 | provider, ok := ops.(interface{ RecoveryProbeAttemptsReady() bool }) |
| 201 | if !ok || !provider.RecoveryProbeAttemptsReady() || ready == nil { |
| 202 | return false |
| 203 | } |
| 204 | } |
| 205 | return true |
| 206 | } |
| 207 | |