@backstage/backend-defaults
Version:
Backend defaults used by Backstage backend apps
194 lines (188 loc) • 6.36 kB
JavaScript
'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