@nocobase/plugin-workflow 2.1.0-alpha.4 → 2.1.0-alpha.45

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 (185) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/dist/client/214.7e602cfe7a8251b8.js +10 -0
  4. package/dist/client/618.19af7f84261c815d.js +10 -0
  5. package/dist/client/67.452743ce8ec30617.js +10 -0
  6. package/dist/client/964.ffbf5b47ed12bbdc.js +10 -0
  7. package/dist/client/Branch.d.ts +7 -3
  8. package/dist/client/BranchContext.d.ts +18 -0
  9. package/dist/client/components/TimeoutInput.d.ts +11 -0
  10. package/dist/client/constants.d.ts +13 -0
  11. package/dist/client/flows/triggerWorkflows.d.ts +14 -1
  12. package/dist/client/hooks/{useWorkflowFilterActionProps.d.ts → useResourceFilterActionProps.d.ts} +1 -1
  13. package/dist/client/index.js +1 -1
  14. package/dist/client/nodes/create.d.ts +10 -0
  15. package/dist/client/nodes/destroy.d.ts +10 -0
  16. package/dist/client/nodes/index.d.ts +5 -0
  17. package/dist/client/nodes/query.d.ts +18 -2
  18. package/dist/client/nodes/update.d.ts +10 -0
  19. package/dist/client/schemas/collection.d.ts +8 -2
  20. package/dist/client/schemas/executions.d.ts +62 -2
  21. package/dist/client/triggers/collection.d.ts +14 -1
  22. package/dist/client/triggers/index.d.ts +4 -0
  23. package/dist/client/triggers/schedule/constants.d.ts +4 -0
  24. package/dist/client/triggers/schedule/index.d.ts +15 -0
  25. package/dist/client/utils.d.ts +17 -0
  26. package/dist/common/collections/executions.d.ts +43 -1
  27. package/dist/common/collections/executions.js +62 -1
  28. package/dist/common/collections/jobs.js +7 -0
  29. package/dist/common/collections/workflows.d.ts +65 -11
  30. package/dist/common/collections/workflows.js +34 -2
  31. package/dist/common/constants.d.ts +5 -0
  32. package/dist/common/constants.js +7 -0
  33. package/dist/externalVersion.js +15 -13
  34. package/dist/locale/de-DE.json +4 -0
  35. package/dist/locale/en-US.json +7 -0
  36. package/dist/locale/es-ES.json +4 -0
  37. package/dist/locale/fr-FR.json +4 -0
  38. package/dist/locale/hu-HU.json +7 -3
  39. package/dist/locale/id-ID.json +4 -0
  40. package/dist/locale/it-IT.json +4 -0
  41. package/dist/locale/ja-JP.json +5 -1
  42. package/dist/locale/ko-KR.json +4 -0
  43. package/dist/locale/nl-NL.json +7 -3
  44. package/dist/locale/pt-BR.json +4 -0
  45. package/dist/locale/ru-RU.json +4 -0
  46. package/dist/locale/tr-TR.json +4 -0
  47. package/dist/locale/uk-UA.json +7 -3
  48. package/dist/locale/vi-VN.json +7 -3
  49. package/dist/locale/zh-CN.json +12 -1
  50. package/dist/locale/zh-TW.json +7 -3
  51. package/dist/node_modules/cron-parser/lib/parser.js +1 -1
  52. package/dist/node_modules/cron-parser/package.json +1 -1
  53. package/dist/node_modules/joi/dist/joi-browser.min.js +1 -0
  54. package/dist/node_modules/joi/lib/annotate.js +175 -0
  55. package/dist/node_modules/joi/lib/base.js +1069 -0
  56. package/dist/node_modules/joi/lib/cache.js +143 -0
  57. package/dist/node_modules/joi/lib/common.js +216 -0
  58. package/dist/node_modules/joi/lib/compile.js +283 -0
  59. package/dist/node_modules/joi/lib/errors.js +271 -0
  60. package/dist/node_modules/joi/lib/extend.js +312 -0
  61. package/dist/node_modules/joi/lib/index.d.ts +2365 -0
  62. package/dist/node_modules/joi/lib/index.js +1 -0
  63. package/dist/node_modules/joi/lib/manifest.js +476 -0
  64. package/dist/node_modules/joi/lib/messages.js +178 -0
  65. package/dist/node_modules/joi/lib/modify.js +267 -0
  66. package/dist/node_modules/joi/lib/ref.js +414 -0
  67. package/dist/node_modules/joi/lib/schemas.js +302 -0
  68. package/dist/node_modules/joi/lib/state.js +166 -0
  69. package/dist/node_modules/joi/lib/template.js +463 -0
  70. package/dist/node_modules/joi/lib/trace.js +346 -0
  71. package/dist/node_modules/joi/lib/types/alternatives.js +364 -0
  72. package/dist/node_modules/joi/lib/types/any.js +174 -0
  73. package/dist/node_modules/joi/lib/types/array.js +809 -0
  74. package/dist/node_modules/joi/lib/types/binary.js +100 -0
  75. package/dist/node_modules/joi/lib/types/boolean.js +150 -0
  76. package/dist/node_modules/joi/lib/types/date.js +233 -0
  77. package/dist/node_modules/joi/lib/types/function.js +93 -0
  78. package/dist/node_modules/joi/lib/types/keys.js +1067 -0
  79. package/dist/node_modules/joi/lib/types/link.js +168 -0
  80. package/dist/node_modules/joi/lib/types/number.js +363 -0
  81. package/dist/node_modules/joi/lib/types/object.js +22 -0
  82. package/dist/node_modules/joi/lib/types/string.js +850 -0
  83. package/dist/node_modules/joi/lib/types/symbol.js +102 -0
  84. package/dist/node_modules/joi/lib/validator.js +750 -0
  85. package/dist/node_modules/joi/lib/values.js +263 -0
  86. package/dist/node_modules/joi/node_modules/@hapi/topo/lib/index.d.ts +60 -0
  87. package/dist/node_modules/joi/node_modules/@hapi/topo/lib/index.js +225 -0
  88. package/dist/node_modules/joi/node_modules/@hapi/topo/package.json +30 -0
  89. package/dist/node_modules/joi/package.json +1 -0
  90. package/dist/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
  91. package/dist/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +10 -0
  92. package/dist/node_modules/lru-cache/dist/commonjs/index.d.ts +1381 -0
  93. package/dist/node_modules/lru-cache/dist/commonjs/index.js +1692 -0
  94. package/dist/node_modules/lru-cache/dist/commonjs/index.min.js +1 -0
  95. package/dist/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
  96. package/dist/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  97. package/dist/node_modules/lru-cache/dist/esm/browser/index.d.ts +1381 -0
  98. package/dist/node_modules/lru-cache/dist/{mjs → esm/browser}/index.js +537 -179
  99. package/dist/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  100. package/dist/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
  101. package/dist/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  102. package/dist/node_modules/lru-cache/dist/esm/index.d.ts +1381 -0
  103. package/dist/node_modules/lru-cache/dist/{cjs → esm}/index.js +538 -184
  104. package/dist/node_modules/lru-cache/dist/esm/index.min.js +2 -0
  105. package/dist/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
  106. package/dist/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +7 -0
  107. package/dist/node_modules/lru-cache/dist/esm/node/index.d.ts +1381 -0
  108. package/dist/node_modules/lru-cache/dist/esm/node/index.js +1688 -0
  109. package/dist/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  110. package/dist/node_modules/lru-cache/package.json +1 -1
  111. package/dist/node_modules/nodejs-snowflake/nodejs_snowflake.js +1 -1
  112. package/dist/node_modules/nodejs-snowflake/package.json +1 -1
  113. package/dist/server/Dispatcher.d.ts +9 -6
  114. package/dist/server/Dispatcher.js +241 -160
  115. package/dist/server/ExecutionTimeoutManager.d.ts +45 -0
  116. package/dist/server/ExecutionTimeoutManager.js +312 -0
  117. package/dist/server/Plugin.d.ts +13 -0
  118. package/dist/server/Plugin.js +49 -4
  119. package/dist/server/Processor.d.ts +65 -9
  120. package/dist/server/Processor.js +285 -33
  121. package/dist/server/RunningExecutionRegistry.d.ts +18 -0
  122. package/dist/server/RunningExecutionRegistry.js +48 -0
  123. package/dist/server/actions/executions.d.ts +4 -3
  124. package/dist/server/actions/executions.js +42 -21
  125. package/dist/server/actions/jobs.d.ts +2 -1
  126. package/dist/server/actions/jobs.js +28 -1
  127. package/dist/server/actions/nodes.d.ts +5 -0
  128. package/dist/server/actions/nodes.js +38 -5
  129. package/dist/server/actions/workflows.d.ts +6 -0
  130. package/dist/server/actions/workflows.js +38 -0
  131. package/dist/server/constants.d.ts +2 -0
  132. package/dist/server/constants.js +3 -0
  133. package/dist/server/index.d.ts +2 -0
  134. package/dist/server/index.js +2 -0
  135. package/dist/server/instructions/ConditionInstruction.d.ts +2 -0
  136. package/dist/server/instructions/ConditionInstruction.js +17 -0
  137. package/dist/server/instructions/CreateInstruction.d.ts +3 -0
  138. package/dist/server/instructions/CreateInstruction.js +25 -0
  139. package/dist/server/instructions/DestroyInstruction.d.ts +3 -0
  140. package/dist/server/instructions/DestroyInstruction.js +25 -0
  141. package/dist/server/instructions/EndInstruction.d.ts +2 -0
  142. package/dist/server/instructions/EndInstruction.js +4 -0
  143. package/dist/server/instructions/MultiConditionsInstruction.d.ts +2 -0
  144. package/dist/server/instructions/MultiConditionsInstruction.js +23 -0
  145. package/dist/server/instructions/OutputInstruction.d.ts +2 -0
  146. package/dist/server/instructions/OutputInstruction.js +14 -0
  147. package/dist/server/instructions/QueryInstruction.d.ts +3 -0
  148. package/dist/server/instructions/QueryInstruction.js +32 -7
  149. package/dist/server/instructions/UpdateInstruction.d.ts +3 -0
  150. package/dist/server/instructions/UpdateInstruction.js +27 -0
  151. package/dist/server/instructions/index.d.ts +24 -4
  152. package/dist/server/instructions/index.js +18 -0
  153. package/dist/server/migrations/20260423225800-fill-workflow-created-updated-by.d.ts +13 -0
  154. package/dist/server/migrations/20260423225800-fill-workflow-created-updated-by.js +57 -0
  155. package/dist/server/migrations/20260501120000-workflow-timeout.d.ts +13 -0
  156. package/dist/server/migrations/20260501120000-workflow-timeout.js +63 -0
  157. package/dist/server/timeout-errors.d.ts +13 -0
  158. package/dist/server/timeout-errors.js +47 -0
  159. package/dist/server/triggers/CollectionTrigger.d.ts +3 -0
  160. package/dist/server/triggers/CollectionTrigger.js +28 -0
  161. package/dist/server/triggers/ScheduleTrigger/index.d.ts +3 -0
  162. package/dist/server/triggers/ScheduleTrigger/index.js +18 -3
  163. package/dist/server/triggers/index.d.ts +3 -0
  164. package/dist/server/triggers/index.js +18 -0
  165. package/dist/server/types/Execution.d.ts +6 -0
  166. package/dist/server/types/Job.d.ts +3 -3
  167. package/dist/server/types/Workflow.d.ts +6 -1
  168. package/dist/server/utils.d.ts +27 -0
  169. package/dist/server/utils.js +138 -2
  170. package/dist/swagger/index.d.ts +849 -106
  171. package/dist/swagger/index.js +969 -208
  172. package/package.json +6 -5
  173. package/dist/client/27bd65abee87cafa.js +0 -10
  174. package/dist/client/478692c1637f2742.js +0 -10
  175. package/dist/client/c1347b9d21f864d9.js +0 -10
  176. package/dist/client/f39e94207f92e352.js +0 -10
  177. package/dist/node_modules/lru-cache/LICENSE +0 -15
  178. package/dist/node_modules/lru-cache/dist/cjs/index-cjs.d.ts +0 -7
  179. package/dist/node_modules/lru-cache/dist/cjs/index-cjs.js +0 -1
  180. package/dist/node_modules/lru-cache/dist/cjs/index.d.ts +0 -807
  181. package/dist/node_modules/lru-cache/dist/cjs/index.min.js +0 -2
  182. package/dist/node_modules/lru-cache/dist/mjs/index.d.ts +0 -807
  183. package/dist/node_modules/lru-cache/dist/mjs/index.min.js +0 -2
  184. /package/dist/node_modules/lru-cache/dist/{cjs → commonjs}/package.json +0 -0
  185. /package/dist/node_modules/lru-cache/dist/{mjs → esm}/package.json +0 -0
