| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "os/exec" |
| 8 | "strconv" |
| 9 | "sync" |
| 10 | "syscall" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/hostbox" |
| 14 | "github.com/carlosframework/platform/internal/hostclaim" |
| 15 | "github.com/carlosframework/platform/internal/hostwrite" |
| 16 | "github.com/carlosframework/platform/internal/moves" |
| 17 | "github.com/carlosframework/platform/internal/provision" |
| 18 | "github.com/carlosframework/platform/internal/release" |
| 19 | ) |
| 20 | |
| 21 | // movesProbeAttempts is the moves OperationAttempts implementation for a |
| 22 | // landing or recovery probe (moves Phase 1 Plan 3 Task 5b; claim Plan 9 Group D, D2; |
| 23 | // rulings H2, H10). The claim binds its methods into the admitted probe's |
| 24 | // activator.ClaimStart; this type journals, counts and settles the probe's |
| 25 | // starts in the box-local op journal (moves.Journal, <DataDir>/.moves/<host>/ |
| 26 | // <op>/), and writes no record and no claim. |
| 27 | // |
| 28 | // One record attempt is one admission. The landing (Task 6) counts it on the |
| 29 | // record (provision.MoveProbeAttempt, Landing.Attempts = k) before it asks the |
| 30 | // activator for the probe; every method here is bound to that k, refused |
| 31 | // unless 1 <= k <= moves.AttemptsFor("landing", admitted), where admitted is |
| 32 | // what T11 admitted for this move (Admitted, H10). Within an attempt the |
| 33 | // activator makes up to five starts (startAdmitted). |
| 34 | // |
| 35 | // Steps it journals, per record attempt k: |
| 36 | // |
| 37 | // - probe_opened_<k>: the first app start's time, RFC 3339. OpenProbe |
| 38 | // writes it once and returns it to every later start, a restarted |
| 39 | // controller included, so the 30 s window is the attempt's and never |
| 40 | // restarts. |
| 41 | // - probe_start_<k>: the current start's reservation (probeStart): the |
| 42 | // admitted session, the start's number n and the role launcher's |
| 43 | // WakeRetryAttempt, and its state, reserved, failed or healthy. |
| 44 | // - probe_session_<session>: the record attempt k the admitted session is |
| 45 | // bound to, written by its first Reserve(0) or OpenProbe (fix round 1, |
| 46 | // finding 5). A session serves one record attempt: once the record's |
| 47 | // index moves past k every method refuses and CurrentAttempt answers |
| 48 | // the zero attempt, so a new record attempt needs a new cold admission. |
| 49 | // |
| 50 | // Every method refuses (hostbox.ErrChanged) once the claim no longer names |
| 51 | // the admitted writer (op id, kind, session, stamp, generation; phase |
| 52 | // probing) or the record no longer names this landing at probing, unmarked, |
| 53 | // at attempt k. A landing wake after the commit (Plan 3 Task 7) is served |
| 54 | // too: its starts are journalled per admitted session (probe_start_committed- |
| 55 | // <session>), bound to the claim at landing/committed and a committed record, |
| 56 | // with no record attempt, no window and no settle. |
| 57 | type movesProbeAttempts struct { |
| 58 | Claims release.Store |
| 59 | Records MovesRecordReader |
| 60 | DataDir string |
| 61 | // Admitted is the number of probe attempts T11 admitted for the move |
| 62 | // (H10: the target's report, or the record once MoveAdmit carries it). |
| 63 | Admitted func(context.Context, *provision.Record) (int, error) |
| 64 | NewID func() string |
| 65 | Now func() time.Time |
| 66 | // Settle is the wait before the second readiness request; zero is |
| 67 | // moves.SettleSeconds. Ready bounds that request; zero is |
| 68 | // moves.ProbeSeconds. |
| 69 | Settle, Ready time.Duration |
| 70 | // RecoveryAttempt, when set, is told of each recovery probe start this |
| 71 | // journal counts: the record, its recovery retry epoch and the start's |
| 72 | // number (the move log's probe_attempt-<epoch>-<n>, Plan 6 Task 3; |
| 73 | // movesWorker.recoveryAttemptLogger, set by movesWorker.probeAttempts). |
| 74 | RecoveryAttempt func(ctx context.Context, rec provision.Record, epoch uint64, n int) |
| 75 | |
| 76 | mu sync.Mutex |
| 77 | current map[string]boundAttempt |
| 78 | } |
| 79 | |
| 80 | // boundAttempt is a session's last reservation and the record attempt the |
| 81 | // session is bound to. |
| 82 | type boundAttempt struct { |
| 83 | k int |
| 84 | key string |
| 85 | attempt hostclaim.WakeRetryAttempt |
| 86 | } |
| 87 | |
| 88 | var _ OperationAttempts = (*movesProbeAttempts)(nil) |
| 89 | |
| 90 | // Journal step names and start states. |
| 91 | const ( |
| 92 | probeStepOpened = "probe_opened_" |
| 93 | probeStepStart = "probe_start_" |
| 94 | probeStepSession = "probe_session_" |
| 95 | |
| 96 | probeReserved = "reserved" |
| 97 | probeFailed = "failed" |
| 98 | probeHealthy = "healthy" |
| 99 | ) |
| 100 | |
| 101 | // probeStart is a probe_start_<k> step's value. |
| 102 | type probeStart struct { |
| 103 | Session uint64 `json:"session"` |
| 104 | N int `json:"n"` |
| 105 | Attempt hostclaim.WakeRetryAttempt `json:"attempt"` |
| 106 | State string `json:"state"` |
| 107 | } |
| 108 | |
| 109 | func (p *movesProbeAttempts) valid() bool { |
| 110 | return p != nil && p.Claims != nil && p.Records != nil && p.DataDir != "" && p.Admitted != nil && p.NewID != nil && p.Now != nil |
| 111 | } |
| 112 | |
| 113 | // bound returns the landing record attempt, zero for a committed wake, |
| 114 | // or -1 for a recovery probe. Every case validates the admitted writer. |
| 115 | // Recovery also returns the budget key from the same record read. |
| 116 | func (p *movesProbeAttempts) bound(ctx context.Context, s OperationStart) (int, string, error) { |
| 117 | k, key, _, err := p.boundRecord(ctx, s) |
| 118 | return k, key, err |
| 119 | } |
| 120 | |
| 121 | // boundRecord is bound, with the record a recovery probe was bound against |
| 122 | // (nil otherwise): Reserve logs a recovery start from it. |
| 123 | func (p *movesProbeAttempts) boundRecord(ctx context.Context, s OperationStart) (int, string, *provision.Record, error) { |
| 124 | if !p.valid() { |
| 125 | return 0, "", nil, hostwrite.ErrUnavailable |
| 126 | } |
| 127 | a := s.Admitted |
| 128 | if a.Claim != nil && claimRecoveryKind(a.Claim.Op.Kind) { |
| 129 | key, rec, err := p.boundRecovery(ctx, s) |
| 130 | return -1, key, rec, err |
| 131 | } |
| 132 | k, key, err := p.boundLanding(ctx, s) |
| 133 | return k, key, nil, err |
| 134 | } |
| 135 | |
| 136 | // boundLanding is bound for a landing's (or a committed wake's) probe. |
| 137 | func (p *movesProbeAttempts) boundLanding(ctx context.Context, s OperationStart) (int, string, error) { |
| 138 | a := s.Admitted |
| 139 | if a.Claim == nil || a.Claim.Holder == nil || a.Claim.Op.Kind != hostclaim.KindLanding { |
| 140 | return 0, "", hostbox.ErrUnsupported |
| 141 | } |
| 142 | if !s.Policy.Probe { |
| 143 | return 0, "", p.boundCommitted(ctx, s) |
| 144 | } |
| 145 | h := a.Claim.Holder |
| 146 | if s.Owner.Host != a.Host || s.Owner.Session != h.Session || s.Owner.Stamp != h.Stamp || s.Owner.Incarnation != h.Incarnation { |
| 147 | return 0, "", hostbox.ErrChanged |
| 148 | } |
| 149 | fresh, err := hostclaim.Read(ctx, p.Claims, a.Host) |
| 150 | if err != nil { |
| 151 | return 0, "", err |
| 152 | } |
| 153 | if !claimSameWriter(a, fresh, hostclaim.SessionContinues) || fresh.Claim.Op.Phase != hostclaim.PhaseLandingProbing { |
| 154 | return 0, "", hostbox.ErrChanged |
| 155 | } |
| 156 | rec, err := p.Records(ctx, h.Account, h.App, a.Host) |
| 157 | if err != nil { |
| 158 | return 0, "", err |
| 159 | } |
| 160 | if rec == nil || rec.Host != a.Host || rec.Move == nil { |
| 161 | return 0, "", hostbox.ErrChanged |
| 162 | } |
| 163 | m := rec.Move |
| 164 | if m.ID != a.Claim.Op.ID || m.Incarnation != h.Incarnation || m.State != provision.MoveLanding || m.LandingPhase != provision.LandProbing || m.Reason != "" || m.Landing == nil { |
| 165 | return 0, "", hostbox.ErrChanged |
| 166 | } |
| 167 | admitted, err := p.Admitted(ctx, rec) |
| 168 | if err != nil { |
| 169 | return 0, "", err |
| 170 | } |
| 171 | k := m.Landing.Attempts |
| 172 | if k < 1 || k > moves.AttemptsFor(moves.ProbeLanding, admitted) { |
| 173 | return 0, "", hostbox.ErrChanged |
| 174 | } |
| 175 | return k, "", nil |
| 176 | } |
| 177 | |
| 178 | // boundCommitted is bound for a committed wake: the claim still names the |
| 179 | // admitted writer at landing/committed, and the record is this move, |
| 180 | // committed. Its starts are journalled per session (startKey). |
| 181 | func (p *movesProbeAttempts) boundCommitted(ctx context.Context, s OperationStart) error { |
| 182 | a := s.Admitted |
| 183 | h := a.Claim.Holder |
| 184 | if a.Claim.Op.Phase != hostclaim.PhaseLandingCommitted || s.Owner.Host != a.Host || s.Owner.Session != h.Session || s.Owner.Stamp != h.Stamp || s.Owner.Incarnation != h.Incarnation { |
| 185 | return hostbox.ErrChanged |
| 186 | } |
| 187 | fresh, err := hostclaim.Read(ctx, p.Claims, a.Host) |
| 188 | if err != nil { |
| 189 | return err |
| 190 | } |
| 191 | if !claimSameWriter(a, fresh, hostclaim.SessionContinues) || fresh.Claim.Op.Phase != hostclaim.PhaseLandingCommitted { |
| 192 | return hostbox.ErrChanged |
| 193 | } |
| 194 | rec, err := p.Records(ctx, h.Account, h.App, a.Host) |
| 195 | if err != nil { |
| 196 | return err |
| 197 | } |
| 198 | if rec == nil || rec.Host != a.Host || rec.Move == nil || rec.Move.ID != a.Claim.Op.ID || rec.Move.Incarnation != h.Incarnation || rec.Move.State != provision.MoveCommitted { |
| 199 | return hostbox.ErrChanged |
| 200 | } |
| 201 | return nil |
| 202 | } |
| 203 | |
| 204 | // startKey names a start's journal step: the record attempt k for a probe, |
| 205 | // the admitted session for a committed wake (k 0), or the recovery and |
| 206 | // admitted session for a recovery probe (k -1). |
| 207 | func startKey(s OperationStart, k int, recoveryKey string) string { |
| 208 | if k < 0 { |
| 209 | return recoveryKey + "-" + strconv.FormatUint(s.Owner.Session, 10) |
| 210 | } |
| 211 | if k == 0 { |
| 212 | return "committed-" + strconv.FormatUint(s.Owner.Session, 10) |
| 213 | } |
| 214 | return strconv.Itoa(k) |
| 215 | } |
| 216 | |
| 217 | func (p *movesProbeAttempts) journal(s OperationStart) (*moves.Journal, error) { |
| 218 | return moves.ReadJournal(p.DataDir, s.Admitted.Host, s.Admitted.Claim.Op.ID) |
| 219 | } |
| 220 | |
| 221 | func probeKey(s OperationStart) string { |
| 222 | return s.Admitted.Host + "\x00" + strconv.FormatUint(s.Owner.Session, 10) |
| 223 | } |
| 224 | |
| 225 | // bindSession checks the session's journalled record attempt against k. |
| 226 | // Absent, it is written into j when write is set (the caller writes j) and |
| 227 | // refused otherwise. |
| 228 | func bindSession(j *moves.Journal, s OperationStart, k int, write bool) error { |
| 229 | if k <= 0 { |
| 230 | return nil // committed wakes and recoveries have no landing record attempt |
| 231 | } |
| 232 | step := probeStepSession + strconv.FormatUint(s.Owner.Session, 10) |
| 233 | raw, ok := j.Steps[step] |
| 234 | switch { |
| 235 | case ok && raw == strconv.Itoa(k): |
| 236 | return nil |
| 237 | case ok: |
| 238 | return fmt.Errorf("%w: session %d is bound to record attempt %s, not %d", hostbox.ErrChanged, s.Owner.Session, raw, k) |
| 239 | case !write: |
| 240 | return fmt.Errorf("%w: session %d is bound to no record attempt", hostbox.ErrChanged, s.Owner.Session) |
| 241 | } |
| 242 | j.Mark(step, strconv.Itoa(k)) |
| 243 | return nil |
| 244 | } |
| 245 | |
| 246 | // OpenProbe journals the attempt's first start time, once. |
| 247 | func (p *movesProbeAttempts) OpenProbe(ctx context.Context, s OperationStart, proposed time.Time) (time.Time, error) { |
| 248 | k, recoveryKey, err := p.bound(ctx, s) |
| 249 | if err != nil { |
| 250 | return time.Time{}, err |
| 251 | } |
| 252 | if k == 0 { |
| 253 | return time.Time{}, hostbox.ErrUnsupported |
| 254 | } |
| 255 | if proposed.IsZero() { |
| 256 | return time.Time{}, hostwrite.ErrUnavailable |
| 257 | } |
| 258 | p.mu.Lock() |
| 259 | defer p.mu.Unlock() |
| 260 | j, err := p.journal(s) |
| 261 | if err != nil { |
| 262 | return time.Time{}, err |
| 263 | } |
| 264 | if err := bindSession(j, s, k, true); err != nil { |
| 265 | return time.Time{}, err |
| 266 | } |
| 267 | step := probeStepOpened + startKey(s, k, recoveryKey) |
| 268 | if raw, ok := j.Steps[step]; ok { |
| 269 | if err := j.Commit(p.DataDir); err != nil { |
| 270 | return time.Time{}, err |
| 271 | } |
| 272 | opened, err := time.Parse(time.RFC3339Nano, raw) |
| 273 | if err != nil { |
| 274 | return time.Time{}, fmt.Errorf("moves journal %s/%s: %s: %w", s.Admitted.Host, s.Admitted.Claim.Op.ID, step, err) |
| 275 | } |
| 276 | return opened, nil |
| 277 | } |
| 278 | opened := proposed.UTC() |
| 279 | j.Mark(step, opened.Format(time.RFC3339Nano)) |
| 280 | if err := j.Commit(p.DataDir); err != nil { |
| 281 | return time.Time{}, err |
| 282 | } |
| 283 | return opened, nil |
| 284 | } |
| 285 | |
| 286 | // readStart returns attempt k's journalled start, or false. |
| 287 | func readStart(j *moves.Journal, key string) (probeStart, bool, error) { |
| 288 | raw, ok := j.Steps[probeStepStart+key] |
| 289 | if !ok { |
| 290 | return probeStart{}, false, nil |
| 291 | } |
| 292 | var st probeStart |
| 293 | if err := json.Unmarshal([]byte(raw), &st); err != nil { |
| 294 | return probeStart{}, false, fmt.Errorf("moves journal %s/%s: %s%s: %w", j.Host, j.Op, probeStepStart, key, err) |
| 295 | } |
| 296 | return st, true, nil |
| 297 | } |
| 298 | |
| 299 | func writeStart(dataDir string, j *moves.Journal, key string, st probeStart) error { |
| 300 | raw, err := json.Marshal(st) |
| 301 | if err != nil { |
| 302 | return err |
| 303 | } |
| 304 | j.Mark(probeStepStart+key, string(raw)) |
| 305 | return j.Commit(dataDir) |
| 306 | } |
| 307 | |
| 308 | // sameStartOwner reports whether a journalled reservation is the one this |
| 309 | // admission's owner made for these launch inputs. |
| 310 | func sameStartOwner(a hostclaim.WakeRetryAttempt, s OperationStart, digest string) bool { |
| 311 | o := s.Owner |
| 312 | return a.ID != "" && a.InvocationID != "" && a.Session == o.Session && a.Box == o.Box && a.Enrollment == o.Enrollment && a.BootID == o.BootID && a.ControllerID == o.ControllerID && a.LaunchInputs == digest |
| 313 | } |
| 314 | |
| 315 | // Reserve journals start n of this admission before it runs. Start 0 is |
| 316 | // reserved by the first Reserve(0) of the admission and validated by every |
| 317 | // later one; start n > 0 only after start n-1 was settled failed. |
| 318 | func (p *movesProbeAttempts) Reserve(ctx context.Context, s OperationStart, n int) (retErr error) { |
| 319 | k, recoveryKey, bindRec, err := p.boundRecord(ctx, s) |
| 320 | if err != nil { |
| 321 | return err |
| 322 | } |
| 323 | digest, err := ClaimLaunchDigest(s.Inputs) |
| 324 | if err != nil { |
| 325 | return err |
| 326 | } |
| 327 | p.mu.Lock() |
| 328 | defer p.mu.Unlock() |
| 329 | j, err := p.journal(s) |
| 330 | if err != nil { |
| 331 | return err |
| 332 | } |
| 333 | if err := bindSession(j, s, k, n == 0); err != nil { |
| 334 | return err |
| 335 | } |
| 336 | st, ok, err := readStart(j, startKey(s, k, recoveryKey)) |
| 337 | if err != nil { |
| 338 | return err |
| 339 | } |
| 340 | same := ok && st.Session == s.Owner.Session |
| 341 | switch { |
| 342 | case n < 0: |
| 343 | return hostbox.ErrChanged |
| 344 | case n == 0 && same: |
| 345 | if st.N != 0 || st.State != probeReserved || !sameStartOwner(st.Attempt, s, digest) { |
| 346 | return hostbox.ErrChanged |
| 347 | } |
| 348 | case n == 0: |
| 349 | // The admission's first start; an earlier admission's journalled |
| 350 | // start, under another session, is over. |
| 351 | st = probeStart{Session: s.Owner.Session, N: 0, State: probeReserved} |
| 352 | default: |
| 353 | if !same || st.N != n-1 || st.State != probeFailed || st.Attempt.LaunchInputs != digest { |
| 354 | return hostbox.ErrChanged |
| 355 | } |
| 356 | st = probeStart{Session: s.Owner.Session, N: n, State: probeReserved} |
| 357 | } |
| 358 | if _, bound := j.Steps[probeStepSession+strconv.FormatUint(s.Owner.Session, 10)]; bound && st.Attempt.ID != "" { |
| 359 | // Reserve(0) validated an existing reservation; the binding it may |
| 360 | // have just written is made durable. |
| 361 | if err := j.Commit(p.DataDir); err != nil { |
| 362 | return err |
| 363 | } |
| 364 | } |
| 365 | if st.Attempt.ID == "" { |
| 366 | counted := 0 |
| 367 | if k < 0 { |
| 368 | count, err := recoveryAttemptCount(j, recoveryKey) |
| 369 | if err != nil { |
| 370 | return err |
| 371 | } |
| 372 | if count >= recoveryProbeAttempts { |
| 373 | return ErrRecoveryProbeExhausted |
| 374 | } |
| 375 | counted = count + 1 |
| 376 | j.Mark(recoveryKey, strconv.Itoa(counted)) |
| 377 | } |
| 378 | defer func() { |
| 379 | if counted > 0 && retErr == nil { |
| 380 | p.recoveryAttempted(ctx, bindRec, counted) |
| 381 | } |
| 382 | }() |
| 383 | o := s.Owner |
| 384 | st.Attempt = hostclaim.WakeRetryAttempt{ID: p.NewID(), Box: o.Box, Enrollment: o.Enrollment, BootID: o.BootID, ControllerID: o.ControllerID, Session: o.Session, InvocationID: p.NewID(), LaunchInputs: digest} |
| 385 | if st.Attempt.ID == "" || st.Attempt.InvocationID == "" || st.Attempt.ID == st.Attempt.InvocationID { |
| 386 | return hostwrite.ErrUnavailable |
| 387 | } |
| 388 | if err := writeStart(p.DataDir, j, startKey(s, k, recoveryKey), st); err != nil { |
| 389 | return err |
| 390 | } |
| 391 | } |
| 392 | if p.current == nil { |
| 393 | p.current = map[string]boundAttempt{} |
| 394 | } |
| 395 | p.current[probeKey(s)] = boundAttempt{k: k, key: startKey(s, k, recoveryKey), attempt: st.Attempt} |
| 396 | return nil |
| 397 | } |
| 398 | |
| 399 | // CurrentAttempt is the reservation the last Reserve journalled for this |
| 400 | // admission; the zero attempt, which the role launcher refuses, before any. |
| 401 | // It reads the record again (the interface gives it no context, so its own |
| 402 | // bounded one): once the record's index is past the bound attempt it answers |
| 403 | // the zero attempt. |
| 404 | func (p *movesProbeAttempts) CurrentAttempt(s OperationStart) hostclaim.WakeRetryAttempt { |
| 405 | if p == nil { |
| 406 | return hostclaim.WakeRetryAttempt{} |
| 407 | } |
| 408 | p.mu.Lock() |
| 409 | cur, ok := p.current[probeKey(s)] |
| 410 | p.mu.Unlock() |
| 411 | if !ok { |
| 412 | return hostclaim.WakeRetryAttempt{} |
| 413 | } |
| 414 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| 415 | defer cancel() |
| 416 | if k, recoveryKey, err := p.bound(ctx, s); err != nil || k != cur.k || startKey(s, k, recoveryKey) != cur.key { |
| 417 | return hostclaim.WakeRetryAttempt{} |
| 418 | } |
| 419 | return cur.attempt |
| 420 | } |
| 421 | |
| 422 | // Failed settles start n, already stopped and drained, as failed. |
| 423 | func (p *movesProbeAttempts) Failed(ctx context.Context, s OperationStart, n int) error { |
| 424 | recovery, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) |
| 425 | defer cancel() |
| 426 | return p.settle(recovery, s, n, probeFailed) |
| 427 | } |
| 428 | |
| 429 | func (p *movesProbeAttempts) settle(ctx context.Context, s OperationStart, n int, state string) error { |
| 430 | k, recoveryKey, err := p.bound(ctx, s) |
| 431 | if err != nil { |
| 432 | return err |
| 433 | } |
| 434 | p.mu.Lock() |
| 435 | defer p.mu.Unlock() |
| 436 | j, err := p.journal(s) |
| 437 | if err != nil { |
| 438 | return err |
| 439 | } |
| 440 | if err := bindSession(j, s, k, false); err != nil { |
| 441 | return err |
| 442 | } |
| 443 | st, ok, err := readStart(j, startKey(s, k, recoveryKey)) |
| 444 | if err != nil { |
| 445 | return err |
| 446 | } |
| 447 | cur := p.current[probeKey(s)] |
| 448 | if !ok || st.Session != s.Owner.Session || st.N != n || st.State != probeReserved || cur.k != k || st.Attempt != cur.attempt { |
| 449 | return hostbox.ErrChanged |
| 450 | } |
| 451 | st.State = state |
| 452 | if err := writeStart(p.DataDir, j, startKey(s, k, recoveryKey), st); err != nil { |
| 453 | return err |
| 454 | } |
| 455 | if k < 0 && state == probeFailed { |
| 456 | count, err := recoveryAttemptCount(j, recoveryKey) |
| 457 | if err != nil { |
| 458 | return err |
| 459 | } |
| 460 | if count >= recoveryProbeAttempts { |
| 461 | return ErrRecoveryProbeExhausted |
| 462 | } |
| 463 | } |
| 464 | return nil |
| 465 | } |
| 466 | |
| 467 | // Healthy accepts start n after readiness, replication and publication: |
| 468 | // the 20 s settle with the process alive, then the probe's own readiness |
| 469 | // check again (again, the claim's ClaimProbeReadiness against the started |
| 470 | // role), with the process still alive after it (moves §7.7). |
| 471 | func (p *movesProbeAttempts) Healthy(ctx context.Context, s OperationStart, n int, cmd *exec.Cmd, again func(context.Context) error) error { |
| 472 | k, _, err := p.bound(ctx, s) |
| 473 | if err != nil { |
| 474 | return err |
| 475 | } |
| 476 | p.mu.Lock() |
| 477 | j, err := p.journal(s) |
| 478 | if err == nil { |
| 479 | err = bindSession(j, s, k, false) |
| 480 | } |
| 481 | p.mu.Unlock() |
| 482 | if err != nil { |
| 483 | return err |
| 484 | } |
| 485 | if k == 0 { |
| 486 | // A committed wake: serving's readiness has passed, with |
| 487 | // replication and publication; there is no probe settle. |
| 488 | return p.settle(ctx, s, n, probeHealthy) |
| 489 | } |
| 490 | if again == nil || !probeAlive(cmd) { |
| 491 | return hostbox.ErrChanged |
| 492 | } |
| 493 | settle, ready := p.Settle, p.Ready |
| 494 | if settle <= 0 { |
| 495 | settle = time.Duration(moves.SettleSeconds * float64(time.Second)) |
| 496 | } |
| 497 | if ready <= 0 { |
| 498 | ready = time.Duration(moves.ProbeSeconds * float64(time.Second)) |
| 499 | } |
| 500 | timer := time.NewTimer(settle) |
| 501 | select { |
| 502 | case <-ctx.Done(): |
| 503 | timer.Stop() |
| 504 | return ctx.Err() |
| 505 | case <-timer.C: |
| 506 | } |
| 507 | if !probeAlive(cmd) { |
| 508 | return fmt.Errorf("%w: the app exited during the settle", hostbox.ErrChanged) |
| 509 | } |
| 510 | asked, cancel := context.WithTimeout(ctx, ready) |
| 511 | err = again(asked) |
| 512 | cancel() |
| 513 | if err != nil { |
| 514 | return err |
| 515 | } |
| 516 | if !probeAlive(cmd) { |
| 517 | return fmt.Errorf("%w: the app exited after the second request", hostbox.ErrChanged) |
| 518 | } |
| 519 | return p.settle(ctx, s, n, probeHealthy) |
| 520 | } |
| 521 | |
| 522 | // probeAlive reports whether cmd's process has not exited. Signal 0 checks |
| 523 | // existence without delivering anything; once the activator's Wait has |
| 524 | // reaped the process it reports os.ErrProcessDone. |
| 525 | func probeAlive(cmd *exec.Cmd) bool { |
| 526 | return cmd != nil && cmd.Process != nil && cmd.Process.Signal(syscall.Signal(0)) == nil |
| 527 | } |
| 528 | |
| 529 | // RecoveryProbeAttemptsReady is checked before the adapter records inputs. |
| 530 | func (p *movesProbeAttempts) RecoveryProbeAttemptsReady() bool { return p.valid() } |
| 531 | |
| 532 | func (p *movesProbeAttempts) boundRecovery(ctx context.Context, s OperationStart) (string, *provision.Record, error) { |
| 533 | a := s.Admitted |
| 534 | if a.Claim.Holder == nil || !s.Policy.Probe { |
| 535 | return "", nil, hostbox.ErrChanged |
| 536 | } |
| 537 | h := a.Claim.Holder |
| 538 | if s.Owner.Host != a.Host || s.Owner.Incarnation != h.Incarnation || s.Owner.Stamp != h.Stamp || s.Owner.Session != h.Session || s.Owner.Box != a.Claim.Op.Exec { |
| 539 | return "", nil, hostbox.ErrChanged |
| 540 | } |
| 541 | fresh, err := hostclaim.Read(ctx, p.Claims, a.Host) |
| 542 | if err != nil { |
| 543 | return "", nil, err |
| 544 | } |
| 545 | if !claimSameWriter(a, fresh, hostclaim.SessionContinues) || fresh.Claim.Op.Phase != hostclaim.PhaseProbing || fresh.Claim.Op.Exec != s.Owner.Box { |
| 546 | return "", nil, hostbox.ErrChanged |
| 547 | } |
| 548 | rec, err := p.Records(ctx, h.Account, h.App, a.Host) |
| 549 | if err != nil { |
| 550 | return "", nil, err |
| 551 | } |
| 552 | if rec == nil || rec.Host != a.Host || rec.Move == nil || rec.Move.ID != a.Claim.Op.ID || rec.Move.Incarnation != h.Incarnation { |
| 553 | return "", nil, hostbox.ErrChanged |
| 554 | } |
| 555 | m := rec.Move |
| 556 | if a.Claim.Op.Kind == hostclaim.KindReverting { |
| 557 | if m.State != provision.MoveReverting || m.ReopenedAt != nil || fresh.Claim.Op.Journal[hostclaim.JournalRevPhase] != hostclaim.PhaseProbing { |
| 558 | return "", nil, hostbox.ErrChanged |
| 559 | } |
| 560 | } else if m.State != provision.MoveRollingBack || m.Rollback == nil || m.Rollback.ID == "" || m.Rollback.ID != a.Claim.Op.Intent["rollback"] || m.Rollback.ID != fresh.Claim.Op.Intent["rollback"] || m.Rollback.SourcePhase != provision.RbSourceProbing { |
| 561 | return "", nil, hostbox.ErrChanged |
| 562 | } |
| 563 | return recoveryAttemptKey(s, m.RecoveryRetryEpoch), rec, nil |
| 564 | } |
| 565 | |
| 566 | // The move has its own journal directory. JSON preserves the rollback and |
| 567 | // incarnation boundaries; sessions share this counter across controller restarts. |
| 568 | func recoveryAttemptKey(s OperationStart, retryEpoch uint64) string { |
| 569 | c := s.Admitted.Claim |
| 570 | rollback := c.Op.ID |
| 571 | if c.Op.Kind == hostclaim.KindRestoring { |
| 572 | rollback = c.Op.Intent["rollback"] |
| 573 | } |
| 574 | parts := []string{c.Op.ID, rollback, c.Holder.Incarnation} |
| 575 | // Keep epoch zero's key so an upgrade preserves existing counts. |
| 576 | if retryEpoch != 0 { |
| 577 | parts = append(parts, strconv.FormatUint(retryEpoch, 10)) |
| 578 | } |
| 579 | raw, _ := json.Marshal(parts) |
| 580 | return "recovery_attempts_" + string(raw) |
| 581 | } |
| 582 | |
| 583 | func recoveryAttemptCount(j *moves.Journal, key string) (int, error) { |
| 584 | raw, ok := j.Steps[key] |
| 585 | if !ok { |
| 586 | return 0, nil |
| 587 | } |
| 588 | n, err := strconv.Atoi(raw) |
| 589 | if err != nil || n < 0 || n > recoveryProbeAttempts { |
| 590 | return 0, hostbox.ErrChanged |
| 591 | } |
| 592 | return n, nil |
| 593 | } |
| 594 | |
| 595 | // recoveryAttempted tells RecoveryAttempt of start n of a recovery probe, |
| 596 | // with the record the start was bound against (its retry epoch): the read |
| 597 | // the binding already made, so a counted start is never lost to a second |
| 598 | // read. The hook's own failed write goes to the outbox. |
| 599 | func (p *movesProbeAttempts) recoveryAttempted(ctx context.Context, rec *provision.Record, n int) { |
| 600 | if p.RecoveryAttempt == nil || rec == nil || rec.Move == nil { |
| 601 | return |
| 602 | } |
| 603 | p.RecoveryAttempt(ctx, *rec, rec.Move.RecoveryRetryEpoch, n) |
| 604 | } |
| 605 | |
| 606 | // probeAttempts is the attempt journal a box builds for its probe wakes |
| 607 | // (Plan 3 Task 5), over the deployment store s, its records and newID, with |
| 608 | // this worker's data directory and clock. Its RecoveryAttempt is the |
| 609 | // worker's move log hook (recoveryAttemptLogger), so every recovery probe |
| 610 | // start it counts is logged (fix round 2, ruling 2). Production's journal |
| 611 | // is the registered wake source's (movesWakeSources.Open, Plan 5 Task 9b), |
| 612 | // built the same way before this worker is armed; it reaches this worker's |
| 613 | // hook through armedMovesWorker (movesWorkerRecoveryAttempt). |
| 614 | func (w *movesWorker) probeAttempts(s release.Store, records MovesRecordReader, newID func() string) *movesProbeAttempts { |
| 615 | return &movesProbeAttempts{Claims: s, Records: records, DataDir: w.cfg.DataDir, Admitted: MovesRecordAdmittedAttempts, |
| 616 | NewID: newID, Now: w.cfg.Now, RecoveryAttempt: w.recoveryAttemptLogger(s)} |
| 617 | } |
| 618 | |