UNPKG

@nocobase/plugin-workflow

Version:

A powerful BPM tool that provides foundational support for business automation, with the capability to extend unlimited triggers and nodes.

321 lines (319 loc) • 10.4 kB
/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var ExecutionTimeoutManager_exports = {}; __export(ExecutionTimeoutManager_exports, { default: () => ExecutionTimeoutManager }); module.exports = __toCommonJS(ExecutionTimeoutManager_exports); var import_database = require("@nocobase/database"); var import_constants = require("./constants"); var import_utils = require("./utils"); const LOAD_BATCH_SIZE = 1e3; const DEFAULT_SCAN_INTERVAL = 3e4; const SCAN_JITTER = 5e3; const MAX_TIMER_DELAY = 2147483647; const EXECUTION_LOCK_TIMEOUT = 6e4; class ExecutionTimeoutManager { constructor(plugin) { this.plugin = plugin; } timers = /* @__PURE__ */ new Map(); scanTimer = null; nextExpiresAtTimer = null; nextExpiresAt = null; scanning = null; stopped = true; getTimeout(execution) { var _a, _b; const timeout = Number(((_b = (_a = execution.workflow) == null ? void 0 : _a.options) == null ? void 0 : _b.timeout) ?? 0); return Number.isFinite(timeout) && timeout > 0 ? timeout : 0; } getExpiresAt(execution, startedAt) { const timeout = this.getTimeout(execution); return timeout > 0 ? new Date(startedAt.getTime() + timeout) : null; } async load() { this.stopped = false; await this.scanExpiredExecutions(); await this.scheduleNextExpiresAtTimer(); this.scheduleScan(); } async unload() { var _a; this.stopped = true; if (this.scanTimer) { clearTimeout(this.scanTimer); this.scanTimer = null; } if (this.nextExpiresAtTimer) { clearTimeout(this.nextExpiresAtTimer); this.nextExpiresAtTimer = null; this.nextExpiresAt = null; } for (const timer of this.timers.values()) { clearTimeout(timer); } this.timers.clear(); await ((_a = this.scanning) == null ? void 0 : _a.catch(() => { })); } isExpired(execution, now = new Date(Date.now())) { return !!execution.expiresAt && execution.expiresAt.getTime() <= now.getTime(); } getRemainingMs(execution, now = new Date(Date.now())) { if (!execution.expiresAt) { return null; } return execution.expiresAt.getTime() - now.getTime(); } async abort(execution, options = {}) { const aborted = await (0, import_utils.abortExecution)(this.plugin, execution, { ...options, reason: import_constants.EXECUTION_REASON.TIMEOUT }); if (aborted) { this.clearExecutionTimeout(execution.id); } return aborted; } async abortExecutionIfExpired(execution, options = {}) { if (execution.status !== import_constants.EXECUTION_STATUS.STARTED || !this.isExpired(execution)) { return false; } return this.abort(execution, options); } async abortExecutionIfExpiredWithLock(executionOrId) { const executionId = typeof executionOrId === "object" ? executionOrId.id : executionOrId; const lock = await this.plugin.app.lockManager.tryAcquire((0, import_utils.getExecutionLockKey)(executionId), EXECUTION_LOCK_TIMEOUT); return lock.runExclusive(async () => { const execution = await this.plugin.db.getRepository("executions").findOne({ filterByTk: executionId }); if (!execution) { return false; } return this.abortExecutionIfExpired(execution); }, EXECUTION_LOCK_TIMEOUT); } clear(executionId) { this.clearExecutionTimeout(executionId); } async shouldContinue(execution, options = {}) { if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) { return false; } if (!this.isExpired(execution)) { return true; } await this.abortExecutionIfExpired(execution, options); return false; } /** * Owner-only per-execution timer. Only call from code paths that have acquired * local execution ownership, such as Dispatcher/Processor processing paths. */ scheduleExecutionTimeout(execution) { const id = String(execution.id); this.clearExecutionTimeout(id); if (execution.status !== import_constants.EXECUTION_STATUS.STARTED || !execution.expiresAt) { return; } this.scheduleNextExpiresAtIfEarlier(execution.expiresAt); const remaining = this.getRemainingMs(execution); if (remaining == null) { return; } if (remaining <= 0) { setImmediate(() => { this.handleExecutionTimeout(id).catch((error) => { this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) timeout handling failed`, { error }); }); }); return; } this.timers.set( id, setTimeout( () => { if (execution.expiresAt && execution.expiresAt.getTime() > Date.now()) { this.scheduleExecutionTimeout(execution); return; } this.handleExecutionTimeout(id).catch((error) => { this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) timeout handling failed`, { error }); }); }, Math.min(remaining, MAX_TIMER_DELAY) ) ); } invalidateNextExpiresAtIfMatches(expiresAt) { if (!expiresAt || !this.nextExpiresAt || this.nextExpiresAt.getTime() !== expiresAt.getTime()) { return; } this.clearNextExpiresAtTimer(); this.scheduleNextExpiresAtTimer().catch((error) => { this.plugin.getLogger("timeout").error(`workflow execution timeout one-shot refresh failed`, { error }); }); } async scanExpiredExecutions() { if (this.scanning) { return this.scanning; } if (this.stopped) { return; } this.scanning = (async () => { const ExecutionModelClass = this.plugin.db.getModel("executions"); const executions = await ExecutionModelClass.findAll({ attributes: ["id", "workflowId", "status", "expiresAt"], where: { status: import_constants.EXECUTION_STATUS.STARTED, expiresAt: { [import_database.Op.not]: null, [import_database.Op.lt]: /* @__PURE__ */ new Date() } }, order: [ ["expiresAt", "ASC"], ["id", "ASC"] ], limit: LOAD_BATCH_SIZE }); for (const execution of executions) { await this.abortExecutionIfExpiredWithLock(execution); } })(); try { await this.scanning; } finally { this.scanning = null; } } scheduleScan() { if (this.stopped) { return; } if (this.scanTimer) { clearTimeout(this.scanTimer); } this.scanTimer = setTimeout( async () => { this.scanTimer = null; if (this.stopped) { return; } try { await this.scanExpiredExecutions(); this.scheduleNextExpiresAtTimer(); } catch (error) { this.plugin.getLogger("timeout").error(`workflow execution timeout scan failed`, { error }); } finally { this.scheduleScan(); } }, DEFAULT_SCAN_INTERVAL + Math.floor(Math.random() * SCAN_JITTER) ); } async scheduleNextExpiresAtTimer() { if (this.stopped) { return; } const ExecutionModelClass = this.plugin.db.getModel("executions"); const execution = await ExecutionModelClass.findOne({ attributes: ["id", "expiresAt"], where: { status: import_constants.EXECUTION_STATUS.STARTED, expiresAt: { [import_database.Op.not]: null } }, order: [ ["expiresAt", "ASC"], ["id", "ASC"] ] }); if (!(execution == null ? void 0 : execution.expiresAt)) { this.clearNextExpiresAtTimer(); return; } this.scheduleNextExpiresAtIfEarlier(execution.expiresAt, true); } scheduleNextExpiresAtIfEarlier(expiresAt, force = false) { if (this.stopped) { return; } if (!force && this.nextExpiresAt && this.nextExpiresAt.getTime() <= expiresAt.getTime()) { return; } this.clearNextExpiresAtTimer(); this.nextExpiresAt = expiresAt; const remaining = expiresAt.getTime() - Date.now(); const delay = Math.max(0, Math.min(remaining, MAX_TIMER_DELAY)); this.nextExpiresAtTimer = setTimeout(() => { this.handleNextExpiresAtTimeout(expiresAt).catch((error) => { this.plugin.getLogger("timeout").error(`workflow execution timeout one-shot failed`, { error }); }); }, delay); } async handleNextExpiresAtTimeout(expiresAt) { this.nextExpiresAtTimer = null; this.nextExpiresAt = null; if (expiresAt.getTime() > Date.now()) { this.scheduleNextExpiresAtIfEarlier(expiresAt, true); return; } await this.scanExpiredExecutions(); await this.scheduleNextExpiresAtTimer(); } clearNextExpiresAtTimer() { if (this.nextExpiresAtTimer) { clearTimeout(this.nextExpiresAtTimer); this.nextExpiresAtTimer = null; } this.nextExpiresAt = null; } clearExecutionTimeout(executionId) { const id = String(executionId); const timer = this.timers.get(id); if (!timer) { return; } clearTimeout(timer); this.timers.delete(id); } async handleExecutionTimeout(executionId) { this.clearExecutionTimeout(executionId); if (this.plugin.abortRunningExecution(executionId, import_constants.EXECUTION_REASON.TIMEOUT)) { return; } await this.abortExecutionIfExpiredWithLock(executionId); } }