From b663d92fa4ef143ee2b4ebaa98150dac321b46da Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 2 Apr 2026 08:23:07 +0700 Subject: [PATCH] fix(cron): skip loadClaimedJob for manual runs Allow manual job runs even when job is disabled by checking job ID before claiming. Prevents skipping manually triggered executions. --- internal/store/pg/cron_exec.go | 6 ++++-- internal/store/pg/cron_scheduler.go | 22 ++++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/internal/store/pg/cron_exec.go b/internal/store/pg/cron_exec.go index fe95d50f..463d00c7 100644 --- a/internal/store/pg/cron_exec.go +++ b/internal/store/pg/cron_exec.go @@ -47,8 +47,10 @@ func (s *PGCronStore) RunJob(ctx context.Context, jobID string, force bool) (boo s.emitEvent(store.CronEvent{Action: "running", JobID: job.ID, JobName: job.Name, UserID: job.UserID}) - // Use executeOneJob for proper state updates, run logging, and retry - s.executeOneJob(*job, handler) + // Run directly without reload — job already loaded and claimed above. + // reloadClaimed=false skips loadClaimedJob (which requires enabled=true), + // allowing manual runs on disabled jobs. + s.executeOneJob(*job, handler, false) s.mu.Lock() s.cacheLoaded = false s.mu.Unlock() diff --git a/internal/store/pg/cron_scheduler.go b/internal/store/pg/cron_scheduler.go index 18b64efa..34239c25 100644 --- a/internal/store/pg/cron_scheduler.go +++ b/internal/store/pg/cron_scheduler.go @@ -176,7 +176,7 @@ func (s *PGCronStore) checkAndRunDueJobs() { wg.Add(1) go func(job store.CronJob) { defer wg.Done() - s.executeOneJob(job, handler) + s.executeOneJob(job, handler, true) }(job) } wg.Wait() @@ -188,14 +188,20 @@ func (s *PGCronStore) checkAndRunDueJobs() { } // executeOneJob runs a single cron job with retry, logs the result, and updates next_run_at. -func (s *PGCronStore) executeOneJob(job store.CronJob, handler func(job *store.CronJob) (*store.CronJobResult, error)) { - if id, parseErr := uuid.Parse(job.ID); parseErr == nil { - freshJob, ok := s.loadClaimedJob(id) - if !ok { - slog.Info("cron job skipped after claim state changed", "id", job.ID) - return +// executeOneJob runs a claimed job. When reloadClaimed is true (scheduler path), +// it re-reads the job from DB to verify claim invariants (enabled + next_run_at IS NULL). +// When false (manual RunJob path), it uses the already-loaded job directly — +// skipping the reload avoids the enabled=true filter that would reject disabled jobs. +func (s *PGCronStore) executeOneJob(job store.CronJob, handler func(job *store.CronJob) (*store.CronJobResult, error), reloadClaimed bool) { + if reloadClaimed { + if id, parseErr := uuid.Parse(job.ID); parseErr == nil { + freshJob, ok := s.loadClaimedJob(id) + if !ok { + slog.Info("cron job skipped after claim state changed", "id", job.ID) + return + } + job = *freshJob } - job = *freshJob } startTime := time.Now()