| index 75299e38c..a07a5dbc1 100644 |
| --- a/internal/console/selfserve_job.go |
| +++ b/internal/console/selfserve_job.go |
| @@ -1125,8 +1125,11 @@ func selfServeStagerReport(row *SelfServeMove, mv *provision.MoveState, ev offli |
| // later than the confirmed request's proposed stamp, so an operator round |
| // moved it without a receipt. The job abandons its wait (one UPDATE) and |
| // issues a fresh bound round on the next step; it never adopts the |
| -// operator's round. |
| -func (s *Server) selfServeSuperseded(row *SelfServeMove, mv *provision.MoveState) (bool, error) { |
| +// operator's round. The abandonment sets the replacement round's fresh |
| +// step deadline in the same statement (final review F8), so a crash |
| +// between the abandon and the issue never leaves the replacement on the |
| +// old round's remainder. |
| +func (s *Server) selfServeSuperseded(row *SelfServeMove, mv *provision.MoveState, now time.Time) (bool, error) { |
| var stamp *time.Time |
| switch row.Request.Kind { |
| case "stage", "maintain": |
| @@ -1140,10 +1143,24 @@ func (s *Server) selfServeSuperseded(row *SelfServeMove, mv *provision.MoveState |
| return false, nil |
| } |
| TestHooks.point(row.Request.Kind + ":abandon") |
| - _, err := s.store.AbandonSelfServeRequest(row.ID, row.Request.Nonce, row.StepDeadline) |
| + _, err := s.store.AbandonSelfServeRequest(row.ID, row.Request.Nonce, selfServeReplacementDeadline(row, now)) |
| return true, err |
| } |
| |
| +// selfServeReplacementDeadline is the fresh allowance of the round that |
| +// replaces a superseded one, by the phase the round was issued in: the |
| +// phase's own deadline (a staging round's, the prechecks' wait, the cut's |
| +// copy allowance), and in ready the maintenance round's copy allowance. |
| +func selfServeReplacementDeadline(row *SelfServeMove, now time.Time) time.Time { |
| + if row.Request.Phase == ssReady { |
| + return now.Add(selfServeCopyAllowance(row)) |
| + } |
| + if d := selfServePhaseDeadline(row, row.Request.Phase, now); !d.IsZero() { |
| + return d |
| + } |
| + return row.StepDeadline |
| +} |
| + |
| // selfServeStaging is the pre-copy, round after round: a round's report |
| // (the stager's, for the confirmed request's stamp) either meets the ready |
| // rule (or the bound: 30 minutes or 10 rounds) and the row goes to the |
| @@ -1156,7 +1173,7 @@ func (s *Server) selfServeStaging(ctx context.Context, t *leaseToken, row *SelfS |
| return nil |
| } |
| if selfServeWaiting(row, "stage", "maintain") { |
| - if sup, err := s.selfServeSuperseded(row, mv); sup || err != nil { |
| + if sup, err := s.selfServeSuperseded(row, mv, now); sup || err != nil { |
| return err |
| } |
| ev, err := s.readOfflineEvidence(ctx, row.Host, row.ID, mv) |
| @@ -1249,7 +1266,7 @@ func (s *Server) selfServePrechecks(ctx context.Context, t *leaseToken, row *Sel |
| } |
| if row.PhaseStep == ssStepPrecheck { |
| if selfServeWaiting(row, "precheck") { |
| - if sup, err := s.selfServeSuperseded(row, mv); sup || err != nil { |
| + if sup, err := s.selfServeSuperseded(row, mv, now); sup || err != nil { |
| return err |
| } |
| fleet, ok := s.readOfflineFleet(ctx) |
| @@ -1359,7 +1376,7 @@ func (s *Server) selfServeReady(ctx context.Context, t *leaseToken, row *SelfSer |
| // An operator round superseded the outstanding one: it is |
| // abandoned, and the next step finds none and restage writes the |
| // cut's own round. |
| - if sup, err := s.selfServeSuperseded(row, mv); sup || err != nil { |
| + if sup, err := s.selfServeSuperseded(row, mv, now); sup || err != nil { |
| return err |
| } |
| ev, err := s.readOfflineEvidence(ctx, row.Host, row.ID, mv) |
| @@ -1384,7 +1401,7 @@ func (s *Server) selfServeReady(ctx context.Context, t *leaseToken, row *SelfSer |
| _, err := s.selfServeApply(ctx, t, row, request{Kind: "maintain", Phase: ssReady}, nil, selfServeRestageMut(row.ID)) |
| return s.selfServePreOfflineRefusal(row, err, now) |
| } |
| - if sup, err := s.selfServeSuperseded(row, mv); sup || err != nil { |
| + if sup, err := s.selfServeSuperseded(row, mv, now); sup || err != nil { |
| return err |
| } |
| ev, err := s.readOfflineEvidence(ctx, row.Host, row.ID, mv) |
| @@ -1435,7 +1452,7 @@ func (s *Server) selfServeRestage(ctx context.Context, t *leaseToken, row *SelfS |
| return err |
| } |
| if selfServeWaiting(row, "stage") && row.Request.Phase == ssRestage { |
| - if sup, err := s.selfServeSuperseded(row, mv); sup || err != nil { |
| + if sup, err := s.selfServeSuperseded(row, mv, now); sup || err != nil { |
| return err |
| } |
| ev, err := s.readOfflineEvidence(ctx, row.Host, row.ID, mv) |
| @@ -1660,7 +1677,7 @@ func (s *Server) selfServeBarrier(ctx context.Context, t *leaseToken, row *SelfS |
| }) |
| return s.selfServePreOfflineRefusal(row, err, now) |
| } |
| - if sup, err := s.selfServeSuperseded(row, mv); sup || err != nil { |
| + if sup, err := s.selfServeSuperseded(row, mv, now); sup || err != nil { |
| return err |
| } |
| fleet, ok := s.readOfflineFleet(ctx) |