| index 7f2b71f8c..8f372d8e5 100644 |
| --- a/internal/console/selfserve_store.go |
| +++ b/internal/console/selfserve_store.go |
| @@ -628,6 +628,19 @@ FROM self_serve_moves WHERE id = ?`, id).Scan(&issued, &pr.Kind, &pr.Phase, &pr. |
| // outstanding (issued, unconsumed) request whatever its kind and phase, or |
| // nil when the row left the phase or ended. |
| func (s *Store) IssueSelfServeRequest(id, kind, phase string, pr persistedRequest, now time.Time) (issued bool, existing *persistedRequest, err error) { |
| + return s.issueSelfServeRequest(id, kind, phase, pr, nil, now) |
| +} |
| + |
| +// IssueSelfServeRequestWithDeadline is IssueSelfServeRequest for a wait |
| +// that starts with its own allowance (the ready phase's maintenance round): |
| +// under the same predicate it sets the row's step deadline to stepDeadline |
| +// and the request deadline from it, in the one statement, so a crash can |
| +// never leave the round's deadline set without its request. |
| +func (s *Store) IssueSelfServeRequestWithDeadline(id, kind, phase string, pr persistedRequest, stepDeadline, now time.Time) (issued bool, existing *persistedRequest, err error) { |
| + return s.issueSelfServeRequest(id, kind, phase, pr, &stepDeadline, now) |
| +} |
| + |
| +func (s *Store) issueSelfServeRequest(id, kind, phase string, pr persistedRequest, stepDeadline *time.Time, now time.Time) (issued bool, existing *persistedRequest, err error) { |
| if pr.Nonce == "" || kind == "" || ssNanos(now) <= 0 { |
| return false, nil, fmt.Errorf("console: self-serve move %s: a request needs a nonce, a kind and a time", id) |
| } |
| @@ -639,13 +652,21 @@ func (s *Store) IssueSelfServeRequest(id, kind, phase string, pr persistedReques |
| if err != nil { |
| return false, nil, err |
| } |
| + // With stepDeadline the row's step deadline is set first in the SET |
| + // list; SQLite evaluates every expression against the row as it was, |
| + // so request_deadline names the new deadline explicitly. |
| + set, args := `step_deadline = step_deadline, request_deadline = CASE WHEN ? THEN step_deadline ELSE 0 END`, []any{ssBool(selfServeAbortExpiring[kind])} |
| + if stepDeadline != nil { |
| + set = `step_deadline = ?, request_deadline = CASE WHEN ? THEN ? ELSE 0 END` |
| + args = []any{ssNanos(*stepDeadline), ssBool(selfServeAbortExpiring[kind]), ssNanos(*stepDeadline)} |
| + } |
| + args = append([]any{ssNanos(now), kind, phase, pr.Nonce, inc, seq, ssNanos(pr.Proposed)}, args...) |
| + args = append(args, ssNanos(now), id, phase) |
| res, err := s.db.Exec(`UPDATE self_serve_moves SET |
| request_issued = ?, request_kind = ?, request_phase = ?, request_nonce = ?, request_expect_inc = ?, request_expect_seq = ?, |
| - request_proposed = ?, request_deadline = CASE WHEN ? THEN step_deadline ELSE 0 END, |
| + request_proposed = ?, `+set+`, |
| request_confirmed = 0, request_consumed = 0, request_voided = 0, request_abandoned = 0, request_expired = 0, updated_at = ? |
| -WHERE id = ? AND phase = ? AND outcome = '' AND (request_consumed <> 0 OR request_issued = 0)`, |
| - ssNanos(now), kind, phase, pr.Nonce, inc, seq, ssNanos(pr.Proposed), ssBool(selfServeAbortExpiring[kind]), ssNanos(now), |
| - id, phase) |
| +WHERE id = ? AND phase = ? AND outcome = '' AND (request_consumed <> 0 OR request_issued = 0)`, args...) |
| won, err := ssWon(res, err) |
| if err != nil || won { |
| return won, nil, err |
| @@ -720,17 +741,38 @@ WHERE id = ? AND request_nonce = ? AND request_consumed = 0 AND request_confirme |
| } |
| |
| // AdvanceSelfServe moves the row from one phase to the next and inserts |
| -// the phase event in the same transaction. One UPDATE, predicated |
| +// the phase event in the same transaction, entering the new phase at its |
| +// first substep (the job's own progression). It is |
| +// AdvanceSelfServeToStep with selfServeFirstSubstep[to]. |
| +func (s *Store) AdvanceSelfServe(id, from, to, reason string, step, window time.Time, nonce string, now time.Time) (bool, error) { |
| + return s.AdvanceSelfServeToStep(id, from, to, reason, selfServeFirstSubstep[to], step, window, nonce, now) |
| +} |
| + |
| +// selfServeMaintainEvery is the ready phase's idle interval: a maintenance |
| +// restage is due this long after the last one ended (spec §6.2). |
| +const selfServeMaintainEvery = 10 * time.Minute |
| + |
| +// AdvanceSelfServeToStep moves the row from one phase to the next, entering |
| +// it at substep: the first substep for the job's own progression |
| +// (AdvanceSelfServe), the substep the record dictates for reconciliation, |
| +// and restage's adopted or issue for the cut handoff. One UPDATE, predicated |
| // |
| // phase = ? AND (abort_requested_at = 0 OR <to> IN ('reverting','done','opened','health','finishing')) |
| // AND (<nonce> = '' OR (request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0)) |
| // |
| -// sets the phase, phase_started_at, the step and window deadlines, the new |
| -// phase's first substep and reason, clears an expiry bound to another |
| -// phase, and with a nonce consumes the request: consumption and the advance |
| -// are one statement, so a stale worker holding another nonce, or the same |
| -// nonce after a void, cannot advance. |
| -func (s *Store) AdvanceSelfServe(id, from, to, reason string, step, window time.Time, nonce string, now time.Time) (bool, error) { |
| +// sets the phase, phase_started_at, the step and window deadlines, the |
| +// substep and reason, clears an expiry bound to another phase, and with a |
| +// nonce consumes the request: consumption and the advance are one |
| +// statement, so a stale worker holding another nonce, or the same nonce |
| +// after a void, cannot advance. Entering staging stamps staging_started_at |
| +// (once), and entering ready starts the idle interval |
| +// (next_maintenance_at = now + 10 min), in the same statement. The phase |
| +// event goes in the same transaction. |
| +func (s *Store) AdvanceSelfServeToStep(id, from, to, reason, substep string, step, window time.Time, nonce string, now time.Time) (bool, error) { |
| + var nextMaint int64 |
| + if to == ssReady { |
| + nextMaint = ssNanos(now.Add(selfServeMaintainEvery)) |
| + } |
| tx, err := s.db.Begin() |
| if err != nil { |
| return false, err |
| @@ -743,12 +785,17 @@ func (s *Store) AdvanceSelfServe(id, from, to, reason string, step, window time. |
| expiry_deadline = CASE WHEN expiry_phase <> ? THEN 0 ELSE expiry_deadline END, |
| expiry_phase = CASE WHEN expiry_phase <> ? THEN '' ELSE expiry_phase END, |
| request_consumed = CASE WHEN ? <> '' THEN 1 ELSE request_consumed END, |
| + staging_started_at = CASE WHEN ? = 'staging' AND staging_started_at = 0 THEN ? ELSE staging_started_at END, |
| + next_maintenance_at = CASE WHEN ? <> 0 THEN ? ELSE next_maintenance_at END, |
| updated_at = ? |
| WHERE id = ? AND phase = ? AND (abort_requested_at = 0 OR ? IN `+selfServeAbortAdvances+`) |
| AND (? = '' OR (request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0))`, |
| - to, ssNanos(now), ssNanos(step), ssNanos(window), selfServeFirstSubstep[to], reason, |
| + to, ssNanos(now), ssNanos(step), ssNanos(window), substep, reason, |
| to, to, to, to, |
| - nonce, ssNanos(now), |
| + nonce, |
| + to, ssNanos(now), |
| + nextMaint, nextMaint, |
| + ssNanos(now), |
| id, from, to, nonce, nonce) |
| won, err := ssWon(res, err) |
| if err != nil || !won { |
| @@ -944,6 +991,86 @@ func (s *Store) SetSelfServeOpenRecordPending(id string, pending bool) error { |
| return s.setSelfServe("open record", id, `open_record_pending = ?`, ssBool(pending)) |
| } |
| |
| +// ---- the job's own decisions (self-serve moves plan 2 Task 7) --------------- |
| + |
| +// ExpireSelfServeRequest decides, durably, that the outstanding request's |
| +// deadline passed before it was confirmed: one UPDATE predicated on the |
| +// nonce, the deadline the worker saw and the cut state it read with the |
| +// request, |
| +// |
| +// WHERE request_nonce = ? AND request_deadline = ? AND cut_requested_at = ? |
| +// AND request_consumed = 0 AND request_expired = 0 |
| +// |
| +// so a cut that landed since (and extended the deadline) makes the decision |
| +// lose, and only one worker ever wins it. Only the winner may fence, void |
| +// and abort (selfServeApply step 2). |
| +func (s *Store) ExpireSelfServeRequest(id, nonce string, deadline, sawCutRequestedAt time.Time) (bool, error) { |
| + return ssWon(s.db.Exec(`UPDATE self_serve_moves SET request_expired = 1, updated_at = ? |
| +WHERE id = ? AND request_nonce = ? AND request_deadline = ? AND cut_requested_at = ? AND request_consumed = 0 AND request_expired = 0`, |
| + time.Now().UnixNano(), id, nonce, ssNanos(deadline), ssNanos(sawCutRequestedAt))) |
| +} |
| + |
| +// VoidExpiredSelfServeRequest retires a won expiry whose fenced observation |
| +// carries no receipt, and records the abort it obliges, in one UPDATE: |
| +// |
| +// WHERE request_nonce = ? AND request_expired = 1 AND request_confirmed = 0 AND request_consumed = 0 |
| +// |
| +// It sets request_consumed and request_voided, and abort_requested_at only |
| +// when it is zero, so an abort a cancellation already requested keeps its |
| +// own time (and its reason: outcome_reason is the expiry's only when this |
| +// void is what requests the abort). Zero rows: the request changed under |
| +// the worker, which reloads it and dispatches again. |
| +func (s *Store) VoidExpiredSelfServeRequest(id, nonce string, now time.Time, reason string) (bool, error) { |
| + if ssNanos(now) <= 0 { |
| + return false, fmt.Errorf("console: self-serve move %s: a void needs a time", id) |
| + } |
| + return ssWon(s.db.Exec(`UPDATE self_serve_moves SET |
| + request_consumed = 1, request_voided = 1, |
| + outcome_reason = CASE WHEN abort_requested_at = 0 THEN ? ELSE outcome_reason END, |
| + abort_requested_at = CASE WHEN abort_requested_at = 0 THEN ? ELSE abort_requested_at END, |
| + updated_at = ? |
| +WHERE id = ? AND request_nonce = ? AND request_expired = 1 AND request_confirmed = 0 AND request_consumed = 0`, |
| + reason, ssNanos(now), ssNanos(now), id, nonce)) |
| +} |
| + |
| +// ConsumeSelfServeMaintenance is the ready phase's maintenance-to-idle |
| +// consume: ConsumeSelfServeRequest's predicate with the cut's, |
| +// |
| +// WHERE request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0 AND cut_requested_at = 0 |
| +// |
| +// so it never runs after a cut won (the stale worker's UPDATE matches |
| +// nothing; it re-reads the row and takes the cut handoff). It leaves the |
| +// row idle: substep round, no step deadline, the next round due at |
| +// nextMaintenance. window_deadline is not touched. |
| +func (s *Store) ConsumeSelfServeMaintenance(id, nonce string, nextMaintenance time.Time) (bool, error) { |
| + return ssWon(s.db.Exec(`UPDATE self_serve_moves SET |
| + request_consumed = 1, phase_step = 'round', step_deadline = 0, next_maintenance_at = ?, updated_at = ? |
| +WHERE id = ? AND request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0 AND cut_requested_at = 0`, |
| + ssNanos(nextMaintenance), time.Now().UnixNano(), id, nonce)) |
| +} |
| + |
| +// AbortSelfServe is the job's own abort decision (a failed fact, a replaced |
| +// participant, an expiry whose action is abort): abort_requested_at and the |
| +// outcome's reason, once, on an open row that has no abort yet: |
| +// |
| +// WHERE outcome = '' AND abort_requested_at = 0 |
| +// |
| +// reason is also the row's reason, so the view says why the move is going |
| +// back. False: an abort (a cancellation, an earlier decision) was already |
| +// requested, and it keeps its own time and reason. |
| +func (s *Store) AbortSelfServe(id, reason string, now time.Time) (bool, error) { |
| + if ssNanos(now) <= 0 { |
| + return false, fmt.Errorf("console: self-serve move %s: an abort needs a time", id) |
| + } |
| + return ssWon(s.db.Exec(`UPDATE self_serve_moves SET abort_requested_at = ?, outcome_reason = ?, reason = ?, updated_at = ? |
| +WHERE id = ? AND outcome = '' AND abort_requested_at = 0`, ssNanos(now), reason, reason, ssNanos(now), id)) |
| +} |
| + |
| +// SetSelfServeReason records why an open row waits (the view's reason). |
| +func (s *Store) SetSelfServeReason(id, reason string) error { |
| + return s.setSelfServe("reason", id, `reason = ?`, reason) |
| +} |
| + |
| // ---- the reclaim rule's evidence (offlinereserve.go) --------------------------- |
| |
| var _ selfServeRowEvidence = (*Store)(nil) |