@@ -29,22 +29,23 @@ __export(Dispatcher_exports, {
29
29
  default: () => Dispatcher
30
30
  });
31
31
  module.exports = __toCommonJS(Dispatcher_exports);
32
- var import_crypto = require("crypto");
32
+ 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
- this.prepare = this.prepare.bind(this);
40
40
  }
41
41
  ready = false;
42
42
  executing = 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.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,11 +96,55 @@ 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
- if (this.events.length > 1) {
100
- logger.info(`new event is pending to be prepared after previous preparation is finished`);
99
+ this.saveEvent();
100
+ }
101
+ saveEvent() {
102
+ if (this.saving) {
101
103
  return;
102
104
  }
103
- setImmediate(this.prepare);
105
+ this.saving = (async () => {
106
+ try {
107
+ while (this.events.length) {
108
+ if (this.executing && this.plugin.db.options.dialect === "sqlite") {
109
+ await this.executing;
110
+ }
111
+ const event = this.events.shift();
112
+ this.eventsCount = this.events.length;
113
+ if (!event) continue;
114
+ const logger = this.plugin.getLogger(event[0].id);
115
+ logger.info(`preparing execution for event`);
116
+ try {
117
+ const execution = await this.createExecution(...event);
118
+ if (!execution.dispatched) {
119
+ if (this.plugin.serving() && !this.executing && !this.pending.length) {
120
+ logger.info(`local pending list is empty, adding execution (${execution.id}) to pending list`);
121
+ this.pending.push({ execution });
122
+ } else {
123
+ logger.info(
124
+ `instance is not serving as worker or local pending list is not empty, sending execution (${execution.id}) to queue`
125
+ );
126
+ try {
127
+ await this.plugin.app.eventQueue.publish(this.plugin.channelPendingExecution, {
128
+ executionId: execution.id
129
+ });
130
+ } catch (qErr) {
131
+ logger.error(`publishing execution (${execution.id}) to queue failed:`, { error: qErr });
132
+ }
133
+ }
134
+ }
135
+ } catch (error) {
136
+ logger.error(`failed to create execution:`, { error });
137
+ }
138
+ }
139
+ } finally {
140
+ this.saving = null;
141
+ if (this.events.length) {
142
+ this.saveEvent();
143
+ } else {
144
+ this.dispatch();
145
+ }
146
+ }
147
+ })();
104
148
  }
