package storage import ( "context" "database/sql" "log/slog" "time" ) func (r *MySQLRepository) StartGameReconciler(ctx context.Context, logger *slog.Logger) { if !r.cfg.Game.Enabled || r.cfg.Game.Interval <= 0 { return } run := func() { since := currentStatDayStart(r.cfg.Dashboard.StatTimezones, r.cfg.Game.CurrentDayLag) until := since.AddDate(0, 0, 1) if err := r.RebuildGameMetricsRange(ctx, since, until); err != nil { if r.metrics != nil { r.metrics.RecordError(err) } logger.Error("dashboard game reconcile failed", "since", since, "until", until, "error", err) return } logger.Info("dashboard game reconcile applied", "since", since, "until", until) } go func() { run() ticker := time.NewTicker(r.cfg.Game.Interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: run() } } }() } func (r *MySQLRepository) RebuildGameMetricsRange(ctx context.Context, start, end time.Time) error { return r.WithDashboardLock(ctx, r.cfg.CDC.BackfillLockWait, func() error { return r.WithTx(ctx, func(tx *sql.Tx) error { for _, statTimezone := range r.cfg.Dashboard.StatTimezones { location, err := time.LoadLocation(statTimezone) if err != nil { location = time.UTC } for day := start.In(location); day.Before(end); day = day.AddDate(0, 0, 1) { dayStart := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, location) dayEnd := dayStart.AddDate(0, 0, 1) for _, sysOrigin := range r.cfg.Dashboard.SysOrigins { if err := r.replaceGameMetricDay(ctx, tx, statTimezone, sysOrigin, dayStart, dayEnd); err != nil { return err } } } if err := r.rebuildAllPeriodFromDays(ctx, tx, statTimezone); err != nil { return err } if err := r.rebuildAllGamePeriodFromDays(ctx, tx, statTimezone); err != nil { return err } } return nil }) }) }