79 lines
2.1 KiB
Go
79 lines
2.1 KiB
Go
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
|
|
}
|