UNPKG

@mastra/core

Version:

Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.

272 lines (269 loc) • 8.56 kB
'use strict'; var chunkBKPEQNQF_cjs = require('./chunk-BKPEQNQF.cjs'); var chunkFCQNDFEW_cjs = require('./chunk-FCQNDFEW.cjs'); var chunk7GW2TQXP_cjs = require('./chunk-7GW2TQXP.cjs'); // src/workflows/scheduler/scheduler.ts var TOPIC_WORKFLOWS = "workflows"; var DEFAULT_TICK_INTERVAL_MS = 1e4; var DEFAULT_BATCH_SIZE = 100; var DEFAULT_MISSES_BEFORE_DELETE = 3; var WorkflowScheduler = class extends chunkFCQNDFEW_cjs.MastraBase { #schedulesStore; #pubsub; #config; #intervalHandle; #inflightTick; #started = false; #stopping = false; /** * Per-schedule count of consecutive ticks where the target workflow was * not registered with the host Mastra instance. Reset when the workflow * resolves or the schedule is deleted. Used to ride out deploy/startup * ordering races before reclaiming a ghost row. */ #missingWorkflowCounts = /* @__PURE__ */ new Map(); constructor({ schedulesStore, pubsub, config }) { super({ component: chunk7GW2TQXP_cjs.RegisteredLogger.WORKFLOW, name: "WorkflowScheduler" }); this.#schedulesStore = schedulesStore; this.#pubsub = pubsub; this.#config = { ...config, tickIntervalMs: config?.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS, batchSize: config?.batchSize ?? DEFAULT_BATCH_SIZE }; } /** Start the periodic tick loop. Runs an immediate tick first. */ async start() { if (this.#started) return; this.#started = true; this.#stopping = false; this.#missingWorkflowCounts.clear(); try { await this.#runTick(); if (this.#stopping || !this.#started) return; this.#intervalHandle = setInterval(() => { void this.#runTick().catch((err) => { this.logger.error("WorkflowScheduler tick crashed", { error: err }); }); }, this.#config.tickIntervalMs); } catch (err) { this.#started = false; this.#stopping = false; throw err; } } /** Stop the tick loop and wait for any in-flight tick to finish. */ async stop() { if (!this.#started) return; this.#stopping = true; if (this.#intervalHandle) { clearInterval(this.#intervalHandle); this.#intervalHandle = void 0; } if (this.#inflightTick) { try { await this.#inflightTick; } catch { } } this.#started = false; this.#stopping = false; } /** True when the scheduler is currently running its tick loop. */ get isRunning() { return this.#started; } /** * Run a single tick. Public for tests; production callers should rely * on the interval started by `start()`. */ async tick() { await this.#runTick(); } // -------- Internals -------- async #runTick() { if (this.#stopping || this.#inflightTick) return; const promise = this.#processTick().finally(() => { this.#inflightTick = void 0; }); this.#inflightTick = promise; await promise; } async #processTick() { let due; try { due = await this.#schedulesStore.listDueSchedules(Date.now(), this.#config.batchSize); } catch (err) { this.logger.error("Failed to list due schedules", { error: err }); return; } for (const schedule of due) { if (this.#stopping) break; await this.#fireSchedule(schedule); } } /** * Check whether a schedule's target workflow is registered with the host * Mastra instance. Returns `true` if no predicate is configured (we can't * verify, so assume the consumer will reject) or if the workflow resolves. * * When the workflow is missing, we increment an in-memory counter and * delete the schedule after `missesBeforeDelete` consecutive misses. The * grace window protects against deploy/startup ordering races where the * scheduler ticks before workflows finish registering on a fresh process. * Returns `false` to tell `#fireSchedule` to skip publishing for this tick. */ async #ensureWorkflowExists(schedule) { const predicate = this.#config.isWorkflowRegistered; if (!predicate) return true; if (schedule.target.type !== "workflow") return true; const workflowId = schedule.target.workflowId; if (predicate(workflowId)) { this.#missingWorkflowCounts.delete(schedule.id); return true; } const limit = this.#config.missesBeforeDelete ?? DEFAULT_MISSES_BEFORE_DELETE; const prev = this.#missingWorkflowCounts.get(schedule.id) ?? 0; const next = prev + 1; if (next < limit) { this.#missingWorkflowCounts.set(schedule.id, next); if (prev === 0) { this.logger.warn("Schedule target workflow is not registered; skipping until it appears", { scheduleId: schedule.id, workflowId, missesBeforeDelete: limit }); } return false; } this.logger.error("Deleting schedule whose target workflow has not been registered", { scheduleId: schedule.id, workflowId, consecutiveMisses: next }); try { await this.#schedulesStore.deleteSchedule(schedule.id); } catch (err) { this.logger.error("Failed to delete ghost schedule", { scheduleId: schedule.id, workflowId, error: err }); return false; } this.#missingWorkflowCounts.delete(schedule.id); return false; } async #fireSchedule(schedule) { if (!await this.#ensureWorkflowExists(schedule)) return; const actualFireAt = Date.now(); let newNextFireAt; try { newNextFireAt = chunkBKPEQNQF_cjs.computeNextFireAt(schedule.cron, { timezone: schedule.timezone, after: actualFireAt }); } catch (err) { this.logger.error("Failed to compute next fire time for schedule", { scheduleId: schedule.id, cron: schedule.cron, error: err }); this.#notifyError(err, schedule.id); return; } const runId = `sched_${schedule.id}_${schedule.nextFireAt}`; let claimed = false; try { claimed = await this.#schedulesStore.updateScheduleNextFire( schedule.id, schedule.nextFireAt, newNextFireAt, actualFireAt, runId ); } catch (err) { this.logger.error("Failed to claim due schedule fire", { scheduleId: schedule.id, runId, error: err }); this.#notifyError(err, schedule.id); return; } if (!claimed) { return; } let triggerStatus = "published"; let triggerError; try { await this.#publishWorkflowStart(schedule, runId); } catch (err) { triggerStatus = "failed"; triggerError = err instanceof Error ? err.message : String(err); this.logger.error("Failed to publish workflow.start for schedule", { scheduleId: schedule.id, runId, error: err }); this.#notifyError(err, schedule.id); } try { await this.#schedulesStore.recordTrigger({ scheduleId: schedule.id, runId, scheduledFireAt: schedule.nextFireAt, actualFireAt, outcome: triggerStatus, error: triggerError, triggerKind: "schedule-fire" }); } catch (err) { this.logger.error("Failed to record schedule trigger", { scheduleId: schedule.id, runId, error: err }); } } /** * Invoke the user-supplied onError hook in isolation. A throwing hook * must not abort the scheduler tick loop, so we swallow + log any error * the callback itself raises. */ #notifyError(error, scheduleId) { if (!this.#config.onError) return; try { this.#config.onError(error, { scheduleId }); } catch (callbackError) { this.logger.error("WorkflowScheduler onError handler threw", { scheduleId, error: callbackError }); } } async #publishWorkflowStart(schedule, runId) { if (schedule.target.type !== "workflow") { throw new Error(`Unsupported schedule target type: ${schedule.target.type}`); } const { workflowId, inputData, initialState, requestContext } = schedule.target; await this.#pubsub.publish(TOPIC_WORKFLOWS, { type: "workflow.start", runId, data: { workflowId, runId, prevResult: { status: "success", output: inputData ?? {} }, requestContext: requestContext ?? {}, initialState: initialState ?? {} } }); } }; exports.WorkflowScheduler = WorkflowScheduler; //# sourceMappingURL=chunk-B3HPYVQP.cjs.map //# sourceMappingURL=chunk-B3HPYVQP.cjs.map