UNPKG

@backstage/backend-defaults

Version:

Backend defaults used by Backstage backend apps

194 lines (188 loc) 6.36 kB
'use strict'; var api = require('@opentelemetry/api'); var luxon = require('luxon'); var Router = require('express-promise-router'); var LocalTaskWorker = require('./LocalTaskWorker.cjs.js'); var TaskWorker = require('./TaskWorker.cjs.js'); var util = require('./util.cjs.js'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var Router__default = /*#__PURE__*/_interopDefaultCompat(Router); const tracer = api.trace.getTracer(util.TRACER_ID); class PluginTaskSchedulerImpl { localWorkersById = /* @__PURE__ */ new Map(); globalWorkersById = /* @__PURE__ */ new Map(); allScheduledTasks = []; shutdownInitiated; counter; duration; lastStarted; lastCompleted; pluginId; databaseFactory; logger; constructor(pluginId, databaseFactory, logger, rootLifecycle) { this.pluginId = pluginId; this.databaseFactory = databaseFactory; this.logger = logger; const meter = api.metrics.getMeter("default"); this.counter = meter.createCounter("backend_tasks.task.runs.count", { description: "Total number of times a task has been run" }); this.duration = meter.createHistogram("backend_tasks.task.runs.duration", { description: "Histogram of task run durations", unit: "seconds" }); this.lastStarted = meter.createGauge("backend_tasks.task.runs.started", { description: "Epoch timestamp seconds when the task was last started", unit: "seconds" }); this.lastCompleted = meter.createGauge( "backend_tasks.task.runs.completed", { description: "Epoch timestamp seconds when the task was last completed", unit: "seconds" } ); this.shutdownInitiated = new Promise((shutdownInitiated) => { rootLifecycle.addShutdownHook(() => shutdownInitiated(true)); }); } async triggerTask(id) { const localTask = this.localWorkersById.get(id); if (localTask) { localTask.trigger(); return; } const knex = await this.databaseFactory(); await TaskWorker.TaskWorker.trigger(knex, id); } async scheduleTask(task) { util.validateId(task.id); const scope = task.scope ?? "global"; const settings = { version: 2, cadence: parseDuration(task.frequency), initialDelayDuration: task.initialDelay && parseDuration(task.initialDelay), timeoutAfterDuration: parseDuration(task.timeout) }; const abortController = util.delegateAbortController(task.signal); this.shutdownInitiated.then(() => abortController.abort()); if (scope === "global") { const knex = await this.databaseFactory(); const worker = new TaskWorker.TaskWorker( task.id, this.instrumentedFunction(task, scope), knex, this.logger.child({ task: task.id }) ); await worker.start(settings, { signal: abortController.signal }); this.globalWorkersById.set(task.id, worker); } else { const worker = new LocalTaskWorker.LocalTaskWorker( task.id, this.instrumentedFunction(task, scope), this.logger.child({ task: task.id }) ); worker.start(settings, { signal: abortController.signal }); this.localWorkersById.set(task.id, worker); } this.allScheduledTasks.push({ id: task.id, scope, settings }); } createScheduledTaskRunner(schedule) { return { run: async (task) => { await this.scheduleTask({ ...task, ...schedule }); } }; } async getScheduledTasks() { return this.allScheduledTasks; } getRouter() { const router = Router__default.default(); router.get("/.backstage/scheduler/v1/tasks", async (_, res) => { const globalState = await TaskWorker.TaskWorker.taskStates( await this.databaseFactory() ); const tasks = new Array(); for (const task of this.allScheduledTasks) { tasks.push({ taskId: task.id, pluginId: this.pluginId, scope: task.scope, settings: task.settings, taskState: this.localWorkersById.get(task.id)?.taskState() ?? globalState.get(task.id) ?? null, workerState: this.localWorkersById.get(task.id)?.workerState() ?? this.globalWorkersById.get(task.id)?.workerState() ?? null }); } res.json({ tasks }); }); router.post( "/.backstage/scheduler/v1/tasks/:id/trigger", async (req, res) => { const { id } = req.params; await this.triggerTask(id); res.status(200).end(); } ); return router; } instrumentedFunction(task, scope) { return async (abort) => { const labels = { taskId: task.id, scope }; this.counter.add(1, { ...labels, result: "started" }); this.lastStarted.record(Date.now() / 1e3, { taskId: task.id }); const startTime = process.hrtime(); try { await tracer.startActiveSpan(`task ${task.id}`, async (span) => { try { span.setAttributes(labels); await task.fn(abort); } catch (error) { if (error instanceof Error) { span.recordException(error); } throw error; } finally { span.end(); } }); labels.result = "completed"; } catch (ex) { labels.result = "failed"; throw ex; } finally { const delta = process.hrtime(startTime); const endTime = delta[0] + delta[1] / 1e9; this.counter.add(1, labels); this.duration.record(endTime, labels); this.lastCompleted.record(Date.now() / 1e3, labels); } }; } } function parseDuration(frequency) { if (typeof frequency === "object" && "cron" in frequency) { return frequency.cron; } if (typeof frequency === "object" && "trigger" in frequency) { return frequency.trigger; } const parsed = luxon.Duration.isDuration(frequency) ? frequency : luxon.Duration.fromObject(frequency); if (!parsed.isValid) { throw new Error( `Invalid duration, ${parsed.invalidReason}: ${parsed.invalidExplanation}` ); } return parsed.toISO(); } exports.PluginTaskSchedulerImpl = PluginTaskSchedulerImpl; exports.parseDuration = parseDuration; //# sourceMappingURL=PluginTaskSchedulerImpl.cjs.js.map