@nocobase/plugin-workflow 2.1.0-beta.36 → 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.
Files changed (71) hide show
  1. package/dist/client/618.19af7f84261c815d.js +10 -0
  2. package/dist/client/964.ffbf5b47ed12bbdc.js +10 -0
  3. package/dist/client/Branch.d.ts +7 -3
  4. package/dist/client/BranchContext.d.ts +18 -0
  5. package/dist/client/components/TimeoutInput.d.ts +11 -0
  6. package/dist/client/constants.d.ts +13 -0
  7. package/dist/client/index.js +1 -1
  8. package/dist/client/nodes/index.d.ts +2 -0
  9. package/dist/client/schemas/executions.d.ts +42 -0
  10. package/dist/client/utils.d.ts +17 -0
  11. package/dist/common/collections/executions.d.ts +42 -0
  12. package/dist/common/collections/executions.js +50 -1
  13. package/dist/common/collections/workflows.d.ts +6 -2
  14. package/dist/common/collections/workflows.js +3 -1
  15. package/dist/common/constants.d.ts +5 -0
  16. package/dist/common/constants.js +7 -0
  17. package/dist/externalVersion.js +12 -12
  18. package/dist/locale/de-DE.json +4 -0
  19. package/dist/locale/en-US.json +7 -0
  20. package/dist/locale/es-ES.json +4 -0
  21. package/dist/locale/fr-FR.json +4 -0
  22. package/dist/locale/hu-HU.json +7 -3
  23. package/dist/locale/id-ID.json +4 -0
  24. package/dist/locale/it-IT.json +4 -0
  25. package/dist/locale/ja-JP.json +5 -1
  26. package/dist/locale/ko-KR.json +4 -0
  27. package/dist/locale/nl-NL.json +7 -3
  28. package/dist/locale/pt-BR.json +4 -0
  29. package/dist/locale/ru-RU.json +4 -0
  30. package/dist/locale/tr-TR.json +4 -0
  31. package/dist/locale/uk-UA.json +7 -3
  32. package/dist/locale/vi-VN.json +7 -3
  33. package/dist/locale/zh-CN.json +8 -0
  34. package/dist/locale/zh-TW.json +7 -3
  35. package/dist/node_modules/cron-parser/package.json +1 -1
  36. package/dist/node_modules/joi/package.json +1 -1
  37. package/dist/node_modules/lru-cache/package.json +1 -1
  38. package/dist/node_modules/nodejs-snowflake/package.json +1 -1
  39. package/dist/server/Dispatcher.d.ts +9 -7
  40. package/dist/server/Dispatcher.js +190 -122
  41. package/dist/server/ExecutionTimeoutManager.d.ts +45 -0
  42. package/dist/server/ExecutionTimeoutManager.js +312 -0
  43. package/dist/server/Plugin.d.ts +12 -0
  44. package/dist/server/Plugin.js +21 -1
  45. package/dist/server/Processor.d.ts +65 -9
  46. package/dist/server/Processor.js +285 -33
  47. package/dist/server/RunningExecutionRegistry.d.ts +18 -0
  48. package/dist/server/RunningExecutionRegistry.js +48 -0
  49. package/dist/server/actions/executions.d.ts +4 -3
  50. package/dist/server/actions/executions.js +42 -21
  51. package/dist/server/actions/jobs.d.ts +2 -1
  52. package/dist/server/actions/jobs.js +28 -1
  53. package/dist/server/constants.d.ts +2 -0
  54. package/dist/server/constants.js +3 -0
  55. package/dist/server/index.d.ts +2 -0
  56. package/dist/server/index.js +2 -0
  57. package/dist/server/instructions/index.d.ts +10 -3
  58. package/dist/server/migrations/20260501120000-workflow-timeout.d.ts +13 -0
  59. package/dist/server/migrations/20260501120000-workflow-timeout.js +63 -0
  60. package/dist/server/timeout-errors.d.ts +13 -0
  61. package/dist/server/timeout-errors.js +47 -0
  62. package/dist/server/types/Execution.d.ts +6 -0
  63. package/dist/server/types/Job.d.ts +3 -3
  64. package/dist/server/types/Workflow.d.ts +6 -1
  65. package/dist/server/utils.d.ts +11 -1
  66. package/dist/server/utils.js +92 -5
  67. package/dist/swagger/index.d.ts +22 -0
  68. package/dist/swagger/index.js +22 -0
  69. package/package.json +2 -2
  70. package/dist/client/261.7722d7400942730e.js +0 -10
  71. package/dist/client/964.6251d37b35710747.js +0 -10
