@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
@@ -33,18 +33,19 @@ var import_node_crypto = require("node:crypto");
33
33
  var import_sequelize = require("sequelize");
34
34
  var import_constants = require("./constants");
35
35
  var import_Plugin = require("./Plugin");
36
+ var import_utils = require("./utils");
36
37
  class Dispatcher {
37
38
  constructor(plugin) {
38
39
  this.plugin = plugin;
39
40
  }
40
41
  ready = false;
41
42
  executing = null;
42
- preparing = null;
43
+ saving = null;
43
44
  pending = [];
44
45
  events = [];
45
46
  eventsCount = 0;
46
47
  get idle() {
47
- return this.ready && !this.executing && !this.preparing && !this.pending.length && !this.events.length;
48
+ return this.ready && !this.executing && !this.saving && !this.pending.length && !this.events.length;
48
49
  }
49
50
  onQueueExecution = async (event) => {
50
51
  const ExecutionRepo = this.plugin.db.getRepository("executions");
@@ -52,11 +53,10 @@ class Dispatcher {
52
53
  filterByTk: event.executionId
53
54
  });
54
55
  if (!execution || execution.dispatched) {
55
- this.plugin.getLogger("dispatcher").info(`execution (${event.executionId}) from queue not found or not in queueing status, skip`);
56
56
  return;
57
57
  }
58
58
  this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) received from queue, adding to pending list`);
59
- this.run({ execution });
59
+ await this.run({ execution });
60
60
  };
61
61
  setReady(ready) {
62
62
  this.ready = ready;
@@ -96,13 +96,13 @@ class Dispatcher {
96
96
  this.eventsCount = this.events.length;
97
97
  logger.info(`new event triggered, now events: ${this.events.length}`);
98
98
  logger.debug(`event data:`, { context });
99
- this.prepare();
99
+ this.saveEvent();
100
100
  }
101
- prepare() {
102
- if (this.preparing) {
101
+ saveEvent() {
102
+ if (this.saving) {
103
103
  return;
104
104
  }
105
- this.preparing = (async () => {
105
+ this.saving = (async () => {
106
106
  try {
107
107
  while (this.events.length) {
108
108
  if (this.executing && this.plugin.db.options.dialect === "sqlite") {
@@ -115,7 +115,7 @@ class Dispatcher {
115
115
  logger.info(`preparing execution for event`);
116
116
  try {
117
117
  const execution = await this.createExecution(...event);
118
- if (!(execution == null ? void 0 : execution.dispatched)) {
118
+ if (!execution.dispatched) {
119
119
  if (this.plugin.serving() && !this.executing && !this.pending.length) {
120
120
  logger.info(`local pending list is empty, adding execution (${execution.id}) to pending list`);
121
121
  this.pending.push({ execution });
@@ -137,9 +137,9 @@ class Dispatcher {
137
137
  }
138
138
  }
139
139
  } finally {
140
- this.preparing = null;
140
+ this.saving = null;
141
141
  if (this.events.length) {
142
- this.prepare();
142
+ this.saveEvent();
143
143
  } else {
144
144
  this.dispatch();
145
145
  }
@@ -152,27 +152,27 @@ class Dispatcher {
152
152
  execution = await job.getExecution();
153
153
  }
154
154
  this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) resuming from job (${job.id}) added to pending list`);
155
- this.run({ execution, job, loaded: true });
155
+ await this.run({ execution, job });
156
156
  }
157
157
  async start(execution) {
158
158
  if (execution.status) {
159
159
  return;
160
160
  }
161
161
  this.plugin.getLogger(execution.workflowId).info(`starting deferred execution (${execution.id})`);
162
- this.run({ execution, loaded: true });
162
+ await this.run({ execution });
163
163
  }
