@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
JavaScript
/**
* 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);
}
}