UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

192 lines (191 loc) 8.2 kB
import { JobValidationException } from "../exceptions/job.exceptions.js"; import { errorSummary } from "../utils/error.utils.js"; import { AwilixResolutionError } from "awilix"; import { AsyncTask, SimpleIntervalJob } from "toad-scheduler"; //#region src/services/task-manager.service.ts var TaskManagerService = class TaskManagerService { taskStates = {}; logger; constructor(loggerFactory, cradleService, toadScheduler) { this.cradleService = cradleService; this.toadScheduler = toadScheduler; this.logger = loggerFactory(TaskManagerService.name); } /** * Create a recurring job or one-time task * @param registration Task registration parameters */ registerJobOrTask(registration) { const { id: taskId, task: serviceIdentifier, preset: schedulerOptions } = registration; try { this.validateInput(taskId, serviceIdentifier, schedulerOptions); } catch (e) { this.logger.error(errorSummary(e), schedulerOptions); return; } const timedTask = this.getSafeTimedTask(taskId, serviceIdentifier); this.taskStates[taskId] = { options: schedulerOptions, timedTask }; if (schedulerOptions.runOnce) timedTask.execute(); else if (schedulerOptions.runDelayed) { const delay = (schedulerOptions.milliseconds ?? 0) + (schedulerOptions.seconds ?? 0) * 1e3; this.runTimeoutTaskInstance(taskId, delay); } else this.scheduleEnabledPeriodicJob(taskId); } /** * Enable the job which must be disabled at boot. Handy for conditional, heavy or long-running non-critical tasks * @param taskId Task identifier * @param failIfEnabled throws when the job is already running */ scheduleDisabledJob(taskId, failIfEnabled = true) { const taskState = this.getTaskState(taskId); if ((taskState?.options)?.disabled !== true) { if (failIfEnabled) throw new JobValidationException(`The requested task with ID ${taskId} was not explicitly disabled and must be running already.`, taskId); return; } taskState.options.disabled = false; this.scheduleEnabledPeriodicJob(taskId); } /** * Disable a running job * @param taskId Task identifier * @param failIfDisabled throws when the job is already disabled */ disableJob(taskId, failIfDisabled = true) { if (this.isTaskDisabled(taskId)) { if (failIfDisabled) throw new JobValidationException("Can't disable a job which is already disabled", taskId); return; } const taskState = this.getTaskState(taskId); taskState.options.disabled = true; taskState.job?.stop(); } /** * Check if a task is currently disabled * @param taskId Task identifier * @returns true if task is disabled */ isTaskDisabled(taskId) { return !!this.getTaskState(taskId).options.disabled; } /** * Remove a task from the scheduler and internal registry * @param taskId Task identifier */ deregisterTask(taskId) { this.getTaskState(taskId); delete this.taskStates[taskId]; this.toadScheduler.removeById(taskId); } /** * Get the internal state of a task * @param taskId Task identifier * @returns Task state */ getTaskState(taskId) { const taskState = this.taskStates[taskId]; if (!taskState) throw new JobValidationException(`The requested task with ID ${taskId} was not registered`, taskId); return taskState; } /** * Execute a task after a delay * @param taskId Task identifier * @param timeoutMs Delay in milliseconds */ runTimeoutTaskInstance(taskId, timeoutMs) { const taskState = this.getTaskState(taskId); this.logger.log(`Running delayed task '${taskId}' in ${timeoutMs}ms`); setTimeout(() => taskState.timedTask.execute(), timeoutMs); } /** * Stop all scheduled tasks */ stopSchedulerTasks() { this.toadScheduler.stop(); } /** * Validates task registration inputs */ validateInput(taskId, serviceIdentifier, schedulerOptions) { if (!taskId) throw new JobValidationException("Task ID was not provided. Can't register task or schedule job.", taskId); const serviceName = serviceIdentifier || "unknown"; const prefix = `Job '${schedulerOptions?.name ?? serviceName}' with ID '${taskId}'`; if (this.taskStates[taskId]) throw new JobValidationException(`${prefix} was already registered. Can't register a key twice.`, taskId); let resolvedService; try { resolvedService = this.cradleService.resolve(serviceIdentifier); } catch (e) { if (e instanceof AwilixResolutionError) throw new JobValidationException(`${prefix} had an awilix dependency resolution error. It can't be scheduled without fixing this problem. Inner error:\n` + e.stack, taskId); else throw new JobValidationException(`${prefix} is not a registered awilix dependency. It can't be scheduled. Error:\n` + e.stack, taskId); } if (typeof resolvedService?.run !== "function") throw new JobValidationException(`${prefix} was resolved but it doesn't have a 'run()' method to call.`, taskId); if (!schedulerOptions?.periodic && !schedulerOptions?.runOnce && !schedulerOptions?.runDelayed) throw new JobValidationException(`${prefix} Provide 'periodic', 'runOnce', or 'runDelayed' option.`, taskId); if (!schedulerOptions?.periodic && !!schedulerOptions.disabled) throw new JobValidationException(`${prefix} Only tasks of type 'periodic' can be disabled at boot.`, taskId); if (schedulerOptions?.runDelayed && !schedulerOptions.milliseconds && !schedulerOptions.seconds) throw new JobValidationException(`${prefix} Provide a delayed timing parameter (milliseconds|seconds)`, taskId); if (schedulerOptions?.periodic && !schedulerOptions.milliseconds && !schedulerOptions.seconds && !schedulerOptions.minutes && !schedulerOptions.hours && !schedulerOptions.days) throw new JobValidationException(`${prefix} Provide a periodic timing parameter (milliseconds|seconds|minutes|hours|days)`, taskId); } /** * Create a safe timed task with error handling * @param taskId Task identifier * @param serviceIdentifier Service to resolve and execute * @returns AsyncTask instance */ getSafeTimedTask(taskId, serviceIdentifier) { const asyncHandler = async () => { await this.timeTask(taskId, serviceIdentifier); }; return new AsyncTask(taskId, asyncHandler, this.getErrorHandler(taskId)); } /** * Execute a task and measure its execution time * @param taskId Task identifier * @param serviceIdentifier Service to resolve and execute */ async timeTask(taskId, serviceIdentifier) { const taskState = this.taskStates[taskId]; taskState.started = Date.now(); await this.cradleService.resolve(serviceIdentifier).run(); taskState.duration = Date.now() - taskState.started; if (taskState.options?.logFirstCompletion !== false && !taskState?.firstCompletion) { this.logger.log(`Task '${taskId}' first completion. Duration ${taskState.duration}ms`); taskState.firstCompletion = Date.now(); } } /** * Create an error handler for a task * @param taskId Task identifier * @returns Error handler function */ getErrorHandler(taskId) { return (error) => { const taskState = this.taskStates[taskId]; taskState.lastError ??= { time: Date.now(), error }; this.logger.error(`Task '${taskId}' threw an exception: ${error.stack}`); }; } /** * Schedule a periodic job that's not disabled * @param taskId Task identifier */ scheduleEnabledPeriodicJob(taskId) { const taskState = this.getTaskState(taskId); if (!taskState?.timedTask || !taskState?.options) throw new JobValidationException(`The requested task with ID ${taskId} was not registered properly ('timedTask' or 'options' missing).`, taskId); const schedulerOptions = taskState.options; const timedTask = taskState.timedTask; if (!schedulerOptions?.periodic) throw new JobValidationException(`The requested task with ID ${taskId} is not periodic and cannot be enabled.`, taskId); if (!schedulerOptions.disabled) { this.logger.log(`Task '${taskId}' was scheduled (runImmediately: ${!!schedulerOptions.runImmediately}).`); const job = new SimpleIntervalJob(schedulerOptions, timedTask); taskState.job = job; this.toadScheduler.addSimpleIntervalJob(job); } else this.logger.log(`Task '${taskId}' was marked as disabled (deferred execution).`); } }; //#endregion export { TaskManagerService }; //# sourceMappingURL=task-manager.service.js.map