@mastra/core
Version:
1 lines • 18.4 kB
Source Map (JSON)
{"version":3,"file":"scheduler-D9Mqp8B5.cjs","names":["MastraBase","RegisteredLogger","#schedulesStore","#pubsub","#config","#started","#stopping","#missingWorkflowCounts","#runTick","#intervalHandle","#inflightTick","#processTick","#fireSchedule","#ensureTargetReady","computeNextFireAt","#notifyError","#publishTargetStart"],"sources":["../src/workflows/scheduler/scheduler.ts"],"sourcesContent":["import { MastraBase } from '../../base';\nimport type { PubSub } from '../../events/pubsub';\nimport { RegisteredLogger } from '../../logger/constants';\nimport type { Schedule, ScheduleTrigger, SchedulesStorage } from '../../storage/domains/schedules/base';\nimport { computeNextFireAt } from './cron';\nimport type { SchedulerConfig } from './types';\n\nconst TOPIC_WORKFLOWS = 'workflows';\nexport const TOPIC_AGENT_SCHEDULES = 'agent-schedules';\nconst DEFAULT_TICK_INTERVAL_MS = 10_000;\nconst DEFAULT_BATCH_SIZE = 100;\nconst DEFAULT_MISSES_BEFORE_DELETE = 3;\n\n/**\n * Drives cron-based workflow triggers.\n *\n * On each tick the scheduler:\n * 1. Loads schedules whose `nextFireAt <= now` from storage.\n * 2. Computes the next fire time from the cron expression.\n * 3. Atomically advances `nextFireAt` via compare-and-swap. Only one\n * instance across many polling the same storage can claim a fire.\n * 4. Publishes a `workflow.start` event on the `workflows` pubsub topic.\n * 5. Records the trigger in the schedule's history.\n *\n * The scheduler does **not** execute workflows. The existing\n * `WorkflowEventProcessor` consumes `workflow.start` events and runs them.\n */\nexport class Scheduler extends MastraBase {\n #schedulesStore: SchedulesStorage;\n #pubsub: PubSub;\n #config: Required<Pick<SchedulerConfig, 'tickIntervalMs' | 'batchSize'>> & SchedulerConfig;\n\n #intervalHandle?: ReturnType<typeof setInterval>;\n #inflightTick?: Promise<void>;\n #started = false;\n #stopping = false;\n\n /**\n * Per-schedule count of consecutive ticks where the target workflow was\n * not registered with the host Mastra instance. Reset when the workflow\n * resolves or the schedule is deleted. Used to ride out deploy/startup\n * ordering races before reclaiming a ghost row.\n */\n #missingWorkflowCounts = new Map<string, number>();\n\n constructor({\n schedulesStore,\n pubsub,\n config,\n }: {\n schedulesStore: SchedulesStorage;\n pubsub: PubSub;\n config?: SchedulerConfig;\n }) {\n super({ component: RegisteredLogger.WORKFLOW, name: 'Scheduler' });\n this.#schedulesStore = schedulesStore;\n this.#pubsub = pubsub;\n this.#config = {\n ...config,\n tickIntervalMs: config?.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS,\n batchSize: config?.batchSize ?? DEFAULT_BATCH_SIZE,\n };\n }\n\n /** Start the periodic tick loop. Runs an immediate tick first. */\n async start(): Promise<void> {\n if (this.#started) return;\n this.#started = true;\n this.#stopping = false;\n // Fresh process / fresh grace window — old miss counts shouldn't carry\n // over into a new start() since the workflow registry may now look\n // different.\n this.#missingWorkflowCounts.clear();\n\n try {\n // Run one tick immediately so newly-due schedules don't wait the full interval.\n await this.#runTick();\n\n // If stop() ran concurrently with the warm-up tick, don't arm a new\n // interval afterwards — the caller has already asked us to shut down.\n if (this.#stopping || !this.#started) return;\n\n this.#intervalHandle = setInterval(() => {\n // Swallow rejections here so a tick failure can't surface as an\n // unhandled promise rejection and crash the host process. #processTick\n // already logs its own errors and notifies onError, so we only need a\n // belt-and-braces logger.error for anything that escapes.\n void this.#runTick().catch(err => {\n this.logger.error('Scheduler tick crashed', { error: err });\n });\n }, this.#config.tickIntervalMs);\n\n // Don't keep the process alive just because the scheduler is polling.\n // The process should be able to exit when all other work is done.\n // Without .unref(), the setInterval prevents clean shutdown in\n // scripts that create a Mastra instance (which auto-creates the\n // notification dispatch workflow with a cron schedule) and exit\n // after a single agent.generate() call.\n // Optional call: on runtimes where setInterval returns a number\n // (e.g. Cloudflare Workers) there is no unref and nothing to release.\n this.#intervalHandle.unref?.();\n } catch (err) {\n // Reset state so a future start() can retry. Without this, a failed\n // warm-up tick would leave #started=true with no interval armed and\n // every subsequent start() call would silently no-op.\n this.#started = false;\n this.#stopping = false;\n throw err;\n }\n }\n\n /** Stop the tick loop and wait for any in-flight tick to finish. */\n async stop(): Promise<void> {\n if (!this.#started) return;\n this.#stopping = true;\n\n if (this.#intervalHandle) {\n clearInterval(this.#intervalHandle);\n this.#intervalHandle = undefined;\n }\n\n if (this.#inflightTick) {\n try {\n await this.#inflightTick;\n } catch {\n // tick errors are already logged; swallow during shutdown\n }\n }\n\n this.#started = false;\n this.#stopping = false;\n }\n\n /** True when the scheduler is currently running its tick loop. */\n get isRunning(): boolean {\n return this.#started;\n }\n\n /**\n * Run a single tick. Public for tests; production callers should rely\n * on the interval started by `start()`.\n */\n async tick(): Promise<void> {\n await this.#runTick();\n }\n\n // -------- Internals --------\n\n async #runTick(): Promise<void> {\n if (this.#stopping || this.#inflightTick) return;\n const promise = this.#processTick().finally(() => {\n this.#inflightTick = undefined;\n });\n this.#inflightTick = promise;\n await promise;\n }\n\n async #processTick(): Promise<void> {\n let due: Schedule[];\n try {\n due = await this.#schedulesStore.listDueSchedules(Date.now(), this.#config.batchSize);\n } catch (err) {\n this.logger.error('Failed to list due schedules', { error: err });\n return;\n }\n\n for (const schedule of due) {\n if (this.#stopping) break;\n await this.#fireSchedule(schedule);\n }\n }\n\n /**\n * Check whether a schedule's target is registered with the host\n * Mastra instance. Returns `true` if no predicate is configured (we can't\n * verify, so assume the consumer will reject) or if the target resolves.\n *\n * When the target is missing, we increment an in-memory counter and\n * delete the schedule after `missesBeforeDelete` consecutive misses. The\n * grace window protects against deploy/startup ordering races where the\n * scheduler ticks before workflows/agents finish registering on a fresh\n * process. Returns `false` to tell `#fireSchedule` to skip publishing for\n * this tick.\n */\n async #ensureTargetReady(schedule: Schedule): Promise<boolean> {\n const predicate = this.#config.isTargetReady;\n if (!predicate) return true;\n\n if (predicate(schedule.target)) {\n this.#missingWorkflowCounts.delete(schedule.id);\n return true;\n }\n\n const targetSummary =\n schedule.target.type === 'workflow'\n ? { workflowId: schedule.target.workflowId }\n : { agentId: schedule.target.agentId };\n\n const limit = this.#config.missesBeforeDelete ?? DEFAULT_MISSES_BEFORE_DELETE;\n const prev = this.#missingWorkflowCounts.get(schedule.id) ?? 0;\n const next = prev + 1;\n\n if (next < limit) {\n this.#missingWorkflowCounts.set(schedule.id, next);\n if (prev === 0) {\n this.logger.warn('Schedule target is not registered; skipping until it appears', {\n scheduleId: schedule.id,\n targetType: schedule.target.type,\n ...targetSummary,\n missesBeforeDelete: limit,\n });\n }\n return false;\n }\n\n // Hit the grace limit — reclaim the row.\n this.logger.error('Deleting schedule whose target has not been registered', {\n scheduleId: schedule.id,\n targetType: schedule.target.type,\n ...targetSummary,\n consecutiveMisses: next,\n });\n try {\n await this.#schedulesStore.deleteSchedule(schedule.id);\n } catch (err) {\n this.logger.error('Failed to delete ghost schedule', {\n scheduleId: schedule.id,\n targetType: schedule.target.type,\n ...targetSummary,\n error: err,\n });\n // Keep the counter so we try again next tick rather than reset and\n // start the grace window over.\n return false;\n }\n this.#missingWorkflowCounts.delete(schedule.id);\n return false;\n }\n\n async #fireSchedule(schedule: Schedule): Promise<void> {\n if (!(await this.#ensureTargetReady(schedule))) return;\n\n const actualFireAt = Date.now();\n\n let newNextFireAt: number;\n try {\n newNextFireAt = computeNextFireAt(schedule.cron, {\n timezone: schedule.timezone,\n after: actualFireAt,\n });\n } catch (err) {\n this.logger.error('Failed to compute next fire time for schedule', {\n scheduleId: schedule.id,\n cron: schedule.cron,\n error: err,\n });\n this.#notifyError(err, schedule.id);\n return;\n }\n\n // Deterministic runId so concurrent ticks across processes derive the same id.\n const runId = `sched_${schedule.id}_${schedule.nextFireAt}`;\n\n let claimed = false;\n try {\n claimed = await this.#schedulesStore.updateScheduleNextFire(\n schedule.id,\n schedule.nextFireAt,\n newNextFireAt,\n actualFireAt,\n runId,\n );\n } catch (err) {\n this.logger.error('Failed to claim due schedule fire', {\n scheduleId: schedule.id,\n runId,\n error: err,\n });\n this.#notifyError(err, schedule.id);\n return;\n }\n\n if (!claimed) {\n // Another instance won the race, the row was paused/disabled, or the\n // expected nextFireAt no longer matches. Skip publishing.\n return;\n }\n\n let triggerStatus: ScheduleTrigger['outcome'] = 'published';\n let triggerError: string | undefined;\n\n try {\n await this.#publishTargetStart(schedule, runId);\n } catch (err) {\n triggerStatus = 'failed';\n triggerError = err instanceof Error ? err.message : String(err);\n this.logger.error('Failed to publish target.start for schedule', {\n scheduleId: schedule.id,\n runId,\n targetType: schedule.target.type,\n error: err,\n });\n this.#notifyError(err, schedule.id);\n }\n\n // For workflow targets we record the trigger now with the claim id —\n // the workflow event processor will reuse the same runId. For\n // agent targets the AgentScheduleWorker records the trigger itself\n // after the agent run starts, so it can write the real agent runId.\n if (schedule.target.type === 'workflow' || triggerStatus === 'failed') {\n try {\n await this.#schedulesStore.recordTrigger({\n scheduleId: schedule.id,\n runId,\n scheduledFireAt: schedule.nextFireAt,\n actualFireAt,\n outcome: triggerStatus,\n error: triggerError,\n triggerKind: 'schedule-fire',\n });\n } catch (err) {\n this.logger.error('Failed to record schedule trigger', {\n scheduleId: schedule.id,\n runId,\n error: err,\n });\n }\n }\n }\n\n /**\n * Invoke the user-supplied onError hook in isolation. A throwing hook\n * must not abort the scheduler tick loop, so we swallow + log any error\n * the callback itself raises.\n */\n #notifyError(error: unknown, scheduleId: string): void {\n if (!this.#config.onError) return;\n try {\n this.#config.onError(error, { scheduleId });\n } catch (callbackError) {\n this.logger.error('Scheduler onError handler threw', {\n scheduleId,\n error: callbackError,\n });\n }\n }\n\n async #publishTargetStart(schedule: Schedule, claimId: string): Promise<void> {\n switch (schedule.target.type) {\n case 'workflow': {\n const { workflowId, inputData, initialState, requestContext } = schedule.target;\n await this.#pubsub.publish(TOPIC_WORKFLOWS, {\n type: 'workflow.start',\n runId: claimId,\n data: {\n workflowId,\n runId: claimId,\n prevResult: { status: 'success', output: inputData ?? {} },\n requestContext: requestContext ?? {},\n initialState: initialState ?? {},\n },\n });\n return;\n }\n case 'agent': {\n await this.#pubsub.publish(TOPIC_AGENT_SCHEDULES, {\n type: 'agent-schedule.fire',\n runId: claimId,\n data: {\n scheduleId: schedule.id,\n claimId,\n scheduledFireAt: schedule.nextFireAt,\n target: schedule.target,\n },\n });\n return;\n }\n default: {\n throw new Error(`Unsupported schedule target type: ${(schedule.target as { type: string }).type}`);\n }\n }\n }\n}\n\n/**\n * @deprecated Renamed to {@link Scheduler}. The scheduler now drives both\n * workflow and agent schedules, so the `Workflow`-prefixed name is no longer\n * accurate. This alias will be removed in a future major release.\n */\nexport const WorkflowScheduler = Scheduler;\n\n/**\n * @deprecated Renamed to {@link Scheduler}. This alias will be removed in a\n * future major release.\n */\nexport type WorkflowScheduler = Scheduler;\n"],"mappings":"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"}