package scoreboard import ( "context" "log/slog" "sync/atomic" "time" ) // FetchFunc retrieves a fresh set of scoreboard entries. type FetchFunc func(ctx context.Context) ([]Entry, error) // Refresher periodically refreshes a Store from a FetchFunc, keeping the last // good snapshot when a refresh fails. type Refresher struct { store *Store fetch FetchFunc interval time.Duration logger *slog.Logger lastErr atomic.Pointer[refreshError] successN atomic.Int64 failureN atomic.Int64 } type refreshError struct { At time.Time Err error } // NewRefresher wires a refresher. interval must be > 0. func NewRefresher(store *Store, fetch FetchFunc, interval time.Duration, logger *slog.Logger) *Refresher { if logger == nil { logger = slog.Default() } return &Refresher{store: store, fetch: fetch, interval: interval, logger: logger} } // RefreshOnce performs a single refresh, updating the store on success. func (r *Refresher) RefreshOnce(ctx context.Context) error { start := time.Now() entries, err := r.fetch(ctx) if err != nil { r.failureN.Add(1) r.lastErr.Store(&refreshError{At: time.Now(), Err: err}) r.logger.Error("scoreboard refresh failed", "err", err, "serving_stale", r.store.Ready()) return err } r.store.Set(entries, time.Now()) r.successN.Add(1) r.logger.Info("scoreboard refreshed", "entries", len(entries), "took", time.Since(start)) return nil } // Run blocks, refreshing on each tick until ctx is cancelled. It does NOT // refresh immediately; call RefreshOnce first if you want a warm start. func (r *Refresher) Run(ctx context.Context) { t := time.NewTicker(r.interval) defer t.Stop() for { select { case <-ctx.Done(): r.logger.Info("scoreboard refresher stopping") return case <-t.C: _ = r.RefreshOnce(ctx) } } } // Stats reports refresh counters and the last error (nil if none) for /metrics // or health output. func (r *Refresher) Stats() (success, failure int64, lastErr error, lastErrAt time.Time) { if e := r.lastErr.Load(); e != nil { lastErr, lastErrAt = e.Err, e.At } return r.successN.Load(), r.failureN.Load(), lastErr, lastErrAt }