@@ -45,6 +45,7 @@ var import_evaluators = require("@nocobase/evaluators");
45
45
  var import_utils = require("@nocobase/utils");
46
46
  var import_set = __toESM(require("lodash/set"));
47
47
  var import_constants = require("./constants");
48
+ var import_timeout_errors = require("./timeout-errors");
48
49
  class Processor {
49
50
  constructor(execution, options) {
50
51
  this.execution = execution;
@@ -66,11 +67,11 @@ class Processor {
66
67
  /**
67
68
  * @experimental
68
69
  */
69
- transaction;
70
+ transaction = null;
70
71
  /**
71
72
  * @experimental
72
73
  */
73
- mainTransaction;
74
+ mainTransaction = null;
74
75
  /**
75
76
  * @experimental
76
77
  */
@@ -82,10 +83,102 @@ class Processor {
82
83
  jobsMapByNodeKey = {};
83
84
  jobResultsMapByNodeKey = {};
84
85
  jobsToSave = /* @__PURE__ */ new Map();
86
+ rerunContext = null;
85
87
  /**
86
88
  * @experimental
87
89
  */
88
90
  lastSavedJob = null;
91
+ abortController = new AbortController();
92
+ timeoutGuard = null;
93
+ runningRegistered = false;
94
+ abortReason = null;
95
+ aborted = false;
96
+ get abortSignal() {
97
+ return this.abortController.signal;
98
+ }
99
+ setTimeoutGuard(ms) {
100
+ if (this.timeoutGuard) {
101
+ clearTimeout(this.timeoutGuard);
102
+ }
103
+ this.timeoutGuard = setTimeout(() => {
104
+ this.abortExecution(import_constants.EXECUTION_REASON.TIMEOUT);
105
+ }, ms);
106
+ }
107
+ abortExecution(reason) {
108
+ this.aborted = true;
109
+ this.abortReason = reason ?? null;
110
+ if (!this.abortController.signal.aborted) {
111
+ this.abortController.abort(
112
+ reason === import_constants.EXECUTION_REASON.TIMEOUT ? new import_timeout_errors.WorkflowTimeoutError("Workflow execution has been aborted") : new Error("Workflow execution has been aborted")
113
+ );
114
+ }
115
+ }
116
+ isTimeoutAborted() {
117
+ return this.abortSignal.aborted;
118
+ }
119
+ /**
120
+ * Create an independent abort handle for background work that outlives this processor's
121
+ * run loop (e.g. fire-and-forget instructions that resume the job later). It mirrors the
122
+ * current abort state and sets its own timer based on the execution's `expiresAt`, so the
123
+ * timeout still applies after the processor has exited its synchronous run.
124
+ *
125
+ * The caller must invoke `dispose()` once the background work settles to release the timer
126
+ * and the abort listener.
127
+ */
128
+ createBackgroundAbortHandle() {
129
+ const controller = new AbortController();
130
+ const sourceSignal = this.abortSignal;
131
+ let timeoutGuard = null;
132
+ let sourceListener = null;
133
+ const abort = (reason) => {
134
+ if (!controller.signal.aborted) {
135
+ controller.abort((0, import_timeout_errors.isWorkflowTimeoutError)(reason) ? reason : new import_timeout_errors.WorkflowTimeoutError());
136
+ }
137
+ };
138
+ if (sourceSignal.aborted) {
139
+ abort(sourceSignal.reason);
140
+ } else {
141
+ sourceListener = () => abort(sourceSignal.reason);
142
+ sourceSignal.addEventListener("abort", sourceListener, { once: true });
143
+ }
144
+ const remaining = this.execution.expiresAt ? this.execution.expiresAt.getTime() - Date.now() : null;
145
+ if (remaining != null) {
146
+ if (remaining <= 0) {
147
+ abort();
148
+ } else {
149
+ timeoutGuard = setTimeout(abort, remaining);
150
+ }
151
+ }
152
+ return {
153
+ signal: controller.signal,
154
+ dispose: () => {
155
+ if (timeoutGuard) {
156
+ clearTimeout(timeoutGuard);
157
+ timeoutGuard = null;
158
+ }
159
+ if (sourceListener) {
160
+ sourceSignal.removeEventListener("abort", sourceListener);
161
+ sourceListener = null;
162
+ }
163
+ },
164
+ throwIfAborted: () => {
165
+ if (controller.signal.aborted) {
166
+ throw controller.signal.reason ?? new import_timeout_errors.WorkflowTimeoutError();
167
+ }
168
+ }
169
+ };
170
+ }
171
+ /**
172
+ * Reload a job and return it only when it is still pending, otherwise `null`. Background
173
+ * work uses this before resuming so it never overwrites a job that another path (timeout
174
+ * abort, a competing resume) has already settled.
175
+ */
176
+ async findPendingJob(jobId) {
177
+ const job = await this.options.plugin.db.getRepository("jobs").findOne({
178
+ filterByTk: jobId
179
+ });
180
+ return (job == null ? void 0 : job.status) === import_constants.JOB_STATUS.PENDING ? job : null;
181
+ }
89
182
  // make dual linked nodes list then cache
90
183
  makeNodes(nodes = []) {
91
184
  this.nodes = nodes;
@@ -122,6 +215,9 @@ class Processor {
122
215
  if (!execution.workflow) {
123
216
  execution.workflow = plugin.enabledCache.get(execution.workflowId) || await execution.getWorkflow({ transaction });
124
217
  }
218
+ if (!execution.workflow) {
219
+ throw new Error(`workflow (#${execution.workflowId}) not found for execution (#${execution.id})`);
220
+ }
125
221
  const nodes = execution.workflow.nodes || await execution.workflow.getNodes({ transaction });
126
222
  execution.workflow.nodes = nodes;
127
223
  this.makeNodes(nodes);
@@ -147,37 +243,117 @@ class Processor {
147
243
  }
148
244
  async start() {
149
245
  const { execution } = this;
150
- if (execution.status) {
246
+ if (!await this.shouldContinueExecution()) {
151
247
  this.logger.warn(`execution was ended with status ${execution.status} before, could not be started again`, {
152
248
  workflowId: execution.workflowId
153
249
  });
154
250
  return;
155
251
  }
156
- await this.prepare();
157
- if (this.nodes.length) {
158
- const head = this.nodes.find((item) => !item.upstream);
159
- await this.run(head, { result: execution.context });
160
- } else {
161
- await this.exit(import_constants.JOB_STATUS.RESOLVED);
252
+ this.enterRunningState();
253
+ try {
254
+ await this.prepare();
255
+ if (this.nodes.length) {
256
+ const head = this.nodes.find((item) => !item.upstream);
257
+ if (!head) {
258
+ this.logger.warn(`head node not found for workflow (${execution.workflowId}), could not be started`, {
259
+ workflowId: execution.workflowId
260
+ });
261
+ return this.exit(import_constants.JOB_STATUS.ERROR);
262
+ }
263
+ await this.run(head);
264
+ } else {
265
+ await this.exit(import_constants.JOB_STATUS.RESOLVED);
266
+ }
267
+ } finally {
268
+ this.leaveRunningState();
162
269
  }
163
270
  }
164
271
  async resume(job) {
165
272
  const { execution } = this;
166
- if (execution.status) {
273
+ if (!await this.shouldContinueExecution()) {
167
274
  this.logger.warn(`execution was ended with status ${execution.status} before, could not be resumed`, {
168
275
  workflowId: execution.workflowId
169
276
  });
170
277
  return;
171
278
  }
172
- await this.prepare();
173
- const node = this.nodesMap.get(job.nodeId);
174
- await this.recall(node, job);
279
+ this.enterRunningState();
280
+ try {
281
+ await this.prepare();
282
+ const node = this.nodesMap.get(job.nodeId);
283
+ await this.recall(node, job);
284
+ } finally {
285
+ this.leaveRunningState();
286
+ }
287
+ }
288
+ resolveRerun(options = {}) {
289
+ const node = this.getRerunNode(options.nodeId);
290
+ const targetJob = this.jobsMapByNodeKey[node.key];
291
+ if (options.nodeId != null && !targetJob) {
292
+ throw new Error(`job of node (#${node.id}) not found in execution (#${this.execution.id})`);
293
+ }
294
+ if (options.nodeId == null && options.overwrite && !targetJob) {
295
+ throw new Error(`job of head node (#${node.id}) not found in execution (#${this.execution.id})`);
296
+ }
297
+ const input = this.getRerunInput(node);
298
+ return { node, input, targetJob };
299
+ }
300
+ async rerun(options = {}) {
301
+ const { execution } = this;
302
+ if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
303
+ throw new Error(`execution (#${execution.id}) is not started`);
304
+ }
305
+ if (!await this.shouldContinueExecution()) {
306
+ this.logger.warn(`execution was ended with status ${execution.status} before, could not be rerun`, {
307
+ workflowId: execution.workflowId
308
+ });
309
+ return;
310
+ }
311
+ this.enterRunningState();
312
+ try {
313
+ await this.prepare();
314
+ const { node, input, targetJob } = this.resolveRerun(options);
315
+ this.rerunContext = {
316
+ overwrite: options.overwrite === true,
317
+ targetJob
318
+ };
319
+ return await this.run(node, input, { rerun: true });
320
+ } finally {
321
+ this.rerunContext = null;
322
+ this.leaveRunningState();
323
+ }
175
324
  }
176
- async exec(instruction, node, prevJob) {
325
+ getRerunNode(nodeId) {
326
+ if (nodeId != null) {
327
+ const node = this.nodesMap.get(nodeId) || this.nodes.find((item) => String(item.id) === String(nodeId));
328
+ if (!node) {
329
+ throw new Error(`node (#${nodeId}) not found in workflow (#${this.execution.workflowId})`);
330
+ }
331
+ return node;
332
+ }
333
+ const head = this.nodes.find((item) => !item.upstream);
334
+ if (!head) {
335
+ throw new Error(`head node not found in workflow (#${this.execution.workflowId})`);
336
+ }
337
+ return head;
338
+ }
339
+ getRerunInput(node) {
340
+ if (!node.upstream) {
341
+ return { result: this.execution.context };
342
+ }
343
+ const upstreamJob = this.jobsMapByNodeKey[node.upstream.key];
344
+ if (!upstreamJob) {
345
+ throw new Error(`upstream job of node (#${node.id}) not found in execution (#${this.execution.id})`);
346
+ }
347
+ return upstreamJob;
348
+ }
349
+ async exec(instruction, node, prevJob, options = {}) {
177
350
  let job;
351
+ if (!await this.shouldContinueExecution()) {
352
+ return this.exit();
353
+ }
178
354
  try {
179
355
  this.logger.debug(`config of node`, { data: node.config, workflowId: node.workflowId });
180
- job = await instruction(node, prevJob, this);
356
+ job = await instruction(node, prevJob, this, { ...options, signal: this.abortSignal });
181
357
  if (job === null) {
182
358
  return this.exit();
183
359
  }
@@ -185,18 +361,27 @@ class Processor {
185
361
  return this.exit(true);
186
362
  }
187
363
  } catch (err) {
188
- this.logger.error(
189
- `execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) failed: `,
190
- { error: err, workflowId: node.workflowId }
191
- );
192
- job = {
193
- result: err instanceof Error ? {
194
- message: err.message,
195
- ...err
196
- } : err,
197
- status: import_constants.JOB_STATUS.ERROR
198
- };
199
- if (prevJob && prevJob.nodeId === node.id) {
364
+ if ((0, import_timeout_errors.isWorkflowTimeoutError)(err) || this.abortSignal.aborted && this.aborted) {
365
+ job = {
366
+ result: {
367
+ message: err.message
368
+ },
369
+ status: import_constants.JOB_STATUS.ABORTED
370
+ };
371
+ } else {
372
+ this.logger.error(
373
+ `execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) failed: `,
374
+ { error: err, workflowId: node.workflowId }
375
+ );
376
+ job = {
377
+ result: err instanceof Error ? {
378
+ ...err,
379
+ message: err.message
380
+ } : err,
381
+ status: import_constants.JOB_STATUS.ERROR
382
+ };
383
+ }
384
+ if (prevJob instanceof import_database.Model && prevJob.nodeId === node.id) {
200
385
  prevJob.set(job);
201
386
  job = prevJob;
202
387
  }
@@ -213,13 +398,16 @@ class Processor {
213
398
  }
214
399
  );
215
400
  this.logger.debug(`result of node`, { data: savedJob.result });
401
+ if (this.execution.status === import_constants.EXECUTION_STATUS.ABORTED || this.isTimeoutAborted()) {
402
+ return this.exit(import_constants.JOB_STATUS.ABORTED);
403
+ }
216
404
  if (savedJob.status === import_constants.JOB_STATUS.RESOLVED && node.downstream) {
217
405
  this.logger.debug(`run next node (${node.downstreamId})`);
218
406
  return this.run(node.downstream, savedJob);
219
407
  }
220
408
  return this.end(node, savedJob);
221
409
  }
222
- async run(node, input) {
410
+ async run(node, input, options) {
223
411
  const { instructions } = this.options.plugin;
224
412
  const instruction = instructions.get(node.type);
225
413
  if (!instruction) {
@@ -231,7 +419,7 @@ class Processor {
231
419
  this.logger.info(`execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id})`, {
232
420
  workflowId: node.workflowId
233
421
  });
234
- return this.exec(instruction.run.bind(instruction), node, input);
422
+ return this.exec(instruction.run.bind(instruction), node, input, options);
235
423
  }
236
424
  // parent node should take over the control
237
425
  async end(node, job) {
@@ -263,6 +451,7 @@ class Processor {
263
451
  return this.exec(instruction.resume.bind(instruction), node, job);
264
452
  }
265
453
  async exit(s) {
454
+ this.leaveRunningState();
266
455
  if (s === true) {
267
456
  return;
268
457
  }
@@ -311,11 +500,34 @@ class Processor {
311
500
  }
312
501
  if (typeof s === "number") {
313
502
  const status = this.constructor.StatusMap[s] ?? Math.sign(s);
314
- await this.execution.update({ status }, { transaction: this.mainTransaction });
503
+ const values = { status };
504
+ if (status === import_constants.EXECUTION_STATUS.ABORTED && this.abortReason) {
505
+ values.reason = this.abortReason;
506
+ }
507
+ const ExecutionModelClass = this.options.plugin.db.getModel("executions");
508
+ const [affected] = await ExecutionModelClass.update(values, {
509
+ where: {
510
+ id: this.execution.id,
511
+ status: import_constants.EXECUTION_STATUS.STARTED
512
+ },
513
+ individualHooks: true,
514
+ transaction: this.mainTransaction
515
+ });
516
+ if (affected) {
517
+ this.execution.set(values);
518
+ } else {
519
+ await this.execution.reload({ transaction: this.mainTransaction });
520
+ }
315
521
  }
316
522
  if (this.mainTransaction && this.mainTransaction !== this.transaction) {
317
523
  await this.mainTransaction.commit();
318
524
  }
525
+ if (this.execution.status === import_constants.EXECUTION_STATUS.STARTED) {
526
+ this.options.plugin.timeoutManager.scheduleExecutionTimeout(this.execution);
527
+ } else {
528
+ this.options.plugin.timeoutManager.clear(this.execution.id);
529
+ this.options.plugin.timeoutManager.invalidateNextExpiresAtIfMatches(this.execution.expiresAt);
530
+ }
319
531
  this.logger.info(`execution (${this.execution.id}) exiting with status ${this.execution.status}`, {
320
532
  workflowId: this.execution.workflowId
321
533
  });
@@ -325,12 +537,21 @@ class Processor {
325
537
  * @experimental
326
538
  */
327
539
  saveJob(payload) {
540
+ var _a;
328
541
  const { database } = this.execution.constructor;
329
- const { model } = database.getCollection("jobs");
542
+ const model = database.getModel("jobs");
330
543
  let job;
331
544
  if (payload instanceof model) {
332
545
  job = payload;
333
546
  job.set("updatedAt", /* @__PURE__ */ new Date());
547
+ } else if (((_a = this.rerunContext) == null ? void 0 : _a.overwrite) && this.rerunContext.targetJob && this.rerunContext.targetJob.nodeId === payload.nodeId) {
548
+ job = this.rerunContext.targetJob;
549
+ job.set({
550
+ status: payload.status,
551
+ result: Object.prototype.hasOwnProperty.call(payload, "result") ? payload.result : null,
552
+ meta: Object.prototype.hasOwnProperty.call(payload, "meta") ? payload.meta : null,
553
+ updatedAt: /* @__PURE__ */ new Date()
554
+ });
334
555
  } else {
335
556
  job = model.build(
336
557
  {
@@ -345,7 +566,7 @@ class Processor {
345
566
  }
346
567
  );
347
568
  }
348
- this.jobsToSave.set(job.id, job);
569
+ this.jobsToSave.set(job.id.toString(), job);
349
570
  this.lastSavedJob = job;
350
571
  this.jobsMapByNodeKey[job.nodeKey] = job;
351
572
  this.jobResultsMapByNodeKey[job.nodeKey] = job.result;
@@ -360,6 +581,37 @@ class Processor {
360
581
  getBranches(node) {
361
582
  return this.nodes.filter((item) => item.upstream === node && item.branchIndex !== null).sort((a, b) => Number(a.branchIndex) - Number(b.branchIndex));
362
583
  }
584
+ enterRunningState() {
585
+ this.options.plugin.timeoutManager.clear(this.execution.id);
586
+ this.abortReason = null;
587
+ this.aborted = false;
588
+ this.options.plugin.registerRunningExecution(this.execution.id, (reason) => this.abortExecution(reason));
589
+ this.runningRegistered = true;
590
+ const remaining = this.execution.expiresAt ? this.execution.expiresAt.getTime() - Date.now() : null;
591
+ if (remaining == null) {
592
+ return;
593
+ }
594
+ if (remaining <= 0) {
595
+ this.abortExecution(import_constants.EXECUTION_REASON.TIMEOUT);
596
+ return;
597
+ }
598
+ this.setTimeoutGuard(remaining);
599
+ }
600
+ async shouldContinueExecution() {
601
+ const transaction = this.mainTransaction ?? this.transaction;
602
+ return this.options.plugin.timeoutManager.shouldContinue(this.execution, { transaction });
603
+ }
604
+ leaveRunningState() {
605
+ if (this.timeoutGuard) {
606
+ clearTimeout(this.timeoutGuard);
607
+ this.timeoutGuard = null;
608
+ }
609
+ if (!this.runningRegistered) {
610
+ return;
611
+ }
612
+ this.options.plugin.unregisterRunningExecution(this.execution.id);
613
+ this.runningRegistered = false;
614
+ }
363
615
  /**
364
616
  * @experimental
365
617
  * find the first node in current branch
@@ -423,7 +675,7 @@ class Processor {
423
675
  * @experimental
424
676
  */
425
677
  getScope(sourceNodeId, includeSelfScope = false) {
426
- const node = this.nodesMap.get(sourceNodeId);
678
+ const node = sourceNodeId ? this.nodesMap.get(sourceNodeId) : void 0;
427
679
  const systemFns = {};
428
680
  const scope = {
429
681
  execution: this.execution,
@@ -0,0 +1,18 @@
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
+ type AbortHandler = {
10
+ abort(reason?: string): void;
11
+ };
12
+ export default class RunningExecutionRegistry {
13
+ private readonly executions;
14
+ register(executionId: number | string, handler: AbortHandler): void;
15
+ unregister(executionId: number | string): void;
16
+ abort(executionId: number | string, reason?: string): boolean;
17
+ }
18
+ export {};
@@ -0,0 +1,48 @@
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 RunningExecutionRegistry_exports = {};
28
+ __export(RunningExecutionRegistry_exports, {
29
+ default: () => RunningExecutionRegistry
30
+ });
31
+ module.exports = __toCommonJS(RunningExecutionRegistry_exports);
32
+ class RunningExecutionRegistry {
33
+ executions = /* @__PURE__ */ new Map();
34
+ register(executionId, handler) {
35
+ this.executions.set(String(executionId), handler);
36
+ }
37
+ unregister(executionId) {
38
+ this.executions.delete(String(executionId));
39
+ }
40
+ abort(executionId, reason) {
41
+ const handler = this.executions.get(String(executionId));
42
+ if (!handler) {
43
+ return false;
44
+ }
45
+ handler.abort(reason);
46
+ return true;
47
+ }
48
+ }
@@ -6,6 +6,7 @@
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
- import { Context } from '@nocobase/actions';
10
- export declare function destroy(context: Context, next: any): Promise<void>;
11
- export declare function cancel(context: Context, next: any): Promise<never>;
9
+ import { Context, Next } from '@nocobase/actions';
10
+ export declare function destroy(context: Context, next: Next): Promise<void>;
11
+ export declare function cancel(context: Context, next: Next): Promise<never>;
12
+ export declare function rerun(context: Context, next: Next): Promise<never>;
@@ -37,12 +37,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
37
37
  var executions_exports = {};
38
38
  __export(executions_exports, {
39
39
  cancel: () => cancel,
40
- destroy: () => destroy
40
+ destroy: () => destroy,
41
+ rerun: () => rerun
41
42
  });
42
43
  module.exports = __toCommonJS(executions_exports);
43
44
  var import_actions = __toESM(require("@nocobase/actions"));
44
45
  var import_database = require("@nocobase/database");
46
+ var import_Plugin = __toESM(require("../Plugin"));
45
47
  var import_constants = require("../constants");
48
+ var import_utils = require("../utils");
46
49
  async function destroy(context, next) {
47
50
  context.action.mergeParams({
48
51
  filter: {
@@ -55,8 +58,8 @@ async function destroy(context, next) {
55
58
  }
56
59
  async function cancel(context, next) {
57
60
  const { filterByTk } = context.action.params;
61
+ const workflowPlugin = context.app.pm.get(import_Plugin.default);
58
62
  const ExecutionRepo = context.db.getRepository("executions");
59
- const JobRepo = context.db.getRepository("jobs");
60
63
  const execution = await ExecutionRepo.findOne({
61
64
  filterByTk,
62
65
  appends: ["jobs"]
@@ -67,30 +70,48 @@ async function cancel(context, next) {
67
70
  if (execution.status) {
68
71
  return context.throw(400);
69
72
  }
70
- await context.db.sequelize.transaction(async (transaction) => {
71
- await execution.update(
72
- {
73
- status: import_constants.EXECUTION_STATUS.ABORTED
74
- },
75
- { transaction }
76
- );
77
- const pendingJobs = execution.jobs.filter((job) => job.status === import_constants.JOB_STATUS.PENDING);
78
- await JobRepo.update({
79
- values: {
80
- status: import_constants.JOB_STATUS.ABORTED
81
- },
82
- filter: {
83
- id: pendingJobs.map((job) => job.id)
84
- },
85
- individualHooks: false,
86
- transaction
87
- });
73
+ try {
74
+ const lock = await context.app.lockManager.tryAcquire((0, import_utils.getExecutionLockKey)(execution.id));
75
+ await lock.runExclusive(async () => {
76
+ await (0, import_utils.abortExecution)(workflowPlugin, execution, { reason: import_constants.EXECUTION_REASON.MANUAL_CANCEL });
77
+ }, 6e4);
78
+ } catch (error) {
79
+ if ((0, import_utils.isLockAcquireError)(error)) {
80
+ return context.throw(409, "Execution is being processed");
81
+ }
82
+ throw error;
83
+ }
84
+ context.body = execution;
85
+ await next();
86
+ }
87
+ async function rerun(context, next) {
88
+ const workflowPlugin = context.app.pm.get(import_Plugin.default);
89
+ const { filterByTk, values = {} } = context.action.params;
90
+ const { nodeId, overwrite } = values;
91
+ const ExecutionRepo = context.db.getRepository("executions");
92
+ const execution = await ExecutionRepo.findOne({
93
+ filterByTk
94
+ });
95
+ if (!execution) {
96
+ return context.throw(404);
97
+ }
98
+ if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
99
+ return context.throw(400, "Only started executions can be rerun");
100
+ }
101
+ await workflowPlugin.run({
102
+ execution,
103
+ rerun: {
104
+ nodeId,
105
+ overwrite: overwrite === true
106
+ }
88
107
  });
89
108
  context.body = execution;
109
+ context.status = 202;
90
110
  await next();
91
111
  }
92
112
  // Annotate the CommonJS export names for ESM import in node:
93
113
  0 && (module.exports = {
94
114
  cancel,
95
- destroy
115
+ destroy,
116
+ rerun
96
117
  });
@@ -6,4 +6,5 @@
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
- export declare function resume(context: any, next: any): Promise<any>;
9
+ import { Context, Next } from '@nocobase/actions';
10
+ export declare function resume(context: Context, next: Next): Promise<never>;
@@ -40,7 +40,10 @@ __export(jobs_exports, {
40
40
  });
41
41
  module.exports = __toCommonJS(jobs_exports);
42
42
  var import_actions = require("@nocobase/actions");
43
+ var import_constants = require("../constants");
43
44
  var import_Plugin = __toESM(require("../Plugin"));
45
+ var import_constants2 = require("../../common/constants");
46
+ var import_utils = require("../utils");
44
47
  async function resume(context, next) {
45
48
  const repository = import_actions.utils.getRepositoryFromParams(context);
46
49
  const workflowPlugin = context.app.pm.get(import_Plugin.default);
@@ -51,11 +54,35 @@ async function resume(context, next) {
51
54
  if (!job) {
52
55
  return context.throw(404, "Job not found");
53
56
  }
57
+ if (!job.execution) {
58
+ job.execution = await job.getExecution();
59
+ }
54
60
  workflowPlugin.getLogger(job.workflowId).warn(`Resuming job #${job.id}...`);
55
- await job.update(values);
61
+ const execution = job.execution;
62
+ if (!execution) {
63
+ return context.throw(400, "Execution is not running");
64
+ }
65
+ if (await workflowPlugin.abortExecutionIfExpired(execution)) {
66
+ return context.throw(400, context.t("Execution timed out", { ns: import_constants2.NAMESPACE }));
67
+ }
68
+ if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
69
+ return context.throw(400, "Execution is not running");
70
+ }
71
+ try {
72
+ const lock = await context.app.lockManager.tryAcquire((0, import_utils.getExecutionLockKey)(job.execution.id));
73
+ await lock.runExclusive(async () => {
74
+ await job.update(values);
75
+ }, 6e4);
76
+ } catch (error) {
77
+ if ((0, import_utils.isLockAcquireError)(error)) {
78
+ return context.throw(409, "Execution is being processed");
79
+ }
80
+ throw error;
81
+ }
56
82
  context.body = job;
57
83
  context.status = 202;
58
84
  await next();
85
+ job.execution = execution;
59
86
  workflowPlugin.resume(job);
60
87
  }
61
88
  // Annotate the CommonJS export names for ESM import in node: