package scheduler import ( "context" "time" "github.com/robfig/cron/v3" "go.uber.org/zap" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/leaderelection" siteusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/site" ) // QuotaResetScheduler handles scheduled usage resets for billing cycles type QuotaResetScheduler struct { cron *cron.Cron resetUsageUC *siteusecase.ResetMonthlyUsageUseCase leaderElection leaderelection.LeaderElection logger *zap.Logger enabled bool schedulePattern string } // cronLogger is a simple adapter for cron to use zap logger type cronLogger struct { logger *zap.Logger } func (l *cronLogger) Info(msg string, keysAndValues ...interface{}) { l.logger.Sugar().Infow(msg, keysAndValues...) } func (l *cronLogger) Error(err error, msg string, keysAndValues ...interface{}) { l.logger.Sugar().Errorw(msg, append(keysAndValues, "error", err)...) } // ProvideQuotaResetScheduler creates a new QuotaResetScheduler from config func ProvideQuotaResetScheduler( cfg *config.Config, resetUsageUC *siteusecase.ResetMonthlyUsageUseCase, leaderElection leaderelection.LeaderElection, logger *zap.Logger, ) *QuotaResetScheduler { enabled := cfg.Scheduler.QuotaResetEnabled schedulePattern := cfg.Scheduler.QuotaResetSchedule // Create cron with logger cronLog := &cronLogger{logger: logger.Named("cron")} c := cron.New( cron.WithLogger(cronLog), cron.WithChain( cron.Recover(cronLog), ), ) return &QuotaResetScheduler{ cron: c, resetUsageUC: resetUsageUC, leaderElection: leaderElection, logger: logger.Named("usage-reset-scheduler"), enabled: enabled, schedulePattern: schedulePattern, } } // Start starts the quota reset scheduler func (s *QuotaResetScheduler) Start() error { if !s.enabled { s.logger.Info("quota reset scheduler is disabled") return nil } s.logger.Info("starting quota reset scheduler", zap.String("schedule", s.schedulePattern)) // Schedule the quota reset job _, err := s.cron.AddFunc(s.schedulePattern, s.resetQuotas) if err != nil { s.logger.Error("failed to schedule quota reset job", zap.Error(err)) return err } // Start the cron scheduler s.cron.Start() s.logger.Info("quota reset scheduler started successfully") return nil } // Stop stops the quota reset scheduler func (s *QuotaResetScheduler) Stop() { if !s.enabled { return } s.logger.Info("stopping quota reset scheduler") ctx := s.cron.Stop() <-ctx.Done() s.logger.Info("quota reset scheduler stopped") } // resetQuotas is the cron job function that resets monthly usage counters func (s *QuotaResetScheduler) resetQuotas() { // Only execute if this instance is the leader if !s.leaderElection.IsLeader() { s.logger.Debug("skipping quota reset - not the leader instance", zap.String("instance_id", s.leaderElection.GetInstanceID())) return } s.logger.Info("executing scheduled usage reset as leader instance", zap.String("instance_id", s.leaderElection.GetInstanceID())) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() output, err := s.resetUsageUC.Execute(ctx) if err != nil { s.logger.Error("usage reset failed", zap.Error(err)) return } s.logger.Info("usage reset completed", zap.Int("processed_sites", output.ProcessedSites), zap.Int("reset_count", output.ResetCount), zap.Int("failed_count", output.FailedCount), zap.Time("processed_at", output.ProcessedAt)) }