package moves import ( "time" "github.com/carlosframework/platform/internal/provision" ) // AbortAfter is how long a Phase 1 move may stay preparing before it aborts // itself (spec §7.11: "a move whose readiness never comes aborts itself // after a bound, so an unreachable target never needs a human"; Plan 5 // ruling K18). §13 pages an operator at 60 minutes with no promotable round; // the abort comes at six hours. The console's sweep (Plan 5 Task 7) CASes // provision.MoveGuardedAbort when AbortDue says so; nothing here writes. const AbortAfter = 6 * time.Hour // AbortDue reports whether rec's move is due its automatic abort at now: a // Phase 1 move (not legacy) still preparing, AbortAfter or more after the // later of its RequestedAt and its ReadyAt. // // The bound is measured from max(RequestedAt, ReadyAt) (ruling K18), not // from Move.UpdatedAt, which every record write stamps and so says nothing // about readiness. Today a preparing move has no ReadyAt (nothing demotes a // ready move to preparing), so the bound runs from the request; taking the // later of the two keeps it right should a demotion ever stamp one. A ready // move is not due: readiness came, and the cut is the operator's (or the // automatic cut's). A move with no request time is never due: there is no // evidence to measure from. func AbortDue(rec provision.Record, now time.Time) bool { m := rec.Move if m == nil || m.Legacy() || m.State != provision.MovePreparing || m.RequestedAt.IsZero() { return false } return !now.Before(PreparingSince(m).Add(AbortAfter)) } // PreparingSince is where a preparing move's clocks run from: the later of // its RequestedAt and its ReadyAt (ruling K18). The six-hour abort (AbortDue) // and the console's 60-minute page (§13) both measure from it, so the two // can never disagree about how long a move has prepared (Plan 6 preflight // L29). func PreparingSince(m *provision.MoveState) time.Time { from := m.RequestedAt if m.ReadyAt != nil && m.ReadyAt.After(from) { from = *m.ReadyAt } return from }