| index 71887aeb0..927d651c3 100644 |
| --- a/internal/console/selfserve_pass.go |
| +++ b/internal/console/selfserve_pass.go |
| @@ -5,10 +5,12 @@ package console |
| |
| import ( |
| "context" |
| + "database/sql" |
| "errors" |
| "fmt" |
| "log" |
| "os" |
| + "time" |
| |
| "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/release" |
| @@ -67,3 +69,167 @@ func (s *Server) selfServeReevaluate(ctx context.Context) { |
| } |
| s.selfServeRecovery.Store(found) |
| } |
| + |
| +// selfServeTickInterval is the pass's period (spec §7: every 5 seconds). A |
| +// var so a test can shorten it. |
| +var selfServeTickInterval = 5 * time.Second |
| + |
| +// selfServeDrainBatch bounds the events one tick delivers. |
| +const selfServeDrainBatch = 100 |
| + |
| +// StartSelfServeMovesPass runs the self-serve moves pass while the console |
| +// is in self-serve mode: every selfServeTickInterval, one iteration admitted |
| +// through plan 1's gate as a writer. With the switch off it stops once |
| +// recovery mode ends (nothing left to recover), and it never runs on a |
| +// console that started with nothing to recover. A host-claim console has no |
| +// self-serve moves: it logs that and returns. |
| +func (s *Server) StartSelfServeMovesPass(ctx context.Context) { |
| + if s.cfg.HostClaim { |
| + log.Printf("console: self-serve moves pass: off on a host-claim console, where self-serve moves are not for it") |
| + return |
| + } |
| + t := time.NewTicker(selfServeTickInterval) |
| + defer t.Stop() |
| + for s.selfServeMode() { |
| + s.selfServeIteration(ctx) |
| + select { |
| + case <-ctx.Done(): |
| + return |
| + case <-t.C: |
| + } |
| + } |
| +} |
| + |
| +// selfServeIteration is one tick as a unit of plan 1's admission (mutating: |
| +// the pass writes records). It reports whether the tick ran. |
| +func (s *Server) selfServeIteration(ctx context.Context) bool { |
| + return s.lifetimesIteration(ctx, "self-serve moves", true, false, s.selfServeTick) |
| +} |
| + |
| +// selfServeTick is one pass: hold the lease (acquired or renewed), the |
| +// reservation scan (recovery mode re-evaluated from the journal and |
| +// active.json), then, only while this console holds the lease, the event |
| +// delivery and one step per open row in order. The lease is renewed every |
| +// selfServeLeaseRenewEvery while the tick runs, and checked with a fresh |
| +// clock before each row. |
| +func (s *Server) selfServeTick(ctx context.Context) { |
| + t := s.holdSelfServeLease(ctx) |
| + s.selfServeReevaluate(ctx) |
| + if t == nil { |
| + return |
| + } |
| + stop := s.keepSelfServeLease(ctx, t) |
| + defer stop() |
| + s.drainSelfServeEvents(ctx, t) |
| + rows, err := s.store.OpenSelfServeMoves() |
| + if err != nil { |
| + log.Printf("console: self-serve moves: the open rows: %v (retrying next tick)", err) |
| + return |
| + } |
| + for i := range rows { |
| + if !t.valid(s.selfServeNow()) { |
| + log.Printf("console: self-serve moves: the console lease ran out mid-tick; the rest wait for the next") |
| + return |
| + } |
| + if err := s.selfServeStep(ctx, t, &rows[i]); err != nil { |
| + log.Printf("console: self-serve move %s (%s): %v", rows[i].ID, rows[i].Host, err) |
| + } |
| + } |
| +} |
| + |
| +// holdSelfServeLease is the tick's lease: the one held, renewed when a |
| +// renewal is due, or a fresh acquisition when none is held or it ran out. |
| +// Nil when another console holds it or it could not be read. |
| +func (s *Server) holdSelfServeLease(ctx context.Context) *leaseToken { |
| + s.selfServeLeaseMu.Lock() |
| + defer s.selfServeLeaseMu.Unlock() |
| + now := s.selfServeNow() |
| + if t := s.selfServeLease; t.valid(now) { |
| + if !t.renewalDue(now) { |
| + return t |
| + } |
| + err := t.renew(ctx, now) |
| + if err == nil { |
| + return t |
| + } |
| + if !errors.Is(err, errSelfServeLeaseLost) { |
| + log.Printf("console: self-serve moves: renewing the console lease: %v", err) |
| + if t.valid(s.selfServeNow()) { |
| + return t |
| + } |
| + } |
| + } |
| + t, ok, err := s.acquireSelfServeLease(ctx, now) |
| + if err != nil { |
| + log.Printf("console: self-serve moves: the console lease: %v (the pass waits)", err) |
| + } |
| + if !ok { |
| + if err == nil && s.selfServeLease != nil { |
| + log.Printf("console: self-serve moves: another console holds the moves lease") |
| + } |
| + s.selfServeLease = nil |
| + return nil |
| + } |
| + s.selfServeLease = t |
| + return t |
| +} |
| + |
| +// keepSelfServeLease renews t every selfServeLeaseRenewEvery until the |
| +// returned stop is called, so a long tick keeps its lease. |
| +func (s *Server) keepSelfServeLease(ctx context.Context, t *leaseToken) (stop func()) { |
| + done := make(chan struct{}) |
| + finished := make(chan struct{}) |
| + go func() { |
| + defer close(finished) |
| + tick := time.NewTicker(selfServeLeaseRenewEvery) |
| + defer tick.Stop() |
| + for { |
| + select { |
| + case <-done: |
| + return |
| + case <-ctx.Done(): |
| + return |
| + case <-tick.C: |
| + if err := t.renew(ctx, s.selfServeNow()); err != nil { |
| + log.Printf("console: self-serve moves: renewing the console lease mid-tick: %v", err) |
| + } |
| + } |
| + } |
| + }() |
| + return func() { close(done); <-finished } |
| +} |
| + |
| +// drainSelfServeEvents delivers the undelivered events, each as one audit |
| +// row in the move's app's activity, at most once (DeliverSelfServeEvent), |
| +// under a context bounded by the lease. A failed delivery stays undelivered |
| +// for the next tick; the others go on. |
| +func (s *Server) drainSelfServeEvents(ctx context.Context, t *leaseToken) { |
| + evs, err := s.store.UnemittedSelfServeEvents(selfServeDrainBatch) |
| + if err != nil { |
| + log.Printf("console: self-serve moves: the undelivered events: %v (retrying next tick)", err) |
| + return |
| + } |
| + for _, e := range evs { |
| + if !t.valid(s.selfServeNow()) { |
| + return |
| + } |
| + dctx, cancel := context.WithDeadline(ctx, t.deadline()) |
| + err := s.store.DeliverSelfServeEvent(dctx, e, func(tx *sql.Tx) error { |
| + return auditActivityTx(tx, e.Account, e.App, "self-serve move", Activity{ |
| + Action: "instance-move", TargetType: "instance", Target: e.Host, Message: e.Phase, Detail: e.Reason}) |
| + }) |
| + cancel() |
| + if err != nil { |
| + log.Printf("console: self-serve move %s: delivering event %d: %v (retrying next tick)", e.ID, e.Seq, err) |
| + } |
| + } |
| +} |
| + |
| +// selfServeStep advances one row by at most one step. Task 7 writes it: the |
| +// row's deadlines, the judged record, reconciliation by the compatible |
| +// sets, the expiry action, abort and the phase function, every record write |
| +// through selfServeApply under selfServeWriteCtx. Until then it leaves |
| +// every row exactly as it found it. |
| +func (s *Server) selfServeStep(ctx context.Context, t *leaseToken, row *SelfServeMove) error { |
| + return nil |
| +} |