@nocobase/plugin-workflow-javascript 3.0.0-alpha.6 → 3.0.0-alpha.8
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/index.js +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/common/constants.d.ts +9 -0
- package/dist/common/constants.js +36 -0
- package/dist/externalVersion.js +6 -5
- package/dist/node_modules/joi/package.json +1 -1
- package/dist/node_modules/winston-transport/package.json +1 -1
- package/dist/server/RunningJobs.d.ts +34 -0
- package/dist/server/RunningJobs.js +105 -0
- package/dist/server/ScriptInstruction.d.ts +9 -21
- package/dist/server/ScriptInstruction.js +44 -134
- package/dist/server/ScriptWorkerRunner.d.ts +27 -0
- package/dist/server/ScriptWorkerRunner.js +140 -0
- package/dist/server/TaskConsumer.d.ts +29 -0
- package/dist/server/TaskConsumer.js +317 -0
- package/dist/server/TaskRecovery.d.ts +22 -0
- package/dist/server/TaskRecovery.js +133 -0
- package/dist/server/constants.d.ts +9 -0
- package/dist/server/constants.js +36 -0
- package/dist/server/plugin.d.ts +14 -0
- package/dist/server/plugin.js +92 -2
- package/package.json +2 -2
|
@@ -0,0 +1,317 @@
|
|
|
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 TaskConsumer_exports = {};
|
|
28
|
+
__export(TaskConsumer_exports, {
|
|
29
|
+
TaskConsumer: () => TaskConsumer
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(TaskConsumer_exports);
|
|
32
|
+
var import_plugin_workflow = require("@nocobase/plugin-workflow");
|
|
33
|
+
var import_constants = require("../common/constants");
|
|
34
|
+
var import_ScriptWorkerRunner = require("./ScriptWorkerRunner");
|
|
35
|
+
function isTaskMessage(message) {
|
|
36
|
+
return typeof message === "object" && message !== null && "jobId" in message;
|
|
37
|
+
}
|
|
38
|
+
function isScriptArguments(value) {
|
|
39
|
+
return value === null || Array.isArray(value) || typeof value === "object" && value !== null;
|
|
40
|
+
}
|
|
41
|
+
function getJavaScriptArguments(job) {
|
|
42
|
+
if (!job.meta || !("args" in job.meta) || !isScriptArguments(job.meta.args)) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
return job.meta.args;
|
|
46
|
+
}
|
|
47
|
+
function getErrorMessage(error) {
|
|
48
|
+
return error instanceof Error ? error.message : String(error);
|
|
49
|
+
}
|
|
50
|
+
function createJobAbortController(workflowPlugin, runningJobs, job, execution) {
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
let timeoutGuard = null;
|
|
53
|
+
const abort = (reason) => {
|
|
54
|
+
if (!controller.signal.aborted) {
|
|
55
|
+
if ((0, import_plugin_workflow.isWorkflowTimeoutError)(reason)) {
|
|
56
|
+
controller.abort(reason);
|
|
57
|
+
} else if (reason === import_plugin_workflow.EXECUTION_REASON.TIMEOUT) {
|
|
58
|
+
controller.abort(new import_plugin_workflow.WorkflowTimeoutError());
|
|
59
|
+
} else {
|
|
60
|
+
controller.abort(reason instanceof Error ? reason : new Error("Workflow execution has been aborted"));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const remaining = execution.expiresAt ? execution.expiresAt.getTime() - Date.now() : null;
|
|
65
|
+
if (remaining != null) {
|
|
66
|
+
if (remaining <= 0) {
|
|
67
|
+
abort();
|
|
68
|
+
} else {
|
|
69
|
+
timeoutGuard = setTimeout(abort, remaining);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const unregister = workflowPlugin.registerRunningExecution(execution.id, abort);
|
|
73
|
+
const unregisterJob = runningJobs.register({
|
|
74
|
+
jobId: job.id,
|
|
75
|
+
executionId: execution.id,
|
|
76
|
+
abort
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
abort,
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
dispose: () => {
|
|
82
|
+
if (timeoutGuard) {
|
|
83
|
+
clearTimeout(timeoutGuard);
|
|
84
|
+
timeoutGuard = null;
|
|
85
|
+
}
|
|
86
|
+
unregister();
|
|
87
|
+
unregisterJob();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
class TaskConsumer {
|
|
92
|
+
constructor(workflowPlugin, runningJobs) {
|
|
93
|
+
this.workflowPlugin = workflowPlugin;
|
|
94
|
+
this.runningJobs = runningJobs;
|
|
95
|
+
}
|
|
96
|
+
ready = false;
|
|
97
|
+
closing = false;
|
|
98
|
+
processing = /* @__PURE__ */ new Set();
|
|
99
|
+
setReady(ready) {
|
|
100
|
+
this.ready = ready;
|
|
101
|
+
}
|
|
102
|
+
idle() {
|
|
103
|
+
return this.ready && !this.closing && this.workflowPlugin.serving();
|
|
104
|
+
}
|
|
105
|
+
async beforeStop() {
|
|
106
|
+
this.closing = true;
|
|
107
|
+
await Promise.allSettled(Array.from(this.processing));
|
|
108
|
+
}
|
|
109
|
+
process = async (message) => {
|
|
110
|
+
if (!isTaskMessage(message)) {
|
|
111
|
+
this.workflowPlugin.getLogger("javascript").warn("invalid JavaScript queue job message ignored", { message });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const processing = this.processWithPermit(message.jobId);
|
|
115
|
+
this.processing.add(processing);
|
|
116
|
+
try {
|
|
117
|
+
await processing;
|
|
118
|
+
} finally {
|
|
119
|
+
this.processing.delete(processing);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
async processWithPermit(jobId) {
|
|
123
|
+
if (this.closing) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const existingJob = await this.workflowPlugin.db.getRepository("jobs").findOne({
|
|
127
|
+
filterByTk: jobId
|
|
128
|
+
});
|
|
129
|
+
if (!existingJob) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (existingJob.status !== import_plugin_workflow.JOB_STATUS.PENDING) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const node = await existingJob.getNode();
|
|
136
|
+
if ((node == null ? void 0 : node.type) !== import_constants.SCRIPT_INSTRUCTION_TYPE) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const claimedJob = await this.claimJob(existingJob);
|
|
140
|
+
if (!claimedJob) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const job = claimedJob;
|
|
144
|
+
const config = node.config;
|
|
145
|
+
const payload = {
|
|
146
|
+
content: (config == null ? void 0 : config.content) ?? "",
|
|
147
|
+
args: getJavaScriptArguments(job),
|
|
148
|
+
timeout: config == null ? void 0 : config.timeout,
|
|
149
|
+
continue: config == null ? void 0 : config.continue
|
|
150
|
+
};
|
|
151
|
+
const execution = await job.getExecution({ attributes: ["id", "workflowId", "status"] });
|
|
152
|
+
if (!execution || execution.status !== import_plugin_workflow.EXECUTION_STATUS.STARTED) {
|
|
153
|
+
await this.finishClaimedJob(job, {
|
|
154
|
+
status: import_plugin_workflow.JOB_STATUS.ABORTED,
|
|
155
|
+
result: `Execution (${job.executionId}) is not running`
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const logger = this.workflowPlugin.getLogger((execution == null ? void 0 : execution.workflowId) ?? "javascript");
|
|
160
|
+
const queueWaitMs = job.startedAt && job.createdAt ? job.startedAt.getTime() - job.createdAt.getTime() : 0;
|
|
161
|
+
logger.info(`JavaScript job (${job.id}) claimed for node (${job.nodeId})`, {
|
|
162
|
+
executionId: job.executionId,
|
|
163
|
+
jobId: job.id,
|
|
164
|
+
nodeId: job.nodeId,
|
|
165
|
+
queueWaitMs
|
|
166
|
+
});
|
|
167
|
+
if (!await this.workflowPlugin.timeoutManager.shouldContinue(execution)) {
|
|
168
|
+
await this.finishClaimedJob(job, {
|
|
169
|
+
status: import_plugin_workflow.JOB_STATUS.ABORTED,
|
|
170
|
+
result: "Execution timeout reached before JavaScript Worker started"
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const abortHandle = createJobAbortController(this.workflowPlugin, this.runningJobs, job, execution);
|
|
175
|
+
const stopHeartbeat = this.startClaimHeartbeat(job, abortHandle.abort);
|
|
176
|
+
let values;
|
|
177
|
+
try {
|
|
178
|
+
const result = await import_ScriptWorkerRunner.ScriptWorkerRunner.run(payload.content, payload.args, {
|
|
179
|
+
timeout: payload.timeout,
|
|
180
|
+
logger,
|
|
181
|
+
signal: abortHandle.signal
|
|
182
|
+
});
|
|
183
|
+
if (result.status === import_plugin_workflow.JOB_STATUS.RESOLVED) {
|
|
184
|
+
logger.info(`script (#${job.nodeId}) get result success`);
|
|
185
|
+
values = { status: import_plugin_workflow.JOB_STATUS.RESOLVED, result: result.result };
|
|
186
|
+
logger.info(`run script execution success, node id: ${job.nodeId},the result is ${result.result}`);
|
|
187
|
+
} else if (payload.continue) {
|
|
188
|
+
logger.warn(`script (#${job.nodeId}) get result failed, the reason is ${result.result}`);
|
|
189
|
+
values = { status: import_plugin_workflow.JOB_STATUS.RESOLVED, result: result.result };
|
|
190
|
+
} else {
|
|
191
|
+
logger.info(`script (#${job.nodeId}) get result failed, the reason is ${result.result}`);
|
|
192
|
+
values = { status: import_plugin_workflow.JOB_STATUS.ERROR, result: result.result };
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const message = getErrorMessage(error);
|
|
196
|
+
logger.error(`script (#${job.nodeId}) get result failed, the reason is ${message}`);
|
|
197
|
+
values = {
|
|
198
|
+
status: abortHandle.signal.aborted || (0, import_plugin_workflow.isWorkflowTimeoutError)(error) ? import_plugin_workflow.JOB_STATUS.ABORTED : import_plugin_workflow.JOB_STATUS.ERROR,
|
|
199
|
+
result: message
|
|
200
|
+
};
|
|
201
|
+
} finally {
|
|
202
|
+
abortHandle.dispose();
|
|
203
|
+
await stopHeartbeat();
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await this.settleClaimedJob(job, execution, values, logger);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
logger.error(`JavaScript job (${job.id}) failed to resume workflow execution (${job.executionId})`, { error });
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async claimJob(job) {
|
|
213
|
+
if (job.startedAt != null) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const now = /* @__PURE__ */ new Date();
|
|
217
|
+
const JobModel = this.workflowPlugin.db.getModel("jobs");
|
|
218
|
+
const [affected] = await JobModel.update(
|
|
219
|
+
{
|
|
220
|
+
startedAt: now
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
where: {
|
|
224
|
+
id: job.id,
|
|
225
|
+
status: import_plugin_workflow.JOB_STATUS.PENDING,
|
|
226
|
+
startedAt: null
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
if (!affected) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const claimedJob = await job.reload();
|
|
234
|
+
return claimedJob;
|
|
235
|
+
}
|
|
236
|
+
async finishClaimedJob(job, values) {
|
|
237
|
+
const updated = await this.updatePendingJob(job, values);
|
|
238
|
+
if (updated) {
|
|
239
|
+
await this.workflowPlugin.resume(job);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async settleClaimedJob(job, execution, values, logger) {
|
|
243
|
+
if (!await this.workflowPlugin.timeoutManager.shouldContinue(execution)) {
|
|
244
|
+
logger.warn(`script (#${job.nodeId}) result discarded because execution (${execution.id}) is ended`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
await execution.reload();
|
|
248
|
+
await job.reload();
|
|
249
|
+
if (execution.status !== import_plugin_workflow.EXECUTION_STATUS.STARTED || job.status !== import_plugin_workflow.JOB_STATUS.PENDING) {
|
|
250
|
+
logger.warn(`script (#${job.nodeId}) result discarded because execution (${execution.id}) is ended`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const updated = await this.updatePendingJob(job, values);
|
|
254
|
+
if (updated) {
|
|
255
|
+
await this.workflowPlugin.resume(job);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async updatePendingJob(job, values) {
|
|
259
|
+
const JobModel = this.workflowPlugin.db.getModel("jobs");
|
|
260
|
+
const [affected] = await JobModel.update(
|
|
261
|
+
{
|
|
262
|
+
status: values.status,
|
|
263
|
+
result: values.result
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
where: {
|
|
267
|
+
id: job.id,
|
|
268
|
+
status: import_plugin_workflow.JOB_STATUS.PENDING,
|
|
269
|
+
startedAt: job.startedAt
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
if (!affected) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
await job.reload();
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
startClaimHeartbeat(job, abort) {
|
|
280
|
+
let heartbeat = null;
|
|
281
|
+
const beat = () => {
|
|
282
|
+
if (heartbeat) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const JobModel = this.workflowPlugin.db.getModel("jobs");
|
|
286
|
+
heartbeat = JobModel.update(
|
|
287
|
+
{ updatedAt: /* @__PURE__ */ new Date() },
|
|
288
|
+
{
|
|
289
|
+
where: {
|
|
290
|
+
id: job.id,
|
|
291
|
+
status: import_plugin_workflow.JOB_STATUS.PENDING,
|
|
292
|
+
startedAt: job.startedAt
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
).then(([affected]) => {
|
|
296
|
+
if (!affected) {
|
|
297
|
+
abort();
|
|
298
|
+
}
|
|
299
|
+
}).catch((error) => {
|
|
300
|
+
this.workflowPlugin.getLogger("javascript").error(`JavaScript job (${job.id}) claim heartbeat failed`, {
|
|
301
|
+
error
|
|
302
|
+
});
|
|
303
|
+
}).finally(() => {
|
|
304
|
+
heartbeat = null;
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
const timer = setInterval(beat, 3e4);
|
|
308
|
+
return async () => {
|
|
309
|
+
clearInterval(timer);
|
|
310
|
+
await heartbeat;
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
315
|
+
0 && (module.exports = {
|
|
316
|
+
TaskConsumer
|
|
317
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
import type WorkflowPlugin from '@nocobase/plugin-workflow';
|
|
10
|
+
export declare class TaskRecovery {
|
|
11
|
+
private readonly workflowPlugin;
|
|
12
|
+
private readonly taskQueueChannel;
|
|
13
|
+
private timer;
|
|
14
|
+
private ready;
|
|
15
|
+
private recovering;
|
|
16
|
+
constructor(workflowPlugin: WorkflowPlugin, taskQueueChannel: string);
|
|
17
|
+
start(): void;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
recover(): void;
|
|
20
|
+
private recoverTasks;
|
|
21
|
+
private republishQueuedJobs;
|
|
22
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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 TaskRecovery_exports = {};
|
|
28
|
+
__export(TaskRecovery_exports, {
|
|
29
|
+
TaskRecovery: () => TaskRecovery
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(TaskRecovery_exports);
|
|
32
|
+
var import_sequelize = require("sequelize");
|
|
33
|
+
var import_plugin_workflow = require("@nocobase/plugin-workflow");
|
|
34
|
+
var import_constants = require("../common/constants");
|
|
35
|
+
const RECOVERY_BATCH_SIZE = 100;
|
|
36
|
+
const RECOVERY_MAX_SCAN_SIZE = 1e3;
|
|
37
|
+
const RECOVERY_INTERVAL = 6e4;
|
|
38
|
+
class TaskRecovery {
|
|
39
|
+
constructor(workflowPlugin, taskQueueChannel) {
|
|
40
|
+
this.workflowPlugin = workflowPlugin;
|
|
41
|
+
this.taskQueueChannel = taskQueueChannel;
|
|
42
|
+
}
|
|
43
|
+
timer = null;
|
|
44
|
+
ready = false;
|
|
45
|
+
recovering = null;
|
|
46
|
+
start() {
|
|
47
|
+
this.ready = true;
|
|
48
|
+
this.recover();
|
|
49
|
+
this.timer = setInterval(() => {
|
|
50
|
+
this.recover();
|
|
51
|
+
}, RECOVERY_INTERVAL);
|
|
52
|
+
}
|
|
53
|
+
async stop() {
|
|
54
|
+
var _a;
|
|
55
|
+
this.ready = false;
|
|
56
|
+
if (this.timer) {
|
|
57
|
+
clearInterval(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
await ((_a = this.recovering) == null ? void 0 : _a.catch(() => void 0));
|
|
61
|
+
}
|
|
62
|
+
recover() {
|
|
63
|
+
if (!this.ready || this.recovering) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const recovering = this.recoverTasks().catch(() => void 0).finally(() => {
|
|
67
|
+
if (this.recovering === recovering) {
|
|
68
|
+
this.recovering = null;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
this.recovering = recovering;
|
|
72
|
+
}
|
|
73
|
+
async recoverTasks() {
|
|
74
|
+
const logger = this.workflowPlugin.getLogger("javascript");
|
|
75
|
+
if (!this.workflowPlugin.serving()) {
|
|
76
|
+
logger.warn("workflow:process is not serving on this instance, JavaScript job recovery will be ignored");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await this.republishQueuedJobs();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error("JavaScript job recovery failed", { error });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async republishQueuedJobs() {
|
|
86
|
+
let cursor = null;
|
|
87
|
+
let scanned = 0;
|
|
88
|
+
const staleBefore = new Date(Date.now() - 12e4);
|
|
89
|
+
const logger = this.workflowPlugin.getLogger("javascript");
|
|
90
|
+
const JobModel = this.workflowPlugin.db.getModel("jobs");
|
|
91
|
+
while (scanned < RECOVERY_MAX_SCAN_SIZE) {
|
|
92
|
+
const jobs = await JobModel.findAll({
|
|
93
|
+
where: {
|
|
94
|
+
...cursor == null ? {} : { id: { [import_sequelize.Op.gt]: cursor } },
|
|
95
|
+
status: import_plugin_workflow.JOB_STATUS.PENDING
|
|
96
|
+
},
|
|
97
|
+
attributes: ["id", "startedAt", "updatedAt"],
|
|
98
|
+
include: [
|
|
99
|
+
{
|
|
100
|
+
association: "node",
|
|
101
|
+
attributes: [],
|
|
102
|
+
where: {
|
|
103
|
+
type: import_constants.SCRIPT_INSTRUCTION_TYPE
|
|
104
|
+
},
|
|
105
|
+
required: true
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
order: [["id", "ASC"]],
|
|
109
|
+
limit: Math.min(RECOVERY_BATCH_SIZE, RECOVERY_MAX_SCAN_SIZE - scanned)
|
|
110
|
+
});
|
|
111
|
+
if (!jobs.length) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
for (const job of jobs) {
|
|
115
|
+
if (job.startedAt) {
|
|
116
|
+
if (job.updatedAt && job.updatedAt <= staleBefore) {
|
|
117
|
+
logger.warn(`JavaScript job (${job.id}) claim heartbeat timed out, manual recovery is required`);
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
await this.workflowPlugin.app.eventQueue.publish(this.taskQueueChannel, {
|
|
122
|
+
jobId: job.id
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
scanned += jobs.length;
|
|
126
|
+
cursor = jobs[jobs.length - 1].id;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
131
|
+
0 && (module.exports = {
|
|
132
|
+
TaskRecovery
|
|
133
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
export declare const PENDING_JAVASCRIPT_TASK_CHANNEL = "@nocobase/plugin-workflow-javascript.pendingJavaScriptTask";
|
|
@@ -0,0 +1,36 @@
|
|
|
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 constants_exports = {};
|
|
28
|
+
__export(constants_exports, {
|
|
29
|
+
PENDING_JAVASCRIPT_TASK_CHANNEL: () => PENDING_JAVASCRIPT_TASK_CHANNEL
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(constants_exports);
|
|
32
|
+
const PENDING_JAVASCRIPT_TASK_CHANNEL = "@nocobase/plugin-workflow-javascript.pendingJavaScriptTask";
|
|
33
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
34
|
+
0 && (module.exports = {
|
|
35
|
+
PENDING_JAVASCRIPT_TASK_CHANNEL
|
|
36
|
+
});
|
package/dist/server/plugin.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
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
|
+
*/
|
|
1
9
|
import { Plugin } from '@nocobase/server';
|
|
2
10
|
export declare class PluginWorkflowScriptServer extends Plugin {
|
|
11
|
+
private readonly runningJobs;
|
|
12
|
+
private workflowPlugin;
|
|
13
|
+
private taskConsumer;
|
|
14
|
+
private taskRecovery;
|
|
15
|
+
private readonly handleJobsAfterBulkUpdate;
|
|
3
16
|
afterAdd(): Promise<void>;
|
|
4
17
|
beforeLoad(): Promise<void>;
|
|
5
18
|
load(): Promise<void>;
|
|
19
|
+
handleSyncMessage(message: unknown): Promise<void>;
|
|
6
20
|
install(): Promise<void>;
|
|
7
21
|
afterEnable(): Promise<void>;
|
|
8
22
|
afterDisable(): Promise<void>;
|
package/dist/server/plugin.js
CHANGED
|
@@ -42,15 +42,105 @@ __export(plugin_exports, {
|
|
|
42
42
|
module.exports = __toCommonJS(plugin_exports);
|
|
43
43
|
var import_server = require("@nocobase/server");
|
|
44
44
|
var import_plugin_workflow = __toESM(require("@nocobase/plugin-workflow"));
|
|
45
|
+
var import_constants = require("../common/constants");
|
|
45
46
|
var import_ScriptInstruction = __toESM(require("./ScriptInstruction"));
|
|
47
|
+
var import_constants2 = require("./constants");
|
|
48
|
+
var import_RunningJobs = require("./RunningJobs");
|
|
49
|
+
var import_TaskConsumer = require("./TaskConsumer");
|
|
50
|
+
var import_TaskRecovery = require("./TaskRecovery");
|
|
46
51
|
class PluginWorkflowScriptServer extends import_server.Plugin {
|
|
52
|
+
runningJobs = new import_RunningJobs.RunningJobs();
|
|
53
|
+
workflowPlugin;
|
|
54
|
+
taskConsumer;
|
|
55
|
+
taskRecovery;
|
|
56
|
+
handleJobsAfterBulkUpdate = async (options) => {
|
|
57
|
+
var _a;
|
|
58
|
+
if (((_a = options.attributes) == null ? void 0 : _a.status) !== import_plugin_workflow.JOB_STATUS.ABORTED) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const JobModel = this.db.getModel("jobs");
|
|
62
|
+
const jobs = await JobModel.findAll({
|
|
63
|
+
attributes: ["id", "executionId"],
|
|
64
|
+
where: options.where,
|
|
65
|
+
include: [
|
|
66
|
+
{
|
|
67
|
+
association: "node",
|
|
68
|
+
attributes: [],
|
|
69
|
+
where: {
|
|
70
|
+
type: import_constants.SCRIPT_INSTRUCTION_TYPE
|
|
71
|
+
},
|
|
72
|
+
required: true
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
association: "execution",
|
|
76
|
+
attributes: ["reason"],
|
|
77
|
+
required: false
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
transaction: options.transaction
|
|
81
|
+
});
|
|
82
|
+
const abortJobs = async () => {
|
|
83
|
+
var _a2;
|
|
84
|
+
for (const job of jobs) {
|
|
85
|
+
try {
|
|
86
|
+
await this.runningJobs.abortAcrossInstances(this.workflowPlugin, {
|
|
87
|
+
type: import_RunningJobs.ABORT_JAVASCRIPT_JOB_SYNC_MESSAGE_TYPE,
|
|
88
|
+
jobId: job.id,
|
|
89
|
+
executionId: job.executionId,
|
|
90
|
+
reason: ((_a2 = job.execution) == null ? void 0 : _a2.reason) ?? void 0
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.workflowPlugin.getLogger("javascript").error(`aborting JavaScript job (${job.id}) failed`, { error });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
if (options.transaction) {
|
|
98
|
+
options.transaction.afterCommit(abortJobs);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await abortJobs();
|
|
102
|
+
};
|
|
47
103
|
async afterAdd() {
|
|
48
104
|
}
|
|
49
105
|
async beforeLoad() {
|
|
50
106
|
}
|
|
51
107
|
async load() {
|
|
52
|
-
|
|
53
|
-
|
|
108
|
+
this.workflowPlugin = this.app.pm.get(import_plugin_workflow.default);
|
|
109
|
+
this.taskConsumer = new import_TaskConsumer.TaskConsumer(this.workflowPlugin, this.runningJobs);
|
|
110
|
+
this.taskRecovery = new import_TaskRecovery.TaskRecovery(this.workflowPlugin, import_constants2.PENDING_JAVASCRIPT_TASK_CHANNEL);
|
|
111
|
+
this.workflowPlugin.registerInstruction(
|
|
112
|
+
import_constants.SCRIPT_INSTRUCTION_TYPE,
|
|
113
|
+
new import_ScriptInstruction.default(this.workflowPlugin, this.runningJobs)
|
|
114
|
+
);
|
|
115
|
+
this.db.on("jobs.afterBulkUpdate", this.handleJobsAfterBulkUpdate);
|
|
116
|
+
const workerConcurrency = Number.parseInt(process.env.WORKFLOW_SCRIPT_WORKER_CONCURRENCY, 10);
|
|
117
|
+
this.app.eventQueue.subscribe(import_constants2.PENDING_JAVASCRIPT_TASK_CHANNEL, {
|
|
118
|
+
concurrency: Number.isInteger(workerConcurrency) && workerConcurrency >= 0 ? workerConcurrency : 0,
|
|
119
|
+
idle: () => this.taskConsumer.idle(),
|
|
120
|
+
process: this.taskConsumer.process
|
|
121
|
+
});
|
|
122
|
+
this.app.on("afterStart", () => {
|
|
123
|
+
this.taskConsumer.setReady(true);
|
|
124
|
+
this.taskRecovery.start();
|
|
125
|
+
});
|
|
126
|
+
this.app.on("beforeStop", async () => {
|
|
127
|
+
this.taskConsumer.setReady(false);
|
|
128
|
+
await this.taskConsumer.beforeStop();
|
|
129
|
+
await this.taskRecovery.stop();
|
|
130
|
+
this.db.off("jobs.afterBulkUpdate", this.handleJobsAfterBulkUpdate);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async handleSyncMessage(message) {
|
|
134
|
+
if (typeof message !== "object" || message === null) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const { type, jobId, reason } = message;
|
|
138
|
+
if (type !== import_RunningJobs.ABORT_JAVASCRIPT_JOB_SYNC_MESSAGE_TYPE) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (typeof jobId === "string" || typeof jobId === "number") {
|
|
142
|
+
this.runningJobs.abortJob(jobId, typeof reason === "string" ? reason : void 0);
|
|
143
|
+
}
|
|
54
144
|
}
|
|
55
145
|
async install() {
|
|
56
146
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/plugin-workflow-javascript",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.8",
|
|
4
4
|
"displayName": "Workflow: JavaScript",
|
|
5
5
|
"displayName.zh-CN": "工作流:JavaScript 节点",
|
|
6
6
|
"description": "Execute a piece of JavaScript in an isolated Node.js environment.",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"Workflow"
|
|
36
36
|
],
|
|
37
37
|
"license": "Apache-2.0",
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "ce017f3deb8b2414c6b13818042c4187270d1d8a"
|
|
39
39
|
}
|