UNPKG

alchemymvc

Version:
700 lines (583 loc) 14.3 kB
const running = alchemy.shared('Task.running', 'Array'), HISTORY_DOC = Symbol('history_document'), PAUSE_PLEDGE = Symbol('pause_pledge'), RUNNING_PLEDGE = Symbol('running_pledge'), STATUS = Symbol('status'), STARTING = 0, STARTED = 1, PAUSED = 2, STOPPED = 3; /** * The base "Task" class * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.5.0 */ const Task = Function.inherits('Alchemy.Base', 'Alchemy.Task', function Task() { // When this command started executing this.started_at = null; // When this command stopped executing this.stopped_at = null; // The current status this[STATUS] = STARTING; // Optional pause pledge this[PAUSE_PLEDGE] = null; // The main running pledge this[RUNNING_PLEDGE] = null; // Caught error this.error = null; // Current percentage this.percentage = null; this.progress = 0; // Status reports this.reports = []; // The payload/settings this.payload = null; // The origin AlchemyTask document this.alchemy_task_document = null; // The AlchemyTaskHistory document this[HISTORY_DOC] = null; }); /** * Add a forced cron schedule: * this task will always run at the given time * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {string} cron_schedule * @param {Object} settings */ Task.setStatic(function addForcedCronSchedule(cron_schedule, settings) { cron_schedule = new Classes.Alchemy.Cron(cron_schedule); if (!this.forced_cron_schedules) { this.forced_cron_schedules = []; } this.forced_cron_schedules.push({cron_schedule, settings}); }); /** * Add a fallback cron schedule: * this task will run at the given time if no other schedule is found * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {string} cron_schedule * @param {Object} settings */ Task.setStatic(function addFallbackCronSchedule(cron_schedule, settings) { cron_schedule = new Classes.Alchemy.Cron(cron_schedule); if (!this.fallback_cron_schedules) { this.fallback_cron_schedules = []; } this.fallback_cron_schedules.push({cron_schedule, settings}); }); /** * Each command has a configuration schema * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.5.0 */ Task.constitute(function setSchema() { // Create the schema this.schema = new Classes.Alchemy.Schema(this); }); /** * Return the class-wide schema * * @type {Schema} */ Task.setProperty(function schema() { return this.constructor.schema; }); /** * This is a wrapper class */ Task.makeAbstractClass(); /** * This wrapper class starts a new group */ Task.startNewGroup(); /** * Indicate this command can be paused * * @type {boolean} */ Task.setProperty('can_be_paused', true); /** * Indicate this command can be stopped * * @type {Schema} */ Task.setProperty('can_be_stopped', true); /** * Static description, * only set when command block should never use * `_getDescription` * * @type {boolean} */ Task.setProperty('static_description', ''); /** * Always execute `_getDescription`, even when * there are no settings * * @type {boolean} */ Task.setProperty('force_description_callback', false); /** * Has this task started? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @type {boolean} */ Task.setProperty(function has_started() { return this[STATUS] > STARTING; }); /** * Has this task been paused? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @type {boolean} */ Task.setProperty(function is_paused() { return this[STATUS] == PAUSED; }); /** * Has this task stopped? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @type {boolean} */ Task.setProperty(function has_stopped() { return this[STATUS] == STOPPED; }); /** * Return the basic record for JSON * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 1.0.0 */ Task.setMethod(function toJSON() { return { id : this.id, name : this.name || this.constructor.type_name, title : this.constructor.title, started : this.started, stopped : this.stopped, paused : this.paused, error : this.error, percentage : this.percentage, reports : this.reports, manual_stop_start : this.manual_stop_start, manual_stop_end : this.manual_stop_end, need_stop : this.need_stop, description : this.description }; }); /** * Callback with a nice description * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 1.3.17 * * @return {Promise<string>} */ Task.setMethod(async function getDescription() { // If there is a static description, that should be returned if (this.static_description && !this.force_description_callback) { return this.static_description; } let description = await this._getDescription(); this.description = description; return description; }); /** * Callback with a nice description, * should be modified upon extension * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.3.0 * * @return {Promise<string>} */ Task.setMethod(async function _getDescription() { return this.constructor.title || this.name; }); /** * The main function to execute. Should not be called directly. * Needs to be overridden by child classes. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.5.0 * @version 1.3.17 */ Task.setMethod(async function executor() { throw new Error('Task ' + this.constructor.title + ' has no executor function!'); }); /** * Set the payload/settings * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Object} payload User provided data */ Task.setMethod(function setPayload(payload) { this.payload = payload; }); /** * Set the original AlchemyTask document * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Document.AlchemyTask} doc */ Task.setMethod(function setAlchemyTaskDocument(doc) { this.alchemy_task_document = doc; }); /** * Set the AlchemyTaskHistory document * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Document.AlchemyTaskHistory} doc */ Task.setMethod(function setAlchemyTaskHistoryDocument(doc) { this[HISTORY_DOC] = doc; }); /** * Start executing the command * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 1.3.17 * * @param {Object} payload User provided data */ Task.setMethod(async function start(payload) { if (this.has_started) { throw new Error('Task ' + this.constructor.title + ' has already started!'); } if (payload) { this.setPayload(payload); } const History = Model.get('System.TaskHistory'); this[STATUS] = STARTED; this[RUNNING_PLEDGE] = new Pledge(); let document = this[HISTORY_DOC]; if (!document) { document = History.createDocument(); document.type = this.constructor.type_path || this.constructor.type_name; document.alchemy_task_id = this.alchemy_task_document?.$pk; document.process_id = process.pid; this[HISTORY_DOC] = document; } let started_at = new Date(); document.settings = this.payload; document.started_at = started_at; document.is_running = true; // Register this command as running running.push(this); await document.save(); // Set the id this.id = String(document.$pk); // Set the start time this.started = started_at.getTime(); let result; try { result = await this.executor(); } catch (err) { if (err == 'stopped') { this.report('stopped', 'Stopped'); return; } // Set the error this.error = err; // Report failed let report = this.report('failed'); report.error = err; throw err; } // If the command hasn't been manually stopped, // report it as done if (!this.manual_stop_end) { this.report('done'); } document.ended_at = new Date(); document.is_running = false; await document.save(); this[RUNNING_PLEDGE].resolve(result); return result; }); /** * Stop the running command * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 1.3.17 */ Task.setMethod(async function stop() { // Set time when manual stop was requested this.manual_stop_start = Date.now(); this.need_stop = true; if (typeof this.doStop == 'function') { let result = await this.doStop(); this.manual_stop_end = Date.now(); return result; } }); /** * Pause the running command * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 1.3.17 */ Task.setMethod(function pause() { // Do nothing if already paused if (this.is_paused) { return; } // If this has stopped, throw an error if (this.has_stopped) { throw new Error('Unable to pause a task that has already stopped'); } this[PAUSE_PLEDGE] = new Pledge(); this[STATUS] = PAUSED; if (typeof this.doPause == 'function') { return this.doPause(); } }); /** * Resume this task if it has been paused * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 1.3.17 */ Task.setMethod(function resume() { if (!this.is_paused) { return; } if (this.has_stopped) { throw new Error('Unable to resume a task that has already stopped'); } if (typeof this.doResume == 'function') { this.doResume(); } // If there is a pause pledge, resolve it if (this[PAUSE_PLEDGE]) { this[PAUSE_PLEDGE].resolve(); this[PAUSE_PLEDGE] = null; } // Set the status back to `started` this[STATUS] = STARTED; }); /** * Get a parameter from the payload by the given name * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ Task.setMethod(function getParam(name) { return this.payload?.[name]; }); /** * Wait for the pause to resolve * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.21 */ Task.setMethod(function waitUntilResumed() { if (!this.is_paused || this.has_stopped) { return false; } let paused = this[PAUSE_PLEDGE]; if (paused) { return paused; } return this.waitIfTooBusy(); }); /** * Wait if the system is too busy * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.21 * @version 1.3.21 */ Task.setMethod(function waitIfTooBusy() { if (!alchemy.isTooBusy()) { return false; } return doAsyncLoopUntilNotBusy(10); }); /** * Do a loop until the system is no longer busy * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.21 * @version 1.3.21 */ async function doAsyncLoopUntilNotBusy(max_tries) { let tries = 0; if (!max_tries || max_tries < 1) { max_tries = 5; } do { console.log('Waiting for system to be less busy', tries) await Pledge.after(500); tries++; } while (tries < max_tries && alchemy.isTooBusy()); return true; } /** * Report command progress * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 1.3.17 * * @param {number} percentage Percentage that is done as a decimal (between 0 & 1) * @param {string} type The type of status * * @return {Object} */ Task.setMethod(function report(percentage, type) { var modified = false, report, i; if (!type && percentage == 'stopped') { type = 'Stopped'; } if (percentage == 'paused') { this.paused = true; } else { this.paused = false; } if (typeof percentage == 'number') { percentage = Math.round(percentage * 10000) / 100; } report = this.reports.last(); if (type == null) { // If no report was found, and no percentage given, // use zero as percentage if (!report && percentage == null) { percentage = 0; } } else { if (report && report.type != type) { report = null; } } if (report == null) { report = { start : Date.now(), type : type, logs : [] }; this.reports.push(report); } report.update = Date.now(); if (percentage == 'done' || percentage == 100) { report.percentage = 100; report.done = true; } else if (percentage == 'failed') { report.done = true; report.failed = true; } else if (percentage == 'stopped') { report.done = true; this.manual_stop_end = Date.now(); } else if (percentage != null && typeof percentage == 'number') { report.percentage = percentage; } if (report.done && this.stopped == null) { this.stopped = Date.now(); } if (report.percentage != null) { this.percentage = report.percentage; this.reportProgress(report.percentage, type); } this.emit('report', report); alchemy.updateData(this.id, this); return report; }); /** * Report progress, Janeway monkey-patches this method * and uses it to display the progress bar. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.5.0 * @version 0.5.0 * * @param {number} value A value between 0-100 * @param {string} label An optional label */ Task.setMethod(function reportProgress(value, label) { this.progress = value; }); /** * Log command messages * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.3.0 */ Task.setMethod(function log() { var report = this.report(), args = [], i; for (i = 0; i < arguments.length; i++) { args[i] = arguments[i]; } report.logs.push({ time : Date.now(), args : args }); }); /** * Start executing a task * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.3.0 * @version 0.5.0 * * @return {Task} */ Task.execute = function execute(name, options, callback) { var constructor = Task.getMember(name), task; if (typeof options == 'function') { callback = options; options = {}; } if (!constructor) { return callback(new Error('Could not find "' + name + '" task')); } task = new constructor(); task.start(options, callback); return task; };