| index e9dae6fed..b00fbb8fe 100644 |
| --- a/internal/host/movesprobeattempts.go |
| +++ b/internal/host/movesprobeattempts.go |
| @@ -41,6 +41,11 @@ import ( |
| // - probe_start_<k>: the current start's reservation (probeStart): the |
| // admitted session, the start's number n and the role launcher's |
| // WakeRetryAttempt, and its state, reserved, failed or healthy. |
| +// - probe_session_<session>: the record attempt k the admitted session is |
| +// bound to, written by its first Reserve(0) or OpenProbe (fix round 1, |
| +// finding 5). A session serves one record attempt: once the record's |
| +// index moves past k every method refuses and CurrentAttempt answers |
| +// the zero attempt, so a new record attempt needs a new cold admission. |
| // |
| // Every method refuses (hostbox.ErrChanged) once the claim no longer names |
| // the admitted writer (op id, kind, session, stamp, generation; phase |
| @@ -62,15 +67,23 @@ type movesProbeAttempts struct { |
| Settle, Ready time.Duration |
| |
| mu sync.Mutex |
| - current map[string]hostclaim.WakeRetryAttempt |
| + current map[string]boundAttempt |
| +} |
| + |
| +// boundAttempt is a session's last reservation and the record attempt the |
| +// session is bound to. |
| +type boundAttempt struct { |
| + k int |
| + attempt hostclaim.WakeRetryAttempt |
| } |
| |
| var _ OperationAttempts = (*movesProbeAttempts)(nil) |
| |
| // Journal step names and start states. |
| const ( |
| - probeStepOpened = "probe_opened_" |
| - probeStepStart = "probe_start_" |
| + probeStepOpened = "probe_opened_" |
| + probeStepStart = "probe_start_" |
| + probeStepSession = "probe_session_" |
| |
| probeReserved = "reserved" |
| probeFailed = "failed" |
| @@ -140,6 +153,24 @@ func probeKey(s OperationStart) string { |
| return s.Admitted.Host + "\x00" + strconv.FormatUint(s.Owner.Session, 10) |
| } |
| |
| +// bindSession checks the session's journalled record attempt against k. |
| +// Absent, it is written into j when write is set (the caller writes j) and |
| +// refused otherwise. |
| +func bindSession(j *moves.Journal, s OperationStart, k int, write bool) error { |
| + step := probeStepSession + strconv.FormatUint(s.Owner.Session, 10) |
| + raw, ok := j.Steps[step] |
| + switch { |
| + case ok && raw == strconv.Itoa(k): |
| + return nil |
| + case ok: |
| + return fmt.Errorf("%w: session %d is bound to record attempt %s, not %d", hostbox.ErrChanged, s.Owner.Session, raw, k) |
| + case !write: |
| + return fmt.Errorf("%w: session %d is bound to no record attempt", hostbox.ErrChanged, s.Owner.Session) |
| + } |
| + j.Mark(step, strconv.Itoa(k)) |
| + return nil |
| +} |
| + |
| // OpenProbe journals the attempt's first start time, once. |
| func (p *movesProbeAttempts) OpenProbe(ctx context.Context, s OperationStart, proposed time.Time) (time.Time, error) { |
| k, err := p.bound(ctx, s) |
| @@ -155,8 +186,14 @@ func (p *movesProbeAttempts) OpenProbe(ctx context.Context, s OperationStart, pr |
| if err != nil { |
| return time.Time{}, err |
| } |
| + if err := bindSession(j, s, k, true); err != nil { |
| + return time.Time{}, err |
| + } |
| step := probeStepOpened + strconv.Itoa(k) |
| if raw, ok := j.Steps[step]; ok { |
| + if err := j.Write(p.DataDir); err != nil { |
| + return time.Time{}, err |
| + } |
| opened, err := time.Parse(time.RFC3339Nano, raw) |
| if err != nil { |
| return time.Time{}, fmt.Errorf("moves journal %s/%s: %s: %w", s.Admitted.Host, s.Admitted.Claim.Op.ID, step, err) |
| @@ -218,6 +255,9 @@ func (p *movesProbeAttempts) Reserve(ctx context.Context, s OperationStart, n in |
| if err != nil { |
| return err |
| } |
| + if err := bindSession(j, s, k, n == 0); err != nil { |
| + return err |
| + } |
| st, ok, err := readStart(j, k) |
| if err != nil { |
| return err |
| @@ -240,6 +280,13 @@ func (p *movesProbeAttempts) Reserve(ctx context.Context, s OperationStart, n in |
| } |
| st = probeStart{Session: s.Owner.Session, N: n, State: probeReserved} |
| } |
| + if _, bound := j.Steps[probeStepSession+strconv.FormatUint(s.Owner.Session, 10)]; bound && st.Attempt.ID != "" { |
| + // Reserve(0) validated an existing reservation; the binding it may |
| + // have just written is made durable. |
| + if err := j.Write(p.DataDir); err != nil { |
| + return err |
| + } |
| + } |
| if st.Attempt.ID == "" { |
| o := s.Owner |
| 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} |
| @@ -251,21 +298,33 @@ func (p *movesProbeAttempts) Reserve(ctx context.Context, s OperationStart, n in |
| } |
| } |
| if p.current == nil { |
| - p.current = map[string]hostclaim.WakeRetryAttempt{} |
| + p.current = map[string]boundAttempt{} |
| } |
| - p.current[probeKey(s)] = st.Attempt |
| + p.current[probeKey(s)] = boundAttempt{k: k, attempt: st.Attempt} |
| return nil |
| } |
| |
| // CurrentAttempt is the reservation the last Reserve journalled for this |
| // admission; the zero attempt, which the role launcher refuses, before any. |
| +// It reads the record again (the interface gives it no context, so its own |
| +// bounded one): once the record's index is past the bound attempt it answers |
| +// the zero attempt. |
| func (p *movesProbeAttempts) CurrentAttempt(s OperationStart) hostclaim.WakeRetryAttempt { |
| if p == nil { |
| return hostclaim.WakeRetryAttempt{} |
| } |
| p.mu.Lock() |
| - defer p.mu.Unlock() |
| - return p.current[probeKey(s)] |
| + cur, ok := p.current[probeKey(s)] |
| + p.mu.Unlock() |
| + if !ok { |
| + return hostclaim.WakeRetryAttempt{} |
| + } |
| + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| + defer cancel() |
| + if k, err := p.bound(ctx, s); err != nil || k != cur.k { |
| + return hostclaim.WakeRetryAttempt{} |
| + } |
| + return cur.attempt |
| } |
| |
| // Failed settles start n, already stopped and drained, as failed. |
| @@ -286,11 +345,15 @@ func (p *movesProbeAttempts) settle(ctx context.Context, s OperationStart, n int |
| if err != nil { |
| return err |
| } |
| + if err := bindSession(j, s, k, false); err != nil { |
| + return err |
| + } |
| st, ok, err := readStart(j, k) |
| if err != nil { |
| return err |
| } |
| - if !ok || st.Session != s.Owner.Session || st.N != n || st.State != probeReserved || st.Attempt != p.current[probeKey(s)] { |
| + cur := p.current[probeKey(s)] |
| + if !ok || st.Session != s.Owner.Session || st.N != n || st.State != probeReserved || cur.k != k || st.Attempt != cur.attempt { |
| return hostbox.ErrChanged |
| } |
| st.State = state |
| @@ -302,7 +365,17 @@ func (p *movesProbeAttempts) settle(ctx context.Context, s OperationStart, n int |
| // check again (again, the claim's ClaimProbeReadiness against the started |
| // role), with the process still alive after it (moves §7.7). |
| func (p *movesProbeAttempts) Healthy(ctx context.Context, s OperationStart, n int, cmd *exec.Cmd, again func(context.Context) error) error { |
| - if _, err := p.bound(ctx, s); err != nil { |
| + k, err := p.bound(ctx, s) |
| + if err != nil { |
| + return err |
| + } |
| + p.mu.Lock() |
| + j, err := p.journal(s) |
| + if err == nil { |
| + err = bindSession(j, s, k, false) |
| + } |
| + p.mu.Unlock() |
| + if err != nil { |
| return err |
| } |
| if again == nil || !probeAlive(cmd) { |
| @@ -326,7 +399,7 @@ func (p *movesProbeAttempts) Healthy(ctx context.Context, s OperationStart, n in |
| return fmt.Errorf("%w: the app exited during the settle", hostbox.ErrChanged) |
| } |
| asked, cancel := context.WithTimeout(ctx, ready) |
| - err := again(asked) |
| + err = again(asked) |
| cancel() |
| if err != nil { |
| return err |