@medusajs/workflow-engine-redis 3.0.0-preview-20250410180148 → 3.0.0-preview-20251201152819

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 (56) hide show
  1. package/dist/loaders/redis.d.ts.map +1 -1
  2. package/dist/loaders/redis.js +10 -10
  3. package/dist/loaders/redis.js.map +1 -1
  4. package/dist/loaders/utils.d.ts.map +1 -1
  5. package/dist/loaders/utils.js +1 -1
  6. package/dist/loaders/utils.js.map +1 -1
  7. package/dist/migrations/Migration20231228143900.d.ts +1 -1
  8. package/dist/migrations/Migration20231228143900.d.ts.map +1 -1
  9. package/dist/migrations/Migration20231228143900.js +1 -1
  10. package/dist/migrations/Migration20231228143900.js.map +1 -1
  11. package/dist/migrations/Migration20241206123341.d.ts +1 -1
  12. package/dist/migrations/Migration20241206123341.d.ts.map +1 -1
  13. package/dist/migrations/Migration20241206123341.js +1 -1
  14. package/dist/migrations/Migration20241206123341.js.map +1 -1
  15. package/dist/migrations/Migration20250120111059.d.ts +1 -1
  16. package/dist/migrations/Migration20250120111059.d.ts.map +1 -1
  17. package/dist/migrations/Migration20250120111059.js +1 -1
  18. package/dist/migrations/Migration20250120111059.js.map +1 -1
  19. package/dist/migrations/Migration20250128174354.d.ts +1 -1
  20. package/dist/migrations/Migration20250128174354.d.ts.map +1 -1
  21. package/dist/migrations/Migration20250128174354.js +1 -1
  22. package/dist/migrations/Migration20250128174354.js.map +1 -1
  23. package/dist/migrations/Migration20250505101505.d.ts +6 -0
  24. package/dist/migrations/Migration20250505101505.d.ts.map +1 -0
  25. package/dist/migrations/Migration20250505101505.js +40 -0
  26. package/dist/migrations/Migration20250505101505.js.map +1 -0
  27. package/dist/migrations/Migration20250819110923.d.ts +6 -0
  28. package/dist/migrations/Migration20250819110923.d.ts.map +1 -0
  29. package/dist/migrations/Migration20250819110923.js +14 -0
  30. package/dist/migrations/Migration20250819110923.js.map +1 -0
  31. package/dist/migrations/Migration20250819110924.d.ts +6 -0
  32. package/dist/migrations/Migration20250819110924.d.ts.map +1 -0
  33. package/dist/migrations/Migration20250819110924.js +16 -0
  34. package/dist/migrations/Migration20250819110924.js.map +1 -0
  35. package/dist/migrations/Migration20250908080326.d.ts +6 -0
  36. package/dist/migrations/Migration20250908080326.d.ts.map +1 -0
  37. package/dist/migrations/Migration20250908080326.js +20 -0
  38. package/dist/migrations/Migration20250908080326.js.map +1 -0
  39. package/dist/models/workflow-execution.d.ts +1 -0
  40. package/dist/models/workflow-execution.d.ts.map +1 -1
  41. package/dist/models/workflow-execution.js +22 -1
  42. package/dist/models/workflow-execution.js.map +1 -1
  43. package/dist/services/workflow-orchestrator.d.ts +16 -3
  44. package/dist/services/workflow-orchestrator.d.ts.map +1 -1
  45. package/dist/services/workflow-orchestrator.js +247 -118
  46. package/dist/services/workflow-orchestrator.js.map +1 -1
  47. package/dist/services/workflows-module.d.ts +114 -9
  48. package/dist/services/workflows-module.d.ts.map +1 -1
  49. package/dist/services/workflows-module.js +113 -50
  50. package/dist/services/workflows-module.js.map +1 -1
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/dist/utils/workflow-orchestrator-storage.d.ts +10 -3
  53. package/dist/utils/workflow-orchestrator-storage.d.ts.map +1 -1
  54. package/dist/utils/workflow-orchestrator-storage.js +343 -143
  55. package/dist/utils/workflow-orchestrator-storage.js.map +1 -1
  56. package/package.json +14 -28
@@ -10,9 +10,10 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _RedisDistributedTransactionStorage_instances, _RedisDistributedTransactionStorage_isWorkerMode, _RedisDistributedTransactionStorage_preventRaceConditionExecutionIfNecessary;
13
+ var _RedisDistributedTransactionStorage_instances, _RedisDistributedTransactionStorage_isWorkerMode, _RedisDistributedTransactionStorage_getLockKey, _RedisDistributedTransactionStorage_acquireLock, _RedisDistributedTransactionStorage_releaseLock, _RedisDistributedTransactionStorage_preventRaceConditionExecutionIfNecessary;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.RedisDistributedTransactionStorage = void 0;
16
+ const core_1 = require("@medusajs/framework/mikro-orm/core");
16
17
  const orchestration_1 = require("@medusajs/framework/orchestration");