105
149
  async resume(job) {
106
150
  let { execution } = job;
@@ -108,27 +152,37 @@ class Dispatcher {
108
152
  execution = await job.getExecution();
109
153
  }
110
154
  this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) resuming from job (${job.id}) added to pending list`);
111
- this.run({ execution, job, loaded: true });
155
+ await this.run({ execution, job });
112
156
  }
113
157
  async start(execution) {
114
158
  if (execution.status) {
115
159
  return;
116
160
  }
117
161
  this.plugin.getLogger(execution.workflowId).info(`starting deferred execution (${execution.id})`);
118
- this.run({ execution, loaded: true });
162
+ await this.run({ execution });
119
163
  }
120
164
  async beforeStop() {
121
165
  this.ready = false;
122
- if (this.events.length) {
123
- await this.prepare();
124
- }
125
- if (this.executing) {
126
- await this.executing;
166
+ this.plugin.getLogger("dispatcher").info("app is stopping, draining local queues...");
167
+ while (this.saving || this.executing || this.events.length || this.pending.length) {
168
+ if (this.saving) {
169
+ await this.saving;
170
+ }
171
+ if (this.executing) {
172
+ await this.executing;
173
+ }
174
+ if (this.events.length && !this.saving) {
175
+ this.saveEvent();
176
+ }
177
+ if (this.pending.length && !this.executing) {
178
+ this.dispatch();
179
+ }
180
+ await new Promise((resolve) => setImmediate(resolve));
127
181
  }
182
+ this.plugin.getLogger("dispatcher").info("local queues drained");
128
183
  }
129
184
  dispatch() {
130
- if (!this.ready) {
131
- this.plugin.getLogger("dispatcher").warn(`app is not ready, new dispatching will be ignored`);
185
+ if (!this.ready && !this.pending.length && !this.events.length) {
132
186
  return;
133
187
  }
134
188
  if (this.executing) {
@@ -136,30 +190,36 @@ class Dispatcher {
136
190
  return;
137
191
  }
138
192
  if (this.events.length) {
139
- return this.prepare();
193
+ this.saveEvent();
194
+ return;
140
195
  }
141
196
  this.executing = (async () => {
142
197
  let next = null;
143
- let execution = null;
144
- if (this.pending.length) {
145
- const pending = this.pending.shift();
146
- 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
+ });
147
203
  if (execution) {
148
- next = [execution, pending.job];
204
+ next = [execution, pending == null ? void 0 : pending.job, pending == null ? void 0 : pending.rerun];
205
+ }
206
+ if (pending && next) {
149
207
  this.plugin.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
150
208
  }
151
209
  } else {
152
- if (this.plugin.serving()) {
153
- execution = await this.acquireQueueingExecution();
154
- if (execution) {
155
- next = [execution];
156
- }
157
- } else {
158
- this.plugin.getLogger("dispatcher").warn(`${import_Plugin.WORKER_JOB_WORKFLOW_PROCESS} is not serving on this instance, new dispatching will be ignored`);
159
- }
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
+ );
160
213
  }
161
214
  if (next) {
162
- 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
+ }
163
223
  }
164
224
  setImmediate(() => {
165
225
  this.executing = null;
@@ -171,7 +231,10 @@ class Dispatcher {
171
231
  })();
172
232
  }
173
233
  async run(pending) {
174
- this.pending.push(pending);
234
+ this.pending.push({
235
+ ...pending,
236
+ immediate: !this.executing && !this.pending.length && !this.saving && !this.events.length
237
+ });
175
238
  this.dispatch();
176
239
  }
177
240
  async triggerSync(workflow, context, { deferred, ...options } = {}) {
@@ -179,13 +242,23 @@ class Dispatcher {
179
242
  try {
180
243
  execution = await this.createExecution(workflow, context, options);
181
244
  } catch (err) {
182
- 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
+ }
183
248
  return null;
184
249
  }
185
250
  try {
186
- 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);
187
258
  } catch (err) {
188
- 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
+ }
189
262
  }
190
263
  return null;
191
264
  }
@@ -195,9 +268,9 @@ class Dispatcher {
195
268
  if (!triggerValid) {
196
269
  return false;
197
270
  }
198
- const { stack } = options;
271
+ const { stack = [] } = options;
199
272
  let valid = true;
200
- if ((stack == null ? void 0 : stack.length) > 0) {
273
+ if (stack == null ? void 0 : stack.length) {
201
274
  const existed = await workflow.countExecutions({
202
275
  where: {
203
276
  id: stack
@@ -207,7 +280,7 @@ class Dispatcher {
207
280
  const limitCount = workflow.options.stackLimit || 1;
208
281
  if (existed >= limitCount) {
209
282
  this.plugin.getLogger(workflow.id).warn(
210
- `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.`
211
284
  );
212
285
  valid = false;
213
286
  }
@@ -219,13 +292,21 @@ class Dispatcher {
219
292
  const { deferred } = options;
220
293
  const transaction = await this.plugin.useDataSourceTransaction("main", options.transaction, true);
221
294
  const sameTransaction = options.transaction === transaction;
222
- 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 });
223
304
  if (!valid) {
224
305
  if (!sameTransaction) {
225
306
  await transaction.commit();
226
307
  }
227
308
  (_a = options.onTriggerFail) == null ? void 0 : _a.call(options, workflow, context, options);
228
- return Promise.reject(new Error("event is not valid"));
309
+ throw new Error("event is not valid");
229
310
  }
230
311
  let execution;
231
312
  try {
@@ -233,8 +314,9 @@ class Dispatcher {
233
314
  {
234
315
  context,
235
316
  key: workflow.key,
236
- eventKey: options.eventKey ?? (0, import_crypto.randomUUID)(),
237
- stack: options.stack,
317
+ eventKey: options.eventKey ?? (0, import_node_crypto.randomUUID)(),
318
+ stack,
319
+ parentExecutionId: options.parentExecutionId ?? null,
238
320
  dispatched: deferred ?? false,
239
321
  status: deferred ? import_constants.EXECUTION_STATUS.STARTED : import_constants.EXECUTION_STATUS.QUEUEING,
240
322
  manually: options.manually
@@ -268,136 +350,135 @@ class Dispatcher {
268
350
  execution.workflow = workflow;
269
351
  return execution;
270
352
  }
271
- prepare = async () => {
272
- if (this.executing && this.plugin.db.options.dialect === "sqlite") {
273
- await this.executing;
274
- }
275
- const event = this.events.shift();
276
- this.eventsCount = this.events.length;
277
- if (!event) {
278
- this.plugin.getLogger("dispatcher").info(`events queue is empty, no need to prepare`);
279
- return;
280
- }
281
- const logger = this.plugin.getLogger(event[0].id);
282
- logger.info(`preparing execution for event`);
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");
283
360
  try {
284
- const execution = await this.createExecution(...event);
285
- if (!(execution == null ? void 0 : execution.dispatched)) {
286
- if (this.plugin.serving() && !this.executing && !this.pending.length) {
287
- logger.info(`local pending list is empty, adding execution (${execution.id}) to pending list`);
288
- this.pending.push({ execution });
361
+ let execution = input;
362
+ if (execution) {
363
+ if (!options.immediate || execution.status !== import_constants.EXECUTION_STATUS.QUEUEING) {
364
+ await execution.reload({ transaction: tx });
365
+ }
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`);
289
379
  } else {
290
- logger.info(
291
- `instance is not serving as worker or local pending list is not empty, sending execution (${execution.id}) to queue`
292
- );
293
- try {
294
- await this.plugin.app.eventQueue.publish(this.plugin.channelPendingExecution, {
295
- executionId: execution.id
296
- });
297
- } catch (qErr) {
298
- logger.error(`publishing execution (${execution.id}) to queue failed:`, { error: qErr });
299
- }
380
+ this.plugin.getLogger("dispatcher").debug(`no execution in db queued to process`);
381
+ }
382
+ }
383
+ if (!execution) {
384
+ if (ownTransaction) {
385
+ await tx.commit();
300
386
  }
387
+ return null;
388
+ }
389
+ const entered = await this.enter(execution, tx);
390
+ if (ownTransaction) {
391
+ await tx.commit();
301
392
  }
