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.
|
// 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 {
|
func (s *Scheduler) registerLocked(ctx context.Context, userID pgtype.UUID, tz string) error {
|
||||||
if existing, ok := s.jobs[userID]; ok {
|
if existing, ok := s.jobs[userID]; ok {
|
||||||
if err := s.gocron.RemoveJob(existing); err != nil {
|
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)
|
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(
|
job, err := s.gocron.NewJob(
|
||||||
gocron.DailyJob(1, gocron.NewAtTimes(
|
gocron.CronJob(cronExpr, false /* withSeconds */),
|
||||||
gocron.NewAtTime(dailyRebuildHour, 0, 0),
|
|
||||||
)),
|
|
||||||
gocron.NewTask(func() {
|
gocron.NewTask(func() {
|
||||||
if err := BuildSystemPlaylists(ctx, s.pool, s.logger, userID, time.Now(), s.dataDir); err != nil {
|
if err := BuildSystemPlaylists(ctx, s.pool, s.logger, userID, time.Now(), s.dataDir); err != nil {
|
||||||
s.logger.Warn("scheduler: build failed",
|
s.logger.Warn("scheduler: build failed",
|
||||||
"user_id", uuidStringPL(userID), "err", err)
|
"user_id", uuidStringPL(userID), "err", err)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
gocron.WithLocation(loc),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("register daily job: %w", err)
|
return fmt.Errorf("register daily job: %w", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user