fix(playlists): use CRON_TZ-prefixed cron expressions for per-job timezone
gocron v2's WithLocation is a SchedulerOption (process-wide), not a JobOption — there's no per-job location knob. To get per-user timezones we use a cron expression with the CRON_TZ= prefix, which the underlying robfig/cron parser honors: CRON_TZ=America/New_York 0 3 * * * Fires at 03:00 in the named zone every day. Same DST-correctness as the original WithLocation approach. Fall-back to UTC moves inline (was validateTimezoneOrUTC); kept the helper because the test file still exercises it. Caught by go vet on CI after slice 2 pushed: "cannot use gocron.WithLocation(loc) (value of type gocron.SchedulerOption) as gocron.JobOption value" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,12 @@ func (s *Scheduler) Refresh(ctx context.Context, userID pgtype.UUID) error {
|
||||
}
|
||||
|
||||
// registerLocked replaces the user's job entry. Caller must hold s.mu.
|
||||
//
|
||||
// gocron v2 doesn't expose a per-job location option — WithLocation is
|
||||
// a SchedulerOption that affects every job. To get per-user timezones
|
||||
// we use a cron expression with the CRON_TZ= prefix, which the
|
||||
// underlying robfig/cron parser honors. The expression "CRON_TZ=$tz
|
||||
// 0 3 * * *" fires at 03:00 in the named zone every day.
|
||||
func (s *Scheduler) registerLocked(ctx context.Context, userID pgtype.UUID, tz string) error {
|
||||
if existing, ok := s.jobs[userID]; ok {
|
||||
if err := s.gocron.RemoveJob(existing); err != nil {
|
||||
@@ -142,18 +148,21 @@ func (s *Scheduler) registerLocked(ctx context.Context, userID pgtype.UUID, tz s
|
||||
}
|
||||
delete(s.jobs, userID)
|
||||
}
|
||||
loc := validateTimezoneOrUTC(tz)
|
||||
// Fall back to UTC if the stored tz is unparseable so a corrupted
|
||||
// DB row doesn't error out registration.
|
||||
resolvedTZ := "UTC"
|
||||
if _, err := validateTimezone(tz); err == nil {
|
||||
resolvedTZ = tz
|
||||
}
|
||||
cronExpr := fmt.Sprintf("CRON_TZ=%s 0 %d * * *", resolvedTZ, dailyRebuildHour)
|
||||
job, err := s.gocron.NewJob(
|
||||
gocron.DailyJob(1, gocron.NewAtTimes(
|
||||
gocron.NewAtTime(dailyRebuildHour, 0, 0),
|
||||
)),
|
||||
gocron.CronJob(cronExpr, false /* withSeconds */),
|
||||
gocron.NewTask(func() {
|
||||
if err := BuildSystemPlaylists(ctx, s.pool, s.logger, userID, time.Now(), s.dataDir); err != nil {
|
||||
s.logger.Warn("scheduler: build failed",
|
||||
"user_id", uuidStringPL(userID), "err", err)
|
||||
}
|
||||
}),
|
||||
gocron.WithLocation(loc),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("register daily job: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user