Skip to content

Commit 4251e1b

Browse files
committed
fix(scheduler): dedupe a job scheduled on the same cadence twice (rate auto-schedule + explicit Scheduler.ts entry) so it fires once, not twice
1 parent 3cd543d commit 4251e1b

1 file changed

Lines changed: 24 additions & 0 deletions

File tree

storage/framework/core/scheduler/src/schedule.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ import { acquireSchedulerLock } from './scheduler-lock'
2828
*/
2929
export class Schedule implements UntimedSchedule {
3030
private static jobs = new Map<string, ScheduledJob>()
31+
// Dedupe registry: normalized job name + cron pattern → the timer already
32+
// started for it, so scheduling the same job on the same cadence twice (a
33+
// Job's `rate` auto-scheduled by the runner + an explicit Scheduler.ts entry)
34+
// creates ONE timer, not two. See start().
35+
private static scheduledKeys = new Map<string, ScheduledJob>()
3136
private cronPattern = ''
3237
private intervalMs: number | null = null
3338
private timezone: Timezone = 'America/Los_Angeles'
@@ -595,6 +600,22 @@ export class Schedule implements UntimedSchedule {
595600
return { stop: () => {}, nextRun: () => null }
596601
}
597602

603+
// Dedupe: the same job scheduled on the same cadence twice — e.g. a Job's
604+
// `rate` auto-scheduled by the runner AND an explicit `schedule.job(...)` in
605+
// the app's Scheduler.ts — must create ONE timer, not two (else it fires
606+
// twice). Key on the normalized name + pattern, so a job intentionally
607+
// scheduled on two DIFFERENT cadences still gets both timers.
608+
const dedupeKey = this.options.name
609+
? `${this.options.name.toLowerCase().replace(/[^a-z0-9]+/g, '')}::${this.cronPattern || `every:${this.intervalMs}`}`
610+
: null
611+
if (dedupeKey) {
612+
const existing = Schedule.scheduledKeys.get(dedupeKey)
613+
if (existing) {
614+
log.debug(`[scheduler] duplicate schedule for "${this.options.name}" (${this.cronPattern || `every ${this.intervalMs}ms`}) skipped`)
615+
return existing
616+
}
617+
}
618+
598619
const task = this.wrapTask(this.task)
599620
let stopped = false
600621
let timer: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval> | null = null
@@ -714,6 +735,9 @@ export class Schedule implements UntimedSchedule {
714735
if (this.options.name) {
715736
Schedule.jobs.set(this.options.name, job)
716737
}
738+
if (dedupeKey) {
739+
Schedule.scheduledKeys.set(dedupeKey, job)
740+
}
717741

718742
log.info(`Scheduled task with pattern: ${this.cronPattern} in timezone: ${this.timezone}`)
719743
return job

0 commit comments

Comments
 (0)