17
18
  const utils_1 = require("@medusajs/framework/utils");
18
19
  const bullmq_1 = require("bullmq");
@@ -23,6 +24,25 @@ var JobType;
23
24
  JobType["STEP_TIMEOUT"] = "step_timeout";
24
25
  JobType["TRANSACTION_TIMEOUT"] = "transaction_timeout";
25
26
  })(JobType || (JobType = {}));
27
+ const THIRTY_MINUTES_IN_MS = 1000 * 60 * 30;
28
+ const REPEATABLE_CLEARER_JOB_ID = "clear-expired-executions";
29
+ const doneStates = new Set([
30
+ utils_1.TransactionStepState.DONE,
31
+ utils_1.TransactionStepState.REVERTED,
32
+ utils_1.TransactionStepState.FAILED,
33
+ utils_1.TransactionStepState.SKIPPED,
34
+ utils_1.TransactionStepState.SKIPPED_FAILURE,
35
+ utils_1.TransactionStepState.TIMEOUT,
36
+ ]);
37
+ const finishedStates = new Set([
38
+ utils_1.TransactionState.DONE,
39
+ utils_1.TransactionState.FAILED,
40
+ utils_1.TransactionState.REVERTED,
41
+ ]);
42
+ const failedStates = new Set([
43
+ utils_1.TransactionState.FAILED,
44
+ utils_1.TransactionState.REVERTED,
45
+ ]);
26
46
  class RedisDistributedTransactionStorage {
27
47
  constructor({ workflowExecutionService, redisConnection, redisWorkerConnection, redisQueueName, redisJobQueueName, logger, isWorkerMode, }) {
28
48
  _RedisDistributedTransactionStorage_instances.add(this);
@@ -31,6 +51,7 @@ class RedisDistributedTransactionStorage {
31
51
  this.logger_ = logger;
32
52
  this.redisClient = redisConnection;
33
53
  this.redisWorkerConnection = redisWorkerConnection;
54
+ this.cleanerQueueName = "workflows-cleaner";
34
55
  this.queueName = redisQueueName;
35
56
  this.jobQueueName = redisJobQueueName;
36
57
  this.queue = new bullmq_1.Queue(redisQueueName, { connection: this.redisClient });
@@ -39,54 +60,154 @@ class RedisDistributedTransactionStorage {
39
60
  connection: this.redisClient,
40
61
  })
41
62
  : undefined;
63
+ this.cleanerQueue_ = isWorkerMode
64
+ ? new bullmq_1.Queue(this.cleanerQueueName, {
65
+ connection: this.redisClient,
66
+ })
67
+ : undefined;
42
68
  __classPrivateFieldSet(this, _RedisDistributedTransactionStorage_isWorkerMode, isWorkerMode, "f");
43
69
  }
44
70
  async onApplicationPrepareShutdown() {
45
71
  // Close worker gracefully, i.e. wait for the current jobs to finish
46
72
  await this.worker?.close();
47
73
  await this.jobWorker?.close();
74
+ await this.cleanerWorker_?.close();
48
75
  }
49
76
  async onApplicationShutdown() {
50
77
  await this.queue?.close();
51
78
  await this.jobQueue?.close();
79
+ await this.cleanerQueue_?.close();
52
80
  }
53
81
  async onApplicationStart() {
82
+ await this.ensureRedisConnection();
54
83
  const allowedJobs = [
55
84
  JobType.RETRY,
56
85
  JobType.STEP_TIMEOUT,
57
86
  JobType.TRANSACTION_TIMEOUT,
58
87
  ];
59
88
  const workerOptions = {
60
- connection: this.redisWorkerConnection /*, runRetryDelay: 100000 for tests */,
89
+ connection: this.redisWorkerConnection,
61
90
  };
62
91
  // TODO: Remove this once we have released to all clients (Added: v2.6+)
63
92
  // Remove all repeatable jobs from the old queue since now we have a queue dedicated to scheduled jobs
64
93
  await this.removeAllRepeatableJobs(this.queue);
65
- this.worker = new bullmq_1.Worker(this.queueName, async (job) => {
66
- this.logger_.debug(`executing job ${job.name} from queue ${this.queueName} with the following data: ${JSON.stringify(job.data)}`);
67
- if (allowedJobs.includes(job.name)) {
68
- await this.executeTransaction(job.data.workflowId, job.data.transactionId);
69
- }
70
- if (job.name === JobType.SCHEDULE) {
71
- // Remove repeatable job from the old queue since now we have a queue dedicated to scheduled jobs
72
- await this.remove(job.data.jobId);
73
- }
74
- }, workerOptions);
75
94
  if (__classPrivateFieldGet(this, _RedisDistributedTransactionStorage_isWorkerMode, "f")) {
95
+ this.worker = new bullmq_1.Worker(this.queueName, async (job) => {
96
+ this.logger_.debug(`executing job ${job.name} from queue ${this.queueName} with the following data: ${JSON.stringify(job.data)}`);
97
+ if (allowedJobs.includes(job.name)) {
98
+ try {
99
+ await this.executeTransaction(job.data.workflowId, job.data.transactionId, job.data.transactionMetadata);
100
+ }
101
+ catch (error) {
102
+ if (!orchestration_1.SkipExecutionError.isSkipExecutionError(error)) {
103
+ throw error;
104
+ }
105
+ }
106
+ }
107
+ if (job.name === JobType.SCHEDULE) {
108
+ // Remove repeatable job from the old queue since now we have a queue dedicated to scheduled jobs
109
+ await this.remove(job.data.jobId);
110
+ }
111
+ }, workerOptions);
76
112
  this.jobWorker = new bullmq_1.Worker(this.jobQueueName, async (job) => {
77
113
  this.logger_.debug(`executing scheduled job ${job.data.jobId} from queue ${this.jobQueueName} with the following options: ${JSON.stringify(job.data.schedulerOptions)}`);
78
114
  return await this.executeScheduledJob(job.data.jobId, job.data.schedulerOptions);
79
115
  }, workerOptions);
116
+ this.cleanerWorker_ = new bullmq_1.Worker(this.cleanerQueueName, async () => {
117
+ await this.clearExpiredExecutions();
118
+ }, workerOptions);
119
+ await this.cleanerQueue_?.add("cleaner", {}, {
120
+ repeat: {
121
+ every: THIRTY_MINUTES_IN_MS,
122
+ },
123
+ jobId: REPEATABLE_CLEARER_JOB_ID,
124
+ removeOnComplete: true,
125
+ removeOnFail: true,
126
+ });
80
127
  }
81
128
  }
82
129
  setWorkflowOrchestratorService(workflowOrchestratorService) {
83
130
  this.workflowOrchestratorService_ = workflowOrchestratorService;
84
131
  }
132
+ async ensureRedisConnection() {
133
+ const reconnectTasks = [];
134
+ if (this.redisClient.status !== "ready") {
135
+ this.logger_.warn(`[Workflow-engine-redis] Redis connection is not ready (status: ${this.redisClient.status}). Attempting to reconnect...`);
136
+ reconnectTasks.push(this.redisClient
137
+ .connect()
138
+ .then(() => {
139
+ this.logger_.info("[Workflow-engine-redis] Redis connection reestablished successfully");
140
+ })
141
+ .catch((error) => {
142
+ this.logger_.error("[Workflow-engine-redis] Failed to reconnect to Redis", error);
143
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.DB_ERROR, `Redis connection failed: ${error.message}`);
144
+ }));
145
+ }
146
+ if (this.redisWorkerConnection.status !== "ready") {
147
+ this.logger_.warn(`[Workflow-engine-redis] Redis worker connection is not ready (status: ${this.redisWorkerConnection.status}). Attempting to reconnect...`);
148
+ reconnectTasks.push(this.redisWorkerConnection
149
+ .connect()
150
+ .then(() => {
151
+ this.logger_.info("[Workflow-engine-redis] Redis worker connection reestablished successfully");
152
+ })
153
+ .catch((error) => {
154
+ this.logger_.error("[Workflow-engine-redis] Failed to reconnect to Redis worker connection", error);
155
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.DB_ERROR, `Redis worker connection failed: ${error.message}`);
156
+ }));
157
+ }
158
+ if (reconnectTasks.length > 0) {
159
+ await (0, utils_1.promiseAll)(reconnectTasks);
160
+ }
161
+ }
85
162
  async saveToDb(data, retentionTime) {
163
+ const isNotStarted = data.flow.state === utils_1.TransactionState.NOT_STARTED;
164
+ const asyncVersion = data.flow._v;
165
+ const isFinished = finishedStates.has(data.flow.state);
166
+ const isWaitingToCompensate = data.flow.state === utils_1.TransactionState.WAITING_TO_COMPENSATE;
167
+ const isFlowInvoking = data.flow.state === utils_1.TransactionState.INVOKING;
168
+ const stepsArray = Object.values(data.flow.steps);
169
+ let currentStep;
170
+ const targetStates = isFlowInvoking
171
+ ? new Set([
172
+ utils_1.TransactionStepState.INVOKING,
173
+ utils_1.TransactionStepState.DONE,
174
+ utils_1.TransactionStepState.FAILED,
175
+ ])
176
+ : new Set([utils_1.TransactionStepState.COMPENSATING]);
177
+ for (let i = stepsArray.length - 1; i >= 0; i--) {
178
+ const step = stepsArray[i];
179
+ if (step.id === "_root") {
180
+ break;
181
+ }
182
+ const isTargetState = targetStates.has(step.invoke?.state);
183
+ if (isTargetState && !currentStep) {
184
+ currentStep = step;
185
+ break;
186
+ }
187
+ }
188
+ let shouldStoreCurrentSteps = false;
189
+ if (currentStep) {
190
+ for (const step of stepsArray) {
191
+ if (step.id === "_root") {
192
+ continue;
193
+ }
194
+ if (step.depth === currentStep.depth &&
195
+ step?.definition?.store === true) {
196
+ shouldStoreCurrentSteps = true;
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ if (!(isNotStarted || isFinished || isWaitingToCompensate) &&
202
+ !shouldStoreCurrentSteps &&
203
+ !asyncVersion) {
204
+ return;
205
+ }
86
206
  await this.workflowExecutionService_.upsert([
87
207
  {
88
208
  workflow_id: data.flow.modelId,
89
209
  transaction_id: data.flow.transactionId,
210
+ run_id: data.flow.runId,
90
211
  execution: data.flow,
91
212
  context: {
92
213
  data: data.context,
@@ -100,16 +221,20 @@ class RedisDistributedTransactionStorage {
100
221
  async deleteFromDb(data) {
101
222
  await this.workflowExecutionService_.delete([
102
223
  {
103
- workflow_id: data.flow.modelId,
104
- transaction_id: data.flow.transactionId,
224
+ run_id: data.flow.runId,
105
225
  },
106
226
  ]);
107
227
  }
108
- async executeTransaction(workflowId, transactionId) {
228
+ async executeTransaction(workflowId, transactionId, transactionMetadata = {}) {
109
229
  return await this.workflowOrchestratorService_.run(workflowId, {
110
230
  transactionId,
111
231
  logOnError: true,
112
232
  throwOnError: false,
233
+ context: {
234
+ eventGroupId: transactionMetadata.eventGroupId,
235
+ parentStepIdempotencyKey: transactionMetadata.parentStepIdempotencyKey,
236
+ preventReleaseEvents: transactionMetadata.preventReleaseEvents,
237
+ },
113
238
  });
114
239
  }
115
240
  async executeScheduledJob(jobId, schedulerOptions) {
@@ -130,101 +255,163 @@ class RedisDistributedTransactionStorage {
130
255
  }
131
256
  }
132
257
  async get(key, options) {
133
- const data = await this.redisClient.get(key);
134
- if (data) {
135
- return JSON.parse(data);
136
- }
137
- const { idempotent, store, retentionTime } = options ?? {};
138
- if (!idempotent && !(store && (0, utils_1.isDefined)(retentionTime))) {
139
- return;
140
- }
141
258
  const [_, workflowId, transactionId] = key.split(":");
142
- const trx = await this.workflowExecutionService_
143
- .retrieve({
144
- workflow_id: workflowId,
145
- transaction_id: transactionId,
146
- }, {
147
- select: ["execution", "context"],
148
- })
149
- .catch(() => undefined);
259
+ const [trx, rawData] = await (0, utils_1.promiseAll)([
260
+ this.workflowExecutionService_
261
+ .list({
262
+ workflow_id: workflowId,
263
+ transaction_id: transactionId,
264
+ }, {
265
+ select: ["execution", "context"],
266
+ order: {
267
+ id: "desc",
268
+ },
269
+ take: 1,
270
+ })
271
+ .then((trx) => trx[0])
272
+ .catch(() => undefined),
273
+ options?._cachedRawData !== undefined
274
+ ? Promise.resolve(options._cachedRawData)
275
+ : this.redisClient.get(key),
276
+ ]);
150
277
  if (trx) {
151
- return {
152
- flow: trx.execution,
153
- context: trx.context.data,
154
- errors: trx.context.errors,
155
- };
156
- }
157
- return;
158
- }
159
- async list() {
160
- const keys = await this.redisClient.keys(orchestration_1.DistributedTransaction.keyPrefix + ":*");
161
- const transactions = [];
162
- for (const key of keys) {
163
- const data = await this.redisClient.get(key);
164
- if (data) {
165
- transactions.push(JSON.parse(data));
278
+ let flow, errors;
279
+ if (rawData) {
280
+ const data = JSON.parse(rawData);
281
+ flow = data.flow;
282
+ errors = data.errors;
283
+ }
284
+ const { idempotent } = options ?? {};
285
+ const execution = trx.execution;
286
+ if (!idempotent) {
287
+ const isFailedOrReverted = failedStates.has(execution.state);
288
+ const isDone = execution.state === utils_1.TransactionState.DONE;
289
+ const isCancellingAndFailedOrReverted = options?.isCancelling && isFailedOrReverted;
290
+ const isNotCancellingAndDoneOrFailedOrReverted = !options?.isCancelling && (isDone || isFailedOrReverted);
291
+ if (isCancellingAndFailedOrReverted ||
292
+ isNotCancellingAndDoneOrFailedOrReverted) {
293
+ return;
294
+ }
166
295
  }
296
+ return new orchestration_1.TransactionCheckpoint(flow ?? trx.execution, trx.context?.data, errors ?? trx.context?.errors);
167
297
  }
168
- return transactions;
298
+ return;
169
299
  }
170
300
  async save(key, data, ttl, options) {
171
301
  /**
172
302
  * Store the retention time only if the transaction is done, failed or reverted.
173
- * From that moment, this tuple can be later on archived or deleted after the retention time.
174
303
  */
175
- const hasFinished = [
176
- utils_1.TransactionState.DONE,
177
- utils_1.TransactionState.FAILED,
178
- utils_1.TransactionState.REVERTED,
179
- ].includes(data.flow.state);
180
- const { retentionTime, idempotent } = options ?? {};
181
- await __classPrivateFieldGet(this, _RedisDistributedTransactionStorage_instances, "m", _RedisDistributedTransactionStorage_preventRaceConditionExecutionIfNecessary).call(this, {
182
- data,
183
- key,
184
- options,
185
- });
186
- if (hasFinished) {
187
- Object.assign(data, {
188
- retention_time: retentionTime,
304
+ const { retentionTime } = options ?? {};
305
+ let lockAcquired = false;
306
+ let storedData;
307
+ if (data.flow._v) {
308
+ lockAcquired = await __classPrivateFieldGet(this, _RedisDistributedTransactionStorage_instances, "m", _RedisDistributedTransactionStorage_acquireLock).call(this, key);
309
+ if (!lockAcquired) {
310
+ throw new Error("Lock not acquired");
311
+ }
312
+ storedData = await this.get(key, {
313
+ isCancelling: !!data.flow.cancelledAt,
189
314
  });
315
+ orchestration_1.TransactionCheckpoint.mergeCheckpoints(data, storedData);
190
316
  }
191
- const stringifiedData = JSON.stringify(data);
192
- if (!hasFinished) {
193
- if (ttl) {
194
- await this.redisClient.set(key, stringifiedData, "EX", ttl);
317
+ try {
318
+ const hasFinished = finishedStates.has(data.flow.state);
319
+ await __classPrivateFieldGet(this, _RedisDistributedTransactionStorage_instances, "m", _RedisDistributedTransactionStorage_preventRaceConditionExecutionIfNecessary).call(this, {
320
+ data: data,
321
+ key,
322
+ options,
323
+ storedData,
324
+ });
325
+ // Only set if not exists
326
+ const shouldSetNX = data.flow.state === utils_1.TransactionState.NOT_STARTED &&
327
+ !data.flow.transactionId.startsWith("auto-");
328
+ if (retentionTime) {
329
+ Object.assign(data, {
330
+ retention_time: retentionTime,
331
+ });
332
+ }
333
+ const execPipeline = () => {
334
+ const stringifiedData = JSON.stringify({
335
+ errors: data.errors,
336
+ flow: data.flow,
337
+ });
338
+ const pipeline = this.redisClient.pipeline();
339
+ if (!hasFinished) {
340
+ if (ttl) {
341
+ if (shouldSetNX) {
342
+ pipeline.set(key, stringifiedData, "EX", ttl, "NX");
343
+ }
344
+ else {
345
+ pipeline.set(key, stringifiedData, "EX", ttl);
346
+ }
347
+ }
348
+ else {
349
+ if (shouldSetNX) {
350
+ pipeline.set(key, stringifiedData, "NX");
351
+ }
352
+ else {
353
+ pipeline.set(key, stringifiedData);
354
+ }
355
+ }
356
+ }
357
+ else {
358
+ pipeline.unlink(key);
359
+ }
360
+ return pipeline.exec().then((result) => {
361
+ if (!shouldSetNX) {
362
+ return result;
363
+ }
364
+ const actionResult = result?.pop();
365
+ const isOk = !!actionResult?.pop();
366
+ if (!isOk) {
367
+ throw new orchestration_1.SkipExecutionError("Transaction already started for transactionId: " +
368
+ data.flow.transactionId);
369
+ }
370
+ return result;
371
+ });
372
+ };
373
+ // Parallelize DB and Redis operations for better performance
374
+ if (hasFinished && !retentionTime) {
375
+ if (!data.flow.metadata?.parentStepIdempotencyKey) {
376
+ await (0, utils_1.promiseAll)([this.deleteFromDb(data), execPipeline()]);
377
+ }
378
+ else {
379
+ await (0, utils_1.promiseAll)([this.saveToDb(data, retentionTime), execPipeline()]);
380
+ }
195
381
  }
196
382
  else {
197
- await this.redisClient.set(key, stringifiedData);
383
+ await (0, utils_1.promiseAll)([this.saveToDb(data, retentionTime), execPipeline()]);
198
384
  }
385
+ return data;
199
386
  }
200
- if (hasFinished && !retentionTime && !idempotent) {
201
- await this.deleteFromDb(data);
202
- }
203
- else {
204
- await this.saveToDb(data, retentionTime);
205
- }
206
- if (hasFinished) {
207
- await this.redisClient.unlink(key);
387
+ finally {
388
+ if (lockAcquired) {
389
+ await __classPrivateFieldGet(this, _RedisDistributedTransactionStorage_instances, "m", _RedisDistributedTransactionStorage_releaseLock).call(this, key);
390
+ }
208
391
  }
209
392
  }
210
393
  async scheduleRetry(transaction, step, timestamp, interval) {
211
394
  await this.queue.add(JobType.RETRY, {
212
395
  workflowId: transaction.modelId,
213
396
  transactionId: transaction.transactionId,
397
+ transactionMetadata: transaction.getFlow().metadata,
214
398
  stepId: step.id,
215
399
  }, {
216
400
  delay: interval > 0 ? interval * 1000 : undefined,
217
- jobId: this.getJobId(JobType.RETRY, transaction, step),
401
+ jobId: this.getJobId(JobType.RETRY, transaction, step, interval),
218
402
  removeOnComplete: true,
219
403
  });
220
404
  }
221
405
  async clearRetry(transaction, step) {
222
- await this.removeJob(JobType.RETRY, transaction, step);
406
+ // Pass retry interval to ensure we remove the correct job (with -retry suffix if interval > 0)
407
+ const interval = step.definition.retryInterval || 0;
408
+ await this.removeJob(JobType.RETRY, transaction, step, interval);
223
409
  }
224
410
  async scheduleTransactionTimeout(transaction, _, interval) {
225
411
  await this.queue.add(JobType.TRANSACTION_TIMEOUT, {
226
412
  workflowId: transaction.modelId,
227
413
  transactionId: transaction.transactionId,
414
+ transactionMetadata: transaction.getFlow().metadata,
228
415
  }, {
229
416
  delay: interval * 1000,
230
417
  jobId: this.getJobId(JobType.TRANSACTION_TIMEOUT, transaction),
@@ -238,6 +425,7 @@ class RedisDistributedTransactionStorage {
238
425
  await this.queue.add(JobType.STEP_TIMEOUT, {
239
426
  workflowId: transaction.modelId,
240
427
  transactionId: transaction.transactionId,
428
+ transactionMetadata: transaction.getFlow().metadata,
241
429
  stepId: step.id,
242
430
  }, {
243
431
  delay: interval * 1000,
@@ -248,18 +436,22 @@ class RedisDistributedTransactionStorage {
248
436
  async clearStepTimeout(transaction, step) {
249
437
  await this.removeJob(JobType.STEP_TIMEOUT, transaction, step);
250
438
  }
251
- getJobId(type, transaction, step) {
439
+ getJobId(type, transaction, step, interval) {
252
440
  const key = [type, transaction.modelId, transaction.transactionId];
253
441
  if (step) {
254
442
  key.push(step.id, step.attempts + "");
443
+ // Add suffix for retry scheduling (interval > 0) to avoid collision with async execution (interval = 0)
444
+ if (type === JobType.RETRY && (0, utils_1.isDefined)(interval) && interval > 0) {
445
+ key.push("retry");
446
+ }
255
447
  if (step.isCompensating()) {
256
448
  key.push("compensate");
257
449
  }
258
450
  }
259
451
  return key.join(":");
260
452
  }
261
- async removeJob(type, transaction, step) {
262
- const jobId = this.getJobId(type, transaction, step);
453
+ async removeJob(type, transaction, step, interval) {
454
+ const jobId = this.getJobId(type, transaction, step, interval);
263
455
  if (type === JobType.SCHEDULE) {
264
456
  const job = await this.jobQueue?.getJob(jobId);
265
457
  if (job) {
@@ -315,21 +507,78 @@ class RedisDistributedTransactionStorage {
315
507
  const repeatableJobs = (await queue.getRepeatableJobs()) ?? [];
316
508
  await (0, utils_1.promiseAll)(repeatableJobs.map((job) => queue.removeRepeatableByKey(job.key)));
317
509
  }
510
+ async clearExpiredExecutions() {
511
+ await this.workflowExecutionService_.delete({
512
+ retention_time: {
513
+ $ne: null,
514
+ },
515
+ updated_at: {
516
+ $lte: (0, core_1.raw)((_alias) => `CURRENT_TIMESTAMP - (INTERVAL '1 second' * "retention_time")`),
517
+ },
518
+ state: {
519
+ $in: [
520
+ utils_1.TransactionState.DONE,
521
+ utils_1.TransactionState.FAILED,
522
+ utils_1.TransactionState.REVERTED,
523
+ ],
524
+ },
525
+ });
526
+ }
318
527
  }
319
528
  exports.RedisDistributedTransactionStorage = RedisDistributedTransactionStorage;
320
- _RedisDistributedTransactionStorage_isWorkerMode = new WeakMap(), _RedisDistributedTransactionStorage_instances = new WeakSet(), _RedisDistributedTransactionStorage_preventRaceConditionExecutionIfNecessary = async function _RedisDistributedTransactionStorage_preventRaceConditionExecutionIfNecessary({ data, key, options, }) {
321
- let isInitialCheckpoint = false;
322
- if (data.flow.state === utils_1.TransactionState.NOT_STARTED) {
323
- isInitialCheckpoint = true;
324
- }
529
+ _RedisDistributedTransactionStorage_isWorkerMode = new WeakMap(), _RedisDistributedTransactionStorage_instances = new WeakSet(), _RedisDistributedTransactionStorage_getLockKey = function _RedisDistributedTransactionStorage_getLockKey(key) {
530
+ return `${key}:lock`;
531
+ }, _RedisDistributedTransactionStorage_acquireLock = async function _RedisDistributedTransactionStorage_acquireLock(key, ttlSeconds = 2) {
532
+ const lockKey = __classPrivateFieldGet(this, _RedisDistributedTransactionStorage_instances, "m", _RedisDistributedTransactionStorage_getLockKey).call(this, key);
533
+ const result = await this.redisClient.set(lockKey, 1, "EX", ttlSeconds, "NX");
534
+ return result === "OK";
535
+ }, _RedisDistributedTransactionStorage_releaseLock = async function _RedisDistributedTransactionStorage_releaseLock(key) {
536
+ const lockKey = __classPrivateFieldGet(this, _RedisDistributedTransactionStorage_instances, "m", _RedisDistributedTransactionStorage_getLockKey).call(this, key);
537
+ await this.redisClient.del(lockKey);
538
+ }, _RedisDistributedTransactionStorage_preventRaceConditionExecutionIfNecessary = async function _RedisDistributedTransactionStorage_preventRaceConditionExecutionIfNecessary({ data, key, options, storedData, }) {
539
+ const isInitialCheckpoint = [utils_1.TransactionState.NOT_STARTED].includes(data.flow.state);
325
540
  /**
326
541
  * In case many execution can succeed simultaneously, we need to ensure that the latest
327
542
  * execution does continue if a previous execution is considered finished
328
543
  */
329
544
  const currentFlow = data.flow;
330
- const { flow: latestUpdatedFlow } = (await this.get(key, options)) ??
331
- { flow: {} };
332
- if (!isInitialCheckpoint && !(0, utils_1.isPresent)(latestUpdatedFlow)) {
545
+ let data_ = storedData ?? {};
546
+ if (!storedData) {
547
+ const rawData = await this.redisClient.get(key);
548
+ if (rawData) {
549
+ data_ = JSON.parse(rawData);
550
+ }
551
+ else {
552
+ // Pass cached raw data to avoid redundant Redis fetch
553
+ const getOptions = {
554
+ ...options,
555
+ isCancelling: !!data.flow.cancelledAt,
556
+ _cachedRawData: rawData,
557
+ };
558
+ data_ =
559
+ (await this.get(key, getOptions)) ??
560
+ { flow: {} };
561
+ }
562
+ }
563
+ const { flow: latestUpdatedFlow } = data_;
564
+ if (options?.stepId) {
565
+ const stepId = options.stepId;
566
+ const currentStep = data.flow.steps[stepId];
567
+ const latestStep = latestUpdatedFlow.steps?.[stepId];
568
+ if (latestStep && currentStep) {
569
+ const isCompensating = data.flow.state === utils_1.TransactionState.COMPENSATING;
570
+ const latestState = isCompensating
571
+ ? latestStep.compensate?.state
572
+ : latestStep.invoke?.state;
573
+ const shouldSkip = doneStates.has(latestState);
574
+ if (shouldSkip) {
575
+ throw new orchestration_1.SkipStepAlreadyFinishedError(`Step ${stepId} already finished by another execution`);
576
+ }
577
+ }
578
+ }
579
+ if (!isInitialCheckpoint &&
580
+ !(0, utils_1.isPresent)(latestUpdatedFlow) &&
581
+ !data.flow.metadata?.parentStepIdempotencyKey) {
333
582
  /**
334
583
  * the initial checkpoint expect no other checkpoint to have been stored.
335
584
  * In case it is not the initial one and another checkpoint is trying to
@@ -338,61 +587,12 @@ _RedisDistributedTransactionStorage_isWorkerMode = new WeakMap(), _RedisDistribu
338
587
  */
339
588
  throw new orchestration_1.SkipExecutionError("Already finished by another execution");
340
589
  }
341
- const currentFlowLastInvokingStepIndex = Object.values(currentFlow.steps).findIndex((step) => {
342
- return [
343
- utils_1.TransactionStepState.INVOKING,
344
- utils_1.TransactionStepState.NOT_STARTED,
345
- ].includes(step.invoke?.state);
346
- });
347
- const latestUpdatedFlowLastInvokingStepIndex = !latestUpdatedFlow.steps
348
- ? 1 // There is no other execution, so the current execution is the latest
349
- : Object.values(latestUpdatedFlow.steps ?? {}).findIndex((step) => {
350
- return [
351
- utils_1.TransactionStepState.INVOKING,
352
- utils_1.TransactionStepState.NOT_STARTED,
353
- ].includes(step.invoke?.state);
354
- });
355
- const currentFlowLastCompensatingStepIndex = Object.values(currentFlow.steps)
356
- .reverse()
357
- .findIndex((step) => {
358
- return [
359
- utils_1.TransactionStepState.COMPENSATING,
360
- utils_1.TransactionStepState.NOT_STARTED,
361
- ].includes(step.compensate?.state);
362
- });
363
- const latestUpdatedFlowLastCompensatingStepIndex = !latestUpdatedFlow.steps
364
- ? -1
365
- : Object.values(latestUpdatedFlow.steps ?? {})
366
- .reverse()
367
- .findIndex((step) => {
368
- return [
369
- utils_1.TransactionStepState.COMPENSATING,
370
- utils_1.TransactionStepState.NOT_STARTED,
371
- ].includes(step.compensate?.state);
372
- });
373
- const isLatestExecutionFinishedIndex = -1;
374
- const invokeShouldBeSkipped = (latestUpdatedFlowLastInvokingStepIndex ===
375
- isLatestExecutionFinishedIndex ||
376
- currentFlowLastInvokingStepIndex <
377
- latestUpdatedFlowLastInvokingStepIndex) &&
378
- currentFlowLastInvokingStepIndex !== isLatestExecutionFinishedIndex;
379
- const compensateShouldBeSkipped = currentFlowLastCompensatingStepIndex <
380
- latestUpdatedFlowLastCompensatingStepIndex &&
381
- currentFlowLastCompensatingStepIndex !== isLatestExecutionFinishedIndex &&
382
- latestUpdatedFlowLastCompensatingStepIndex !==
383
- isLatestExecutionFinishedIndex;
384
- if ((data.flow.state !== utils_1.TransactionState.COMPENSATING &&
385
- invokeShouldBeSkipped) ||
386
- (data.flow.state === utils_1.TransactionState.COMPENSATING &&
387
- compensateShouldBeSkipped) ||
388
- (latestUpdatedFlow.state === utils_1.TransactionState.COMPENSATING &&
389
- ![utils_1.TransactionState.REVERTED, utils_1.TransactionState.FAILED].includes(currentFlow.state) &&
390
- currentFlow.state !== latestUpdatedFlow.state) ||
391
- (latestUpdatedFlow.state === utils_1.TransactionState.REVERTED &&
392
- currentFlow.state !== utils_1.TransactionState.REVERTED) ||
393
- (latestUpdatedFlow.state === utils_1.TransactionState.FAILED &&
394
- currentFlow.state !== utils_1.TransactionState.FAILED)) {
395
- throw new orchestration_1.SkipExecutionError("Already finished by another execution");
590
+ // Ensure that the latest execution was not cancelled, otherwise we skip the execution
591
+ const latestTransactionCancelledAt = latestUpdatedFlow.cancelledAt;
592
+ const currentTransactionCancelledAt = currentFlow.cancelledAt;
593
+ if (!!latestTransactionCancelledAt &&
594
+ currentTransactionCancelledAt == null) {
595
+ throw new orchestration_1.SkipCancelledExecutionError("Workflow execution has been cancelled during the execution");
396
596
  }
397
597
  };
398
598
  //# sourceMappingURL=workflow-orchestrator-storage.js.map