393
+ return entered;
302
394
  } catch (error) {
303
- logger.error(`failed to create execution:`, { error });
304
- }
305
- if (this.events.length) {
306
- await this.prepare();
307
- } else {
308
- this.plugin.getLogger("dispatcher").info("no more events need to be prepared, dispatching...");
309
- if (this.executing) {
310
- await this.executing;
395
+ if (ownTransaction) {
396
+ await tx.rollback();
311
397
  }
312
- this.dispatch();
398
+ if (error instanceof Error) {
399
+ logger.error(`entering execution failed: ${error.message}`, { error });
400
+ }
401
+ return null;
313
402
  }
314
- };
315
- async acquirePendingExecution(execution) {
316
- const logger = this.plugin.getLogger(execution.workflowId);
317
- const isolationLevel = this.plugin.db.options.dialect === "sqlite" ? [][0] : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ;
318
- let fetched = execution;
319
- try {
320
- await this.plugin.db.sequelize.transaction({ isolationLevel }, async (transaction) => {
321
- const ExecutionModelClass = this.plugin.db.getModel("executions");
322
- const [affected] = await ExecutionModelClass.update(
323
- { dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED },
324
- {
325
- where: {
326
- id: execution.id,
327
- dispatched: false
328
- },
329
- transaction
330
- }
331
- );
332
- if (!affected) {
333
- fetched = null;
334
- return;
335
- }
336
- await execution.reload({ transaction });
403
+ }
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
337
410
  });
338
- } catch (error) {
339
- logger.error(`acquiring pending execution failed: ${error.message}`, { error });
340
411
  }