164
164
  async beforeStop() {
165
165
  this.ready = false;
166
166
  this.plugin.getLogger("dispatcher").info("app is stopping, draining local queues...");
167
- while (this.preparing || this.executing || this.events.length || this.pending.length) {
168
- if (this.preparing) {
169
- await this.preparing;
167
+ while (this.saving || this.executing || this.events.length || this.pending.length) {
168
+ if (this.saving) {
169
+ await this.saving;
170
170
  }
171
171
  if (this.executing) {
172
172
  await this.executing;
173
173
  }
174
- if (this.events.length && !this.preparing) {
175
- this.prepare();
174
+ if (this.events.length && !this.saving) {
175
+ this.saveEvent();
176
176
  }
177
177
  if (this.pending.length && !this.executing) {
178
178
  this.dispatch();
@@ -190,33 +190,36 @@ class Dispatcher {
190
190
  return;
191
191
  }
192
192
  if (this.events.length) {
193
- this.prepare();
193
+ this.saveEvent();
194
194
  return;
195
195
  }
196
196
  this.executing = (async () => {
197
197
  let next = null;
198
- let execution = null;
199
- if (this.pending.length) {
200
- const pending = this.pending.shift();
201
- execution = pending.loaded ? pending.execution : await this.acquirePendingExecution(pending.execution);
198
+ const pending = this.pending.shift() ?? null;
199
+ if (pending || this.ready && this.plugin.serving()) {
200
+ const execution = await this.prepare((pending == null ? void 0 : pending.execution) ?? null, {
201
+ immediate: pending == null ? void 0 : pending.immediate
202
+ });
202
203
  if (execution) {
203
- next = [execution, pending.job];
204
+ next = [execution, pending == null ? void 0 : pending.job, pending == null ? void 0 : pending.rerun];
205
+ }
206
+ if (pending && next) {
204
207
  this.plugin.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
205
208
  }
206
209
  } else {
207
- if (this.ready && this.plugin.serving()) {
208
- execution = await this.acquireQueueingExecution();
209
- if (execution) {
210
- next = [execution];
211
- }
212
- } else {
213
- this.plugin.getLogger("dispatcher").warn(
214
- `${import_Plugin.WORKER_JOB_WORKFLOW_PROCESS} is not serving on this instance or app not ready, new dispatching will be ignored`
215
- );
216
- }
210
+ this.plugin.getLogger("dispatcher").warn(
211
+ `${import_Plugin.WORKER_JOB_WORKFLOW_PROCESS} is not serving on this instance or app not ready, new dispatching will be ignored`
212
+ );
217
213
  }
218
214
  if (next) {
219
- await this.process(...next);
215
+ try {
216
+ await this.process(next[0], next[1], { rerun: next[2] });
217
+ } catch (error) {
218
+ this.plugin.getLogger(next[0].workflowId).error(`execution (${next[0].id}) process failed`, { error });
219
+ if (pending && (0, import_utils.isLockAcquireError)(error)) {
220
+ this.pending.unshift({ ...pending, execution: next[0], immediate: true });
221
+ }
222
+ }
220
223
  }
221
224
  setImmediate(() => {
222
225
  this.executing = null;
@@ -228,7 +231,10 @@ class Dispatcher {
228
231
  })();
229
232
  }
230
233
  async run(pending) {
231
- this.pending.push(pending);
234
+ this.pending.push({
235
+ ...pending,
236
+ immediate: !this.executing && !this.pending.length && !this.saving && !this.events.length
237
+ });
232
238
  this.dispatch();
233
239
  }
234
240
  async triggerSync(workflow, context, { deferred, ...options } = {}) {
@@ -236,13 +242,23 @@ class Dispatcher {
236
242
  try {
237
243
  execution = await this.createExecution(workflow, context, options);
238
244
  } catch (err) {
239
- this.plugin.getLogger(workflow.id).error(`creating execution failed: ${err.message}`, err);
245
+ if (err instanceof Error) {
246
+ this.plugin.getLogger(workflow.id).error(`creating execution failed: ${err.message}`, err);
247
+ }
240
248
  return null;
241
249
  }
242
250
  try {
243
- return this.process(execution, null, options);
251
+ const entered = await this.prepare(execution, {
252
+ transaction: this.plugin.useDataSourceTransaction("main", options.transaction)
253
+ });
254
+ if (!entered) {
255
+ return null;
256
+ }
257
+ return this.process(entered, void 0, options);
244
258
  } catch (err) {
245
- this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err);
259
+ if (err instanceof Error) {
260
+ this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err);
261
+ }
246
262
  }
247
263
  return null;
248
264
  }
@@ -252,9 +268,9 @@ class Dispatcher {
252
268
  if (!triggerValid) {
253
269
  return false;
254
270
  }
255
- const { stack } = options;
271
+ const { stack = [] } = options;
256
272
  let valid = true;
257
- if ((stack == null ? void 0 : stack.length) > 0) {
273
+ if (stack == null ? void 0 : stack.length) {
258
274
  const existed = await workflow.countExecutions({
259
275
  where: {
260
276
  id: stack
@@ -264,7 +280,7 @@ class Dispatcher {
264
280
  const limitCount = workflow.options.stackLimit || 1;
265
281
  if (existed >= limitCount) {
266
282
  this.plugin.getLogger(workflow.id).warn(
267
- `workflow ${workflow.id} has already been triggered in stacks executions (${stack}), and max call coont is ${limitCount}, newly triggering will be skipped.`
283
+ `workflow ${workflow.id} has already been triggered in stacks executions (${stack}), and max call count is ${limitCount}, newly triggering will be skipped.`
268
284
  );
269
285
  valid = false;
270
286
  }
@@ -276,13 +292,21 @@ class Dispatcher {
276
292
  const { deferred } = options;
277
293
  const transaction = await this.plugin.useDataSourceTransaction("main", options.transaction, true);
278
294
  const sameTransaction = options.transaction === transaction;
279
- const valid = await this.validateEvent(workflow, context, { ...options, transaction });
295
+ let stack = options.stack;
296
+ if (options.parentExecutionId && !stack) {
297
+ const parentExecution = await this.plugin.db.getRepository("executions").findOne({
298
+ filterByTk: options.parentExecutionId,
299
+ transaction
300
+ });
301
+ stack = parentExecution ? [...parentExecution.stack ?? [], parentExecution.id] : [];
302
+ }
303
+ const valid = await this.validateEvent(workflow, context, { ...options, stack, transaction });
280
304
  if (!valid) {
281
305
  if (!sameTransaction) {
282
306
  await transaction.commit();
283
307
  }
284
308
  (_a = options.onTriggerFail) == null ? void 0 : _a.call(options, workflow, context, options);
285
- return Promise.reject(new Error("event is not valid"));
309
+ throw new Error("event is not valid");
286
310
  }
287
311
  let execution;
288
312
  try {
@@ -291,7 +315,8 @@ class Dispatcher {
291
315
  context,
292
316
  key: workflow.key,
293
317
  eventKey: options.eventKey ?? (0, import_node_crypto.randomUUID)(),
294
- stack: options.stack,
318
+ stack,
319
+ parentExecutionId: options.parentExecutionId ?? null,
295
320
  dispatched: deferred ?? false,
296
321
  status: deferred ? import_constants.EXECUTION_STATUS.STARTED : import_constants.EXECUTION_STATUS.QUEUEING,
297
322
  manually: options.manually
@@ -325,92 +350,135 @@ class Dispatcher {
325
350
  execution.workflow = workflow;
326
351
  return execution;
327
352
  }
328
- async acquirePendingExecution(execution) {
329
- const logger = this.plugin.getLogger(execution.workflowId);
330
- const isolationLevel = this.plugin.db.options.dialect === "sqlite" ? [][0] : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ;
331
- let fetched = execution;
353
+ async prepare(input, options = {}) {
354
+ const transaction = options.transaction;
355
+ const ownTransaction = !transaction;
356
+ const tx = transaction || await this.plugin.db.sequelize.transaction({
357
+ isolationLevel: this.plugin.db.options.dialect === "sqlite" ? void 0 : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ
358
+ });
359
+ const logger = input ? this.plugin.getLogger(input.workflowId) : this.plugin.getLogger("dispatcher");
332
360
  try {
333
- await this.plugin.db.sequelize.transaction({ isolationLevel }, async (transaction) => {
334
- const ExecutionModelClass = this.plugin.db.getModel("executions");
335
- const [affected] = await ExecutionModelClass.update(
336
- { dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED },
337
- {
338
- where: {
339
- id: execution.id,
340
- dispatched: false
341
- },
342
- transaction
343
- }
344
- );
345
- if (!affected) {
346
- fetched = null;
347
- return;
361
+ let execution = input;
362
+ if (execution) {
363
+ if (!options.immediate || execution.status !== import_constants.EXECUTION_STATUS.QUEUEING) {
364
+ await execution.reload({ transaction: tx });
348
365
  }
349
- await execution.reload({ transaction });
350
- });
351
- } catch (error) {
352
- logger.error(`acquiring pending execution failed: ${error.message}`, { error });
353
- }
354
- return fetched;
355
- }
356
- async acquireQueueingExecution() {
357
- const isolationLevel = this.plugin.db.options.dialect === "sqlite" ? [][0] : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ;
358
- let fetched = null;
359
- try {
360
- await this.plugin.db.sequelize.transaction(
361
- {
362
- isolationLevel
363
- },
364
- async (transaction) => {
365
- const execution = await this.plugin.db.getRepository("executions").findOne({
366
- filter: {
367
- dispatched: false,
368
- "workflow.enabled": true
369
- },
370
- sort: "id",
371
- transaction
372
- });
373
- if (execution) {
374
- this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) fetched from db`);
375
- await execution.update(
376
- {
377
- dispatched: true,
378
- status: import_constants.EXECUTION_STATUS.STARTED
379
- },
380
- { transaction }
381
- );
382
- execution.workflow = this.plugin.enabledCache.get(execution.workflowId);
383
- fetched = execution;
384
- } else {
385
- this.plugin.getLogger("dispatcher").debug(`no execution in db queued to process`);
386
- }
366
+ } else {
367
+ execution = await this.plugin.db.getRepository("executions").findOne({
368
+ filter: {
369
+ dispatched: false,
370
+ "workflow.enabled": true
371
+ },
372
+ sort: "id",
373
+ transaction: tx,
374
+ lock: tx.LOCK.UPDATE,
375
+ skipLocked: true
376
+ });
377
+ if (execution) {
378
+ this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) fetched from db`);
379
+ } else {
380
+ this.plugin.getLogger("dispatcher").debug(`no execution in db queued to process`);
387
381
  }
388
- );
382
+ }
383
+ if (!execution) {
384
+ if (ownTransaction) {
385
+ await tx.commit();
386
+ }
387
+ return null;
388
+ }
389
+ const entered = await this.enter(execution, tx);
390
+ if (ownTransaction) {
391
+ await tx.commit();
392
+ }
393
+ return entered;
389
394
  } catch (error) {
390
- this.plugin.getLogger("dispatcher").error(`fetching execution from db failed: ${error.message}`, { error });
395
+ if (ownTransaction) {
396
+ await tx.rollback();
397
+ }
398
+ if (error instanceof Error) {
399
+ logger.error(`entering execution failed: ${error.message}`, { error });
400
+ }
401
+ return null;
391
402
  }
392
- return fetched;
393
403
  }
394
- async process(execution, job, options = {}) {
395
- var _a, _b;
396
- const logger = this.plugin.getLogger(execution.workflowId);
404
+ async enter(execution, transaction) {
405
+ const workflow = execution.workflow || this.plugin.enabledCache.get(execution.workflowId) || await execution.getWorkflow({ transaction });
406
+ if (!workflow) {
407
+ this.plugin.getLogger(execution.workflowId).warn(`workflow (${execution.workflowId}) not found for execution`, {
408
+ workflowId: execution.workflowId,
409
+ executionId: execution.id
410
+ });
411
+ }
412
+ if (execution.status && execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
413
+ return null;
414
+ }
415
+ if (execution.dispatched && execution.status === import_constants.EXECUTION_STATUS.STARTED && execution.startedAt) {
416
+ execution.workflow = workflow;
417
+ return execution;
418
+ }
419
+ const values = {
420
+ dispatched: true,
421
+ status: import_constants.EXECUTION_STATUS.STARTED
422
+ };
423
+ const where = {
424
+ id: execution.id,
425
+ status: execution.status ?? null
426
+ };
397
427
  if (!execution.dispatched) {
398
- const transaction = await this.plugin.useDataSourceTransaction("main", options.transaction);
399
- await execution.update({ dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED }, { transaction });
400
- logger.info(`execution (${execution.id}) from pending list updated to started`);
428
+ where.dispatched = false;
401
429
  }
402
- const processor = this.plugin.createProcessor(execution, options);
403
- logger.info(`execution (${execution.id}) ${job ? "resuming" : "starting"}...`);
404
- try {
405
- await (job ? processor.resume(job) : processor.start());
406
- logger.info(`execution (${execution.id}) finished with status: ${execution.status}`);
407
- logger.debug(`execution (${execution.id}) details:`, { execution });
408
- if (execution.status && ((_b = (_a = execution.workflow.options) == null ? void 0 : _a.deleteExecutionOnStatus) == null ? void 0 : _b.includes(execution.status))) {
409
- await execution.destroy({ transaction: processor.mainTransaction });
430
+ if (!execution.startedAt) {
431
+ const startedAt = /* @__PURE__ */ new Date();
432
+ values.startedAt = startedAt;
433
+ execution.workflow = workflow;
434
+ values.expiresAt = this.plugin.timeoutManager.getExpiresAt(execution, startedAt);
435
+ where.startedAt = null;
436
+ }
437
+ const ExecutionModelClass = this.plugin.db.getModel("executions");
438
+ const [affected] = await ExecutionModelClass.update(values, {
439
+ where,
440
+ transaction
441
+ });
442
+ if (!affected) {
443
+ return null;
444
+ }
445
+ await execution.reload({ transaction });
446
+ execution.workflow = workflow;
447
+ return execution;
448
+ }
449
+ async process(execution, job = null, options = {}) {
450
+ const { rerun, ...processorOptions } = options;
451
+ const logger = this.plugin.getLogger(execution.workflowId);
452
+ const run = async () => {
453
+ var _a, _b, _c;
454
+ if (!execution.dispatched) {
455
+ const transaction = await this.plugin.useDataSourceTransaction("main", processorOptions.transaction);
456
+ await execution.update({ dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED }, { transaction });
457
+ logger.info(`execution (${execution.id}) from pending list updated to started`);
410
458
  }
411
- } catch (err) {
412
- logger.error(`execution (${execution.id}) error: ${err.message}`, err);
459
+ this.plugin.timeoutManager.scheduleExecutionTimeout(execution);
460
+ const processor = this.plugin.createProcessor(execution, processorOptions);
461
+ logger.info(`execution (${execution.id}) ${rerun ? "rerunning" : job ? "resuming" : "starting"}...`);
462
+ try {
463
+ await (rerun ? processor.rerun(rerun) : job ? processor.resume(job) : processor.start());
464
+ logger.info(`execution (${execution.id}) finished with status: ${execution.status}`);
465
+ logger.debug(`execution (${execution.id}) details:`, { execution });
466
+ if (execution.status && ((_c = (_b = (_a = execution.workflow) == null ? void 0 : _a.options) == null ? void 0 : _b.deleteExecutionOnStatus) == null ? void 0 : _c.includes(execution.status))) {
467
+ await execution.destroy({ transaction: processor.mainTransaction });
468
+ }
469
+ } catch (err) {
470
+ if (err instanceof Error) {
471
+ logger.error(`execution (${execution.id}) error: ${err.message}`, err);
472
+ }
473
+ }
474
+ return processor;
475
+ };
476
+ const lock = await this.plugin.app.lockManager.tryAcquire((0, import_utils.getExecutionLockKey)(execution.id), 6e4);
477
+ try {
478
+ return await lock.runExclusive(run, 6e4);
479
+ } catch (error) {
480
+ logger.error(`execution (${execution.id}) could not acquire process lock`, { error });
481
+ throw error;
413
482
  }
414
- return processor;
415
483
  }
416
484
  }
@@ -0,0 +1,45 @@
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 { Transactionable } from '@nocobase/database';
10
+ import type PluginWorkflowServer from './Plugin';
11
+ import type { ExecutionModel } from './types';
12
+ export default class ExecutionTimeoutManager {
13
+ private readonly plugin;
14
+ private readonly timers;
15
+ private scanTimer;
16
+ private nextExpiresAtTimer;
17
+ private nextExpiresAt;
18
+ private scanning;
19
+ private stopped;
20
+ constructor(plugin: PluginWorkflowServer);
21
+ getTimeout(execution: ExecutionModel): number;
22
+ getExpiresAt(execution: ExecutionModel, startedAt: Date): Date;
23
+ load(): Promise<void>;
24
+ unload(): Promise<void>;
25
+ isExpired(execution: ExecutionModel, now?: Date): boolean;
26
+ getRemainingMs(execution: ExecutionModel, now?: Date): number;
27
+ abort(execution: ExecutionModel, options?: Transactionable): Promise<boolean>;
28
+ abortExecutionIfExpired(execution: ExecutionModel, options?: Transactionable): Promise<boolean>;
29
+ clear(executionId: number | string): void;
30
+ shouldContinue(execution: ExecutionModel, options?: Transactionable): Promise<boolean>;
31
+ /**
32
+ * Owner-only per-execution timer. Only call from code paths that have acquired
33
+ * local execution ownership, such as Dispatcher/Processor processing paths.
34
+ */
35
+ scheduleExecutionTimeout(execution: ExecutionModel): void;
36
+ invalidateNextExpiresAtIfMatches(expiresAt?: Date | null): void;
37
+ private scanExpiredExecutions;
38
+ private scheduleScan;
39
+ private scheduleNextExpiresAtTimer;
40
+ private scheduleNextExpiresAtIfEarlier;
41
+ private handleNextExpiresAtTimeout;
42
+ private clearNextExpiresAtTimer;
43
+ private clearExecutionTimeout;
44
+ private handleExecutionTimeout;
45
+ }