@nocobase/plugin-workflow 2.1.0-beta.37 → 2.1.0-beta.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/618.19af7f84261c815d.js +10 -0
- package/dist/client/964.ffbf5b47ed12bbdc.js +10 -0
- package/dist/client/components/TimeoutInput.d.ts +11 -0
- package/dist/client/constants.d.ts +13 -0
- package/dist/client/index.js +1 -1
- package/dist/client/schemas/executions.d.ts +42 -0
- package/dist/common/collections/executions.d.ts +42 -0
- package/dist/common/collections/executions.js +50 -1
- package/dist/common/collections/workflows.d.ts +6 -2
- package/dist/common/collections/workflows.js +3 -1
- package/dist/common/constants.d.ts +5 -0
- package/dist/common/constants.js +7 -0
- package/dist/externalVersion.js +12 -12
- package/dist/locale/de-DE.json +4 -0
- package/dist/locale/en-US.json +7 -0
- package/dist/locale/es-ES.json +4 -0
- package/dist/locale/fr-FR.json +4 -0
- package/dist/locale/hu-HU.json +7 -3
- package/dist/locale/id-ID.json +4 -0
- package/dist/locale/it-IT.json +4 -0
- package/dist/locale/ja-JP.json +5 -1
- package/dist/locale/ko-KR.json +4 -0
- package/dist/locale/nl-NL.json +7 -3
- package/dist/locale/pt-BR.json +4 -0
- package/dist/locale/ru-RU.json +4 -0
- package/dist/locale/tr-TR.json +4 -0
- package/dist/locale/uk-UA.json +7 -3
- package/dist/locale/vi-VN.json +7 -3
- package/dist/locale/zh-CN.json +8 -0
- package/dist/locale/zh-TW.json +7 -3
- package/dist/node_modules/cron-parser/package.json +1 -1
- package/dist/node_modules/joi/package.json +1 -1
- package/dist/node_modules/lru-cache/package.json +1 -1
- package/dist/node_modules/nodejs-snowflake/package.json +1 -1
- package/dist/server/Dispatcher.d.ts +9 -7
- package/dist/server/Dispatcher.js +190 -122
- package/dist/server/ExecutionTimeoutManager.d.ts +45 -0
- package/dist/server/ExecutionTimeoutManager.js +312 -0
- package/dist/server/Plugin.d.ts +12 -0
- package/dist/server/Plugin.js +21 -1
- package/dist/server/Processor.d.ts +65 -9
- package/dist/server/Processor.js +285 -33
- package/dist/server/RunningExecutionRegistry.d.ts +18 -0
- package/dist/server/RunningExecutionRegistry.js +48 -0
- package/dist/server/actions/executions.d.ts +4 -3
- package/dist/server/actions/executions.js +42 -21
- package/dist/server/actions/jobs.d.ts +2 -1
- package/dist/server/actions/jobs.js +28 -1
- package/dist/server/constants.d.ts +2 -0
- package/dist/server/constants.js +3 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/server/instructions/index.d.ts +10 -3
- package/dist/server/migrations/20260501120000-workflow-timeout.d.ts +13 -0
- package/dist/server/migrations/20260501120000-workflow-timeout.js +63 -0
- package/dist/server/timeout-errors.d.ts +13 -0
- package/dist/server/timeout-errors.js +47 -0
- package/dist/server/types/Execution.d.ts +6 -0
- package/dist/server/types/Job.d.ts +3 -3
- package/dist/server/types/Workflow.d.ts +6 -1
- package/dist/server/utils.d.ts +11 -1
- package/dist/server/utils.js +92 -5
- package/dist/swagger/index.d.ts +22 -0
- package/dist/swagger/index.js +22 -0
- package/package.json +2 -2
- package/dist/client/261.7722d7400942730e.js +0 -10
- package/dist/client/964.6251d37b35710747.js +0 -10
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var ExecutionTimeoutManager_exports = {};
|
|
28
|
+
__export(ExecutionTimeoutManager_exports, {
|
|
29
|
+
default: () => ExecutionTimeoutManager
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(ExecutionTimeoutManager_exports);
|
|
32
|
+
var import_database = require("@nocobase/database");
|
|
33
|
+
var import_constants = require("./constants");
|
|
34
|
+
var import_utils = require("./utils");
|
|
35
|
+
const LOAD_BATCH_SIZE = 1e3;
|
|
36
|
+
const DEFAULT_SCAN_INTERVAL = 3e4;
|
|
37
|
+
const SCAN_JITTER = 5e3;
|
|
38
|
+
const MAX_TIMER_DELAY = 2147483647;
|
|
39
|
+
class ExecutionTimeoutManager {
|
|
40
|
+
constructor(plugin) {
|
|
41
|
+
this.plugin = plugin;
|
|
42
|
+
}
|
|
43
|
+
timers = /* @__PURE__ */ new Map();
|
|
44
|
+
scanTimer = null;
|
|
45
|
+
nextExpiresAtTimer = null;
|
|
46
|
+
nextExpiresAt = null;
|
|
47
|
+
scanning = null;
|
|
48
|
+
stopped = true;
|
|
49
|
+
getTimeout(execution) {
|
|
50
|
+
var _a, _b;
|
|
51
|
+
const timeout = Number(((_b = (_a = execution.workflow) == null ? void 0 : _a.options) == null ? void 0 : _b.timeout) ?? 0);
|
|
52
|
+
return Number.isFinite(timeout) && timeout > 0 ? timeout : 0;
|
|
53
|
+
}
|
|
54
|
+
getExpiresAt(execution, startedAt) {
|
|
55
|
+
const timeout = this.getTimeout(execution);
|
|
56
|
+
return timeout > 0 ? new Date(startedAt.getTime() + timeout) : null;
|
|
57
|
+
}
|
|
58
|
+
async load() {
|
|
59
|
+
this.stopped = false;
|
|
60
|
+
await this.scanExpiredExecutions();
|
|
61
|
+
await this.scheduleNextExpiresAtTimer();
|
|
62
|
+
this.scheduleScan();
|
|
63
|
+
}
|
|
64
|
+
async unload() {
|
|
65
|
+
var _a;
|
|
66
|
+
this.stopped = true;
|
|
67
|
+
if (this.scanTimer) {
|
|
68
|
+
clearTimeout(this.scanTimer);
|
|
69
|
+
this.scanTimer = null;
|
|
70
|
+
}
|
|
71
|
+
if (this.nextExpiresAtTimer) {
|
|
72
|
+
clearTimeout(this.nextExpiresAtTimer);
|
|
73
|
+
this.nextExpiresAtTimer = null;
|
|
74
|
+
this.nextExpiresAt = null;
|
|
75
|
+
}
|
|
76
|
+
for (const timer of this.timers.values()) {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
this.timers.clear();
|
|
80
|
+
await ((_a = this.scanning) == null ? void 0 : _a.catch(() => {
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
isExpired(execution, now = /* @__PURE__ */ new Date()) {
|
|
84
|
+
return !!execution.expiresAt && execution.expiresAt.getTime() <= now.getTime();
|
|
85
|
+
}
|
|
86
|
+
getRemainingMs(execution, now = /* @__PURE__ */ new Date()) {
|
|
87
|
+
if (!execution.expiresAt) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return execution.expiresAt.getTime() - now.getTime();
|
|
91
|
+
}
|
|
92
|
+
async abort(execution, options = {}) {
|
|
93
|
+
const aborted = await (0, import_utils.abortExecution)(this.plugin, execution, {
|
|
94
|
+
...options,
|
|
95
|
+
reason: import_constants.EXECUTION_REASON.TIMEOUT
|
|
96
|
+
});
|
|
97
|
+
if (aborted) {
|
|
98
|
+
this.clearExecutionTimeout(execution.id);
|
|
99
|
+
}
|
|
100
|
+
return aborted;
|
|
101
|
+
}
|
|
102
|
+
async abortExecutionIfExpired(execution, options = {}) {
|
|
103
|
+
if (execution.status !== import_constants.EXECUTION_STATUS.STARTED || !this.isExpired(execution)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return this.abort(execution, options);
|
|
107
|
+
}
|
|
108
|
+
clear(executionId) {
|
|
109
|
+
this.clearExecutionTimeout(executionId);
|
|
110
|
+
}
|
|
111
|
+
async shouldContinue(execution, options = {}) {
|
|
112
|
+
if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (!this.isExpired(execution)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
await this.abortExecutionIfExpired(execution, options);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Owner-only per-execution timer. Only call from code paths that have acquired
|
|
123
|
+
* local execution ownership, such as Dispatcher/Processor processing paths.
|
|
124
|
+
*/
|
|
125
|
+
scheduleExecutionTimeout(execution) {
|
|
126
|
+
const id = String(execution.id);
|
|
127
|
+
this.clearExecutionTimeout(id);
|
|
128
|
+
if (execution.status !== import_constants.EXECUTION_STATUS.STARTED || !execution.expiresAt) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.scheduleNextExpiresAtIfEarlier(execution.expiresAt);
|
|
132
|
+
const remaining = this.getRemainingMs(execution);
|
|
133
|
+
if (remaining == null) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (remaining <= 0) {
|
|
137
|
+
setImmediate(() => {
|
|
138
|
+
this.handleExecutionTimeout(id).catch((error) => {
|
|
139
|
+
this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) timeout handling failed`, {
|
|
140
|
+
error
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.timers.set(
|
|
147
|
+
id,
|
|
148
|
+
setTimeout(
|
|
149
|
+
() => {
|
|
150
|
+
if (execution.expiresAt && execution.expiresAt.getTime() > Date.now()) {
|
|
151
|
+
this.scheduleExecutionTimeout(execution);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.handleExecutionTimeout(id).catch((error) => {
|
|
155
|
+
this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) timeout handling failed`, {
|
|
156
|
+
error
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
Math.min(remaining, MAX_TIMER_DELAY)
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
invalidateNextExpiresAtIfMatches(expiresAt) {
|
|
165
|
+
if (!expiresAt || !this.nextExpiresAt || this.nextExpiresAt.getTime() !== expiresAt.getTime()) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.clearNextExpiresAtTimer();
|
|
169
|
+
this.scheduleNextExpiresAtTimer().catch((error) => {
|
|
170
|
+
this.plugin.getLogger("timeout").error(`workflow execution timeout one-shot refresh failed`, { error });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async scanExpiredExecutions() {
|
|
174
|
+
if (this.scanning) {
|
|
175
|
+
return this.scanning;
|
|
176
|
+
}
|
|
177
|
+
if (this.stopped) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.scanning = (async () => {
|
|
181
|
+
const ExecutionModelClass = this.plugin.db.getModel("executions");
|
|
182
|
+
const executions = await ExecutionModelClass.findAll({
|
|
183
|
+
attributes: ["id", "workflowId", "status", "expiresAt"],
|
|
184
|
+
where: {
|
|
185
|
+
status: import_constants.EXECUTION_STATUS.STARTED,
|
|
186
|
+
expiresAt: {
|
|
187
|
+
[import_database.Op.not]: null,
|
|
188
|
+
[import_database.Op.lt]: /* @__PURE__ */ new Date()
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
order: [
|
|
192
|
+
["expiresAt", "ASC"],
|
|
193
|
+
["id", "ASC"]
|
|
194
|
+
],
|
|
195
|
+
limit: LOAD_BATCH_SIZE
|
|
196
|
+
});
|
|
197
|
+
for (const execution of executions) {
|
|
198
|
+
await this.abortExecutionIfExpired(execution);
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
try {
|
|
202
|
+
await this.scanning;
|
|
203
|
+
} finally {
|
|
204
|
+
this.scanning = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
scheduleScan() {
|
|
208
|
+
if (this.stopped) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (this.scanTimer) {
|
|
212
|
+
clearTimeout(this.scanTimer);
|
|
213
|
+
}
|
|
214
|
+
this.scanTimer = setTimeout(
|
|
215
|
+
async () => {
|
|
216
|
+
this.scanTimer = null;
|
|
217
|
+
if (this.stopped) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await this.scanExpiredExecutions();
|
|
222
|
+
this.scheduleNextExpiresAtTimer();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.plugin.getLogger("timeout").error(`workflow execution timeout scan failed`, { error });
|
|
225
|
+
} finally {
|
|
226
|
+
this.scheduleScan();
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
DEFAULT_SCAN_INTERVAL + Math.floor(Math.random() * SCAN_JITTER)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
async scheduleNextExpiresAtTimer() {
|
|
233
|
+
if (this.stopped) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const ExecutionModelClass = this.plugin.db.getModel("executions");
|
|
237
|
+
const execution = await ExecutionModelClass.findOne({
|
|
238
|
+
attributes: ["id", "expiresAt"],
|
|
239
|
+
where: {
|
|
240
|
+
status: import_constants.EXECUTION_STATUS.STARTED,
|
|
241
|
+
expiresAt: {
|
|
242
|
+
[import_database.Op.not]: null
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
order: [
|
|
246
|
+
["expiresAt", "ASC"],
|
|
247
|
+
["id", "ASC"]
|
|
248
|
+
]
|
|
249
|
+
});
|
|
250
|
+
if (!(execution == null ? void 0 : execution.expiresAt)) {
|
|
251
|
+
this.clearNextExpiresAtTimer();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
this.scheduleNextExpiresAtIfEarlier(execution.expiresAt, true);
|
|
255
|
+
}
|
|
256
|
+
scheduleNextExpiresAtIfEarlier(expiresAt, force = false) {
|
|
257
|
+
if (this.stopped) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!force && this.nextExpiresAt && this.nextExpiresAt.getTime() <= expiresAt.getTime()) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
this.clearNextExpiresAtTimer();
|
|
264
|
+
this.nextExpiresAt = expiresAt;
|
|
265
|
+
const remaining = expiresAt.getTime() - Date.now();
|
|
266
|
+
const delay = Math.max(0, Math.min(remaining, MAX_TIMER_DELAY));
|
|
267
|
+
this.nextExpiresAtTimer = setTimeout(() => {
|
|
268
|
+
this.handleNextExpiresAtTimeout(expiresAt).catch((error) => {
|
|
269
|
+
this.plugin.getLogger("timeout").error(`workflow execution timeout one-shot failed`, { error });
|
|
270
|
+
});
|
|
271
|
+
}, delay);
|
|
272
|
+
}
|
|
273
|
+
async handleNextExpiresAtTimeout(expiresAt) {
|
|
274
|
+
this.nextExpiresAtTimer = null;
|
|
275
|
+
this.nextExpiresAt = null;
|
|
276
|
+
if (expiresAt.getTime() > Date.now()) {
|
|
277
|
+
this.scheduleNextExpiresAtIfEarlier(expiresAt, true);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
await this.scanExpiredExecutions();
|
|
281
|
+
await this.scheduleNextExpiresAtTimer();
|
|
282
|
+
}
|
|
283
|
+
clearNextExpiresAtTimer() {
|
|
284
|
+
if (this.nextExpiresAtTimer) {
|
|
285
|
+
clearTimeout(this.nextExpiresAtTimer);
|
|
286
|
+
this.nextExpiresAtTimer = null;
|
|
287
|
+
}
|
|
288
|
+
this.nextExpiresAt = null;
|
|
289
|
+
}
|
|
290
|
+
clearExecutionTimeout(executionId) {
|
|
291
|
+
const id = String(executionId);
|
|
292
|
+
const timer = this.timers.get(id);
|
|
293
|
+
if (!timer) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
this.timers.delete(id);
|
|
298
|
+
}
|
|
299
|
+
async handleExecutionTimeout(executionId) {
|
|
300
|
+
this.clearExecutionTimeout(executionId);
|
|
301
|
+
if (this.plugin.abortRunningExecution(executionId, import_constants.EXECUTION_REASON.TIMEOUT)) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const execution = await this.plugin.db.getRepository("executions").findOne({
|
|
305
|
+
filterByTk: executionId
|
|
306
|
+
});
|
|
307
|
+
if (!execution) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
await this.abortExecutionIfExpired(execution);
|
|
311
|
+
}
|
|
312
|
+
}
|
package/dist/server/Plugin.d.ts
CHANGED
|
@@ -13,10 +13,13 @@ import { Registry } from '@nocobase/utils';
|
|
|
13
13
|
import { Logger } from '@nocobase/logger';
|
|
14
14
|
import Dispatcher, { EventOptions } from './Dispatcher';
|
|
15
15
|
import Processor from './Processor';
|
|
16
|
+
import ExecutionTimeoutManager from './ExecutionTimeoutManager';
|
|
17
|
+
import RunningExecutionRegistry from './RunningExecutionRegistry';
|
|
16
18
|
import { CustomFunction } from './functions';
|
|
17
19
|
import Trigger from './triggers';
|
|
18
20
|
import { InstructionInterface } from './instructions';
|
|
19
21
|
import type { ExecutionModel, WorkflowModel } from './types';
|
|
22
|
+
import type { Transaction } from '@nocobase/database';
|
|
20
23
|
type ID = number | string;
|
|
21
24
|
export declare const WORKER_JOB_WORKFLOW_PROCESS = "workflow:process";
|
|
22
25
|
export default class PluginWorkflowServer extends Plugin {
|
|
@@ -26,6 +29,8 @@ export default class PluginWorkflowServer extends Plugin {
|
|
|
26
29
|
enabledCache: Map<number, WorkflowModel>;
|
|
27
30
|
snowflake: Snowflake;
|
|
28
31
|
private dispatcher;
|
|
32
|
+
readonly timeoutManager: ExecutionTimeoutManager;
|
|
33
|
+
readonly runningExecutionRegistry: RunningExecutionRegistry;
|
|
29
34
|
get channelPendingExecution(): string;
|
|
30
35
|
private loggerCache;
|
|
31
36
|
private meter;
|
|
@@ -38,6 +43,12 @@ export default class PluginWorkflowServer extends Plugin {
|
|
|
38
43
|
private onBeforeStop;
|
|
39
44
|
handleSyncMessage(message: any): Promise<void>;
|
|
40
45
|
serving(): boolean;
|
|
46
|
+
abortExecutionIfExpired(execution: ExecutionModel, options?: {
|
|
47
|
+
transaction?: Transaction;
|
|
48
|
+
}): Promise<boolean>;
|
|
49
|
+
registerRunningExecution(executionId: number | string, abort: (reason?: string) => void): void;
|
|
50
|
+
unregisterRunningExecution(executionId: number | string): void;
|
|
51
|
+
abortRunningExecution(executionId: number | string, reason?: string): boolean;
|
|
41
52
|
/**
|
|
42
53
|
* @experimental
|
|
43
54
|
*/
|
|
@@ -65,6 +76,7 @@ export default class PluginWorkflowServer extends Plugin {
|
|
|
65
76
|
private toggle;
|
|
66
77
|
trigger(workflow: WorkflowModel, context: object, options?: EventOptions): void | Promise<Processor | null>;
|
|
67
78
|
run(pending: Parameters<Dispatcher['run']>[0]): Promise<void>;
|
|
79
|
+
dispatch(): void;
|
|
68
80
|
resume(job: any): Promise<void>;
|
|
69
81
|
/**
|
|
70
82
|
* Start a deferred execution
|
package/dist/server/Plugin.js
CHANGED
|
@@ -48,6 +48,8 @@ var import_server = require("@nocobase/server");
|
|
|
48
48
|
var import_utils = require("@nocobase/utils");
|
|
49
49
|
var import_Dispatcher = __toESM(require("./Dispatcher"));
|
|
50
50
|
var import_Processor = __toESM(require("./Processor"));
|
|
51
|
+
var import_ExecutionTimeoutManager = __toESM(require("./ExecutionTimeoutManager"));
|
|
52
|
+
var import_RunningExecutionRegistry = __toESM(require("./RunningExecutionRegistry"));
|
|
51
53
|
var import_actions = __toESM(require("./actions"));
|
|
52
54
|
var import_nodes = require("./actions/nodes");
|
|
53
55
|
var import_workflows = require("./actions/workflows");
|
|
@@ -72,6 +74,8 @@ class PluginWorkflowServer extends import_server.Plugin {
|
|
|
72
74
|
enabledCache = /* @__PURE__ */ new Map();
|
|
73
75
|
snowflake;
|
|
74
76
|
dispatcher = new import_Dispatcher.default(this);
|
|
77
|
+
timeoutManager = new import_ExecutionTimeoutManager.default(this);
|
|
78
|
+
runningExecutionRegistry = new import_RunningExecutionRegistry.default();
|
|
75
79
|
get channelPendingExecution() {
|
|
76
80
|
return `${this.name}.pendingExecution`;
|
|
77
81
|
}
|
|
@@ -166,9 +170,9 @@ class PluginWorkflowServer extends import_server.Plugin {
|
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
this.checker = setInterval(() => {
|
|
169
|
-
this.getLogger("dispatcher").debug(`(cycling) check for queueing executions`);
|
|
170
173
|
this.dispatcher.dispatch();
|
|
171
174
|
}, 3e5);
|
|
175
|
+
await this.timeoutManager.load();
|
|
172
176
|
this.app.on("workflow:dispatch", () => {
|
|
173
177
|
this.app.logger.info("workflow:dispatch");
|
|
174
178
|
this.dispatcher.dispatch();
|
|
@@ -181,6 +185,7 @@ class PluginWorkflowServer extends import_server.Plugin {
|
|
|
181
185
|
if (this.checker) {
|
|
182
186
|
clearInterval(this.checker);
|
|
183
187
|
}
|
|
188
|
+
await this.timeoutManager.unload();
|
|
184
189
|
await this.dispatcher.beforeStop();
|
|
185
190
|
this.app.logger.info(`stopping workflow plugin before app (${this.app.name}) shutdown...`);
|
|
186
191
|
for (const workflow of this.enabledCache.values()) {
|
|
@@ -214,6 +219,18 @@ class PluginWorkflowServer extends import_server.Plugin {
|
|
|
214
219
|
serving() {
|
|
215
220
|
return this.app.serving(WORKER_JOB_WORKFLOW_PROCESS);
|
|
216
221
|
}
|
|
222
|
+
async abortExecutionIfExpired(execution, options = {}) {
|
|
223
|
+
return this.timeoutManager.abortExecutionIfExpired(execution, options);
|
|
224
|
+
}
|
|
225
|
+
registerRunningExecution(executionId, abort) {
|
|
226
|
+
this.runningExecutionRegistry.register(executionId, { abort });
|
|
227
|
+
}
|
|
228
|
+
unregisterRunningExecution(executionId) {
|
|
229
|
+
this.runningExecutionRegistry.unregister(executionId);
|
|
230
|
+
}
|
|
231
|
+
abortRunningExecution(executionId, reason) {
|
|
232
|
+
return this.runningExecutionRegistry.abort(executionId, reason);
|
|
233
|
+
}
|
|
217
234
|
/**
|
|
218
235
|
* @experimental
|
|
219
236
|
*/
|
|
@@ -434,6 +451,9 @@ class PluginWorkflowServer extends import_server.Plugin {
|
|
|
434
451
|
async run(pending) {
|
|
435
452
|
return this.dispatcher.run(pending);
|
|
436
453
|
}
|
|
454
|
+
dispatch() {
|
|
455
|
+
return this.dispatcher.dispatch();
|
|
456
|
+
}
|
|
437
457
|
async resume(job) {
|
|
438
458
|
return this.dispatcher.resume(job);
|
|
439
459
|
}
|
|
@@ -6,14 +6,29 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
+
/// <reference types="node" />
|
|
9
10
|
import { Transaction, Transactionable } from '@nocobase/database';
|
|
10
11
|
import { Logger } from '@nocobase/logger';
|
|
11
12
|
import type Plugin from './Plugin';
|
|
13
|
+
import { IJob } from './instructions';
|
|
12
14
|
import type { ExecutionModel, FlowNodeModel, JobModel } from './types';
|
|
15
|
+
export type ProcessorRunOptions = {
|
|
16
|
+
rerun?: true;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
};
|
|
19
|
+
export type ProcessorRerunOptions = {
|
|
20
|
+
nodeId?: string | number;
|
|
21
|
+
overwrite?: boolean;
|
|
22
|
+
};
|
|
13
23
|
export interface ProcessorOptions extends Transactionable {
|
|
14
24
|
plugin: Plugin;
|
|
15
25
|
[key: string]: any;
|
|
16
26
|
}
|
|
27
|
+
export type BackgroundAbortHandle = {
|
|
28
|
+
signal: AbortSignal;
|
|
29
|
+
dispose: () => void;
|
|
30
|
+
throwIfAborted: () => void;
|
|
31
|
+
};
|
|
17
32
|
export default class Processor {
|
|
18
33
|
execution: ExecutionModel;
|
|
19
34
|
options: ProcessorOptions;
|
|
@@ -31,11 +46,11 @@ export default class Processor {
|
|
|
31
46
|
/**
|
|
32
47
|
* @experimental
|
|
33
48
|
*/
|
|
34
|
-
transaction
|
|
49
|
+
transaction?: Transaction | null;
|
|
35
50
|
/**
|
|
36
51
|
* @experimental
|
|
37
52
|
*/
|
|
38
|
-
mainTransaction
|
|
53
|
+
mainTransaction?: Transaction | null;
|
|
39
54
|
/**
|
|
40
55
|
* @experimental
|
|
41
56
|
*/
|
|
@@ -47,29 +62,70 @@ export default class Processor {
|
|
|
47
62
|
private jobsMapByNodeKey;
|
|
48
63
|
private jobResultsMapByNodeKey;
|
|
49
64
|
private jobsToSave;
|
|
65
|
+
private rerunContext;
|
|
50
66
|
/**
|
|
51
67
|
* @experimental
|
|
52
68
|
*/
|
|
53
69
|
lastSavedJob: JobModel | null;
|
|
70
|
+
abortController: AbortController;
|
|
71
|
+
timeoutGuard: NodeJS.Timeout | null;
|
|
72
|
+
private runningRegistered;
|
|
73
|
+
private abortReason;
|
|
74
|
+
private aborted;
|
|
54
75
|
constructor(execution: ExecutionModel, options: ProcessorOptions);
|
|
76
|
+
get abortSignal(): AbortSignal;
|
|
77
|
+
setTimeoutGuard(ms: number): void;
|
|
78
|
+
abortExecution(reason?: string): void;
|
|
79
|
+
isTimeoutAborted(): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Create an independent abort handle for background work that outlives this processor's
|
|
82
|
+
* run loop (e.g. fire-and-forget instructions that resume the job later). It mirrors the
|
|
83
|
+
* current abort state and sets its own timer based on the execution's `expiresAt`, so the
|
|
84
|
+
* timeout still applies after the processor has exited its synchronous run.
|
|
85
|
+
*
|
|
86
|
+
* The caller must invoke `dispose()` once the background work settles to release the timer
|
|
87
|
+
* and the abort listener.
|
|
88
|
+
*/
|
|
89
|
+
createBackgroundAbortHandle(): BackgroundAbortHandle;
|
|
90
|
+
/**
|
|
91
|
+
* Reload a job and return it only when it is still pending, otherwise `null`. Background
|
|
92
|
+
* work uses this before resuming so it never overwrites a job that another path (timeout
|
|
93
|
+
* abort, a competing resume) has already settled.
|
|
94
|
+
*/
|
|
95
|
+
findPendingJob(jobId: number | string): Promise<JobModel | null>;
|
|
55
96
|
private makeNodes;
|
|
56
97
|
private makeJobs;
|
|
57
98
|
prepare(): Promise<void>;
|
|
58
|
-
start(): Promise<
|
|
99
|
+
start(): Promise<any>;
|
|
59
100
|
resume(job: JobModel): Promise<void>;
|
|
101
|
+
resolveRerun(options?: ProcessorRerunOptions): {
|
|
102
|
+
node: FlowNodeModel;
|
|
103
|
+
input: JobModel | {
|
|
104
|
+
result: any;
|
|
105
|
+
};
|
|
106
|
+
targetJob: JobModel;
|
|
107
|
+
};
|
|
108
|
+
rerun(options?: ProcessorRerunOptions): Promise<any>;
|
|
109
|
+
private getRerunNode;
|
|
110
|
+
private getRerunInput;
|
|
60
111
|
private exec;
|
|
61
|
-
run(node:
|
|
62
|
-
|
|
112
|
+
run(node: FlowNodeModel, input?: JobModel | {
|
|
113
|
+
result: unknown;
|
|
114
|
+
}, options?: ProcessorRunOptions): Promise<any>;
|
|
115
|
+
end(node: FlowNodeModel, job: JobModel): Promise<any>;
|
|
63
116
|
private recall;
|
|
64
117
|
exit(s?: number | true): Promise<any>;
|
|
65
118
|
/**
|
|
66
119
|
* @experimental
|
|
67
120
|
*/
|
|
68
|
-
saveJob(payload: JobModel |
|
|
121
|
+
saveJob(payload: JobModel | IJob): JobModel;
|
|
69
122
|
/**
|
|
70
123
|
* @experimental
|
|
71
124
|
*/
|
|
72
125
|
getBranches(node: FlowNodeModel): FlowNodeModel[];
|
|
126
|
+
private enterRunningState;
|
|
127
|
+
private shouldContinueExecution;
|
|
128
|
+
private leaveRunningState;
|
|
73
129
|
/**
|
|
74
130
|
* @experimental
|
|
75
131
|
* find the first node in current branch
|
|
@@ -79,7 +135,7 @@ export default class Processor {
|
|
|
79
135
|
* @experimental
|
|
80
136
|
* find the node start current branch
|
|
81
137
|
*/
|
|
82
|
-
findBranchParentNode(node
|
|
138
|
+
findBranchParentNode(node?: FlowNodeModel): FlowNodeModel | null;
|
|
83
139
|
/**
|
|
84
140
|
* @experimental
|
|
85
141
|
*/
|
|
@@ -102,7 +158,7 @@ export default class Processor {
|
|
|
102
158
|
[key: string]: any;
|
|
103
159
|
};
|
|
104
160
|
$system: {};
|
|
105
|
-
$scopes:
|
|
161
|
+
$scopes: Record<string, any>;
|
|
106
162
|
$env: {};
|
|
107
163
|
};
|
|
108
164
|
$context: any;
|
|
@@ -110,7 +166,7 @@ export default class Processor {
|
|
|
110
166
|
[key: string]: any;
|
|
111
167
|
};
|
|
112
168
|
$system: {};
|
|
113
|
-
$scopes:
|
|
169
|
+
$scopes: Record<string, any>;
|
|
114
170
|
$env: {};
|
|
115
171
|
};
|
|
116
172
|
/**
|