341
- return fetched;
342
- }
343
- async acquireQueueingExecution() {
344
- const isolationLevel = this.plugin.db.options.dialect === "sqlite" ? [][0] : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ;
345
- let fetched = null;
346
- try {
347
- await this.plugin.db.sequelize.transaction(
348
- {
349
- isolationLevel
350
- },
351
- async (transaction) => {
352
- const execution = await this.plugin.db.getRepository("executions").findOne({
353
- filter: {
354
- dispatched: false,
355
- "workflow.enabled": true
356
- },
357
- sort: "id",
358
- transaction
359
- });
360
- if (execution) {
361
- this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) fetched from db`);
362
- await execution.update(
363
- {
364
- dispatched: true,
365
- status: import_constants.EXECUTION_STATUS.STARTED
366
- },
367
- { transaction }
368
- );
369
- execution.workflow = this.plugin.enabledCache.get(execution.workflowId);
370
- fetched = execution;
371
- } else {
372
- this.plugin.getLogger("dispatcher").debug(`no execution in db queued to process`);
373
- }
374
- }
375
- );
376
- } catch (error) {
377
- this.plugin.getLogger("dispatcher").error(`fetching execution from db failed: ${error.message}`, { error });
412
+ if (execution.status && execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
413
+ return null;
378
414
  }
379
- return fetched;
380
- }
381
- async process(execution, job, options = {}) {
382
- var _a, _b;
383
- const logger = this.plugin.getLogger(execution.workflowId);
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
+ };
384
427
  if (!execution.dispatched) {
385
- const transaction = await this.plugin.useDataSourceTransaction("main", options.transaction);
386
- await execution.update({ dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED }, { transaction });
387
- logger.info(`execution (${execution.id}) from pending list updated to started`);
428
+ where.dispatched = false;
429
+ }
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;
388
444
  }
389
- const processor = this.plugin.createProcessor(execution, options);
390
- logger.info(`execution (${execution.id}) ${job ? "resuming" : "starting"}...`);
391
- try {
392
- await (job ? processor.resume(job) : processor.start());
393
- logger.info(`execution (${execution.id}) finished with status: ${execution.status}`);
394
- logger.debug(`execution (${execution.id}) details:`, { execution });
395
- if (execution.status && ((_b = (_a = execution.workflow.options) == null ? void 0 : _a.deleteExecutionOnStatus) == null ? void 0 : _b.includes(execution.status))) {
396
- await execution.destroy({ transaction: processor.mainTransaction });
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`);
397
458
  }
398
- } catch (err) {
399
- 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;
400
482
  }
401
- return processor;
402
483
  }
403
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
+ }