@medusajs/workflow-engine-redis 3.0.0-snapshot-20250410112222 → 3.0.0-snapshot-20251104011621
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/loaders/redis.d.ts.map +1 -1
- package/dist/loaders/redis.js +10 -10
- package/dist/loaders/redis.js.map +1 -1
- package/dist/loaders/utils.d.ts.map +1 -1
- package/dist/loaders/utils.js +1 -1
- package/dist/loaders/utils.js.map +1 -1
- package/dist/migrations/Migration20231228143900.d.ts +1 -1
- package/dist/migrations/Migration20231228143900.d.ts.map +1 -1
- package/dist/migrations/Migration20231228143900.js +1 -1
- package/dist/migrations/Migration20231228143900.js.map +1 -1
- package/dist/migrations/Migration20241206123341.d.ts +1 -1
- package/dist/migrations/Migration20241206123341.d.ts.map +1 -1
- package/dist/migrations/Migration20241206123341.js +1 -1
- package/dist/migrations/Migration20241206123341.js.map +1 -1
- package/dist/migrations/Migration20250120111059.d.ts +1 -1
- package/dist/migrations/Migration20250120111059.d.ts.map +1 -1
- package/dist/migrations/Migration20250120111059.js +1 -1
- package/dist/migrations/Migration20250120111059.js.map +1 -1
- package/dist/migrations/Migration20250128174354.d.ts +1 -1
- package/dist/migrations/Migration20250128174354.d.ts.map +1 -1
- package/dist/migrations/Migration20250128174354.js +1 -1
- package/dist/migrations/Migration20250128174354.js.map +1 -1
- package/dist/migrations/Migration20250505101505.d.ts +6 -0
- package/dist/migrations/Migration20250505101505.d.ts.map +1 -0
- package/dist/migrations/Migration20250505101505.js +40 -0
- package/dist/migrations/Migration20250505101505.js.map +1 -0
- package/dist/migrations/Migration20250819110923.d.ts +6 -0
- package/dist/migrations/Migration20250819110923.d.ts.map +1 -0
- package/dist/migrations/Migration20250819110923.js +14 -0
- package/dist/migrations/Migration20250819110923.js.map +1 -0
- package/dist/migrations/Migration20250819110924.d.ts +6 -0
- package/dist/migrations/Migration20250819110924.d.ts.map +1 -0
- package/dist/migrations/Migration20250819110924.js +16 -0
- package/dist/migrations/Migration20250819110924.js.map +1 -0
- package/dist/migrations/Migration20250908080326.d.ts +6 -0
- package/dist/migrations/Migration20250908080326.d.ts.map +1 -0
- package/dist/migrations/Migration20250908080326.js +20 -0
- package/dist/migrations/Migration20250908080326.js.map +1 -0
- package/dist/models/workflow-execution.d.ts +1 -0
- package/dist/models/workflow-execution.d.ts.map +1 -1
- package/dist/models/workflow-execution.js +22 -1
- package/dist/models/workflow-execution.js.map +1 -1
- package/dist/services/workflow-orchestrator.d.ts +16 -3
- package/dist/services/workflow-orchestrator.d.ts.map +1 -1
- package/dist/services/workflow-orchestrator.js +247 -118
- package/dist/services/workflow-orchestrator.js.map +1 -1
- package/dist/services/workflows-module.d.ts +114 -9
- package/dist/services/workflows-module.d.ts.map +1 -1
- package/dist/services/workflows-module.js +115 -52
- package/dist/services/workflows-module.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/workflow-orchestrator-storage.d.ts +10 -3
- package/dist/utils/workflow-orchestrator-storage.d.ts.map +1 -1
- package/dist/utils/workflow-orchestrator-storage.js +334 -134
- package/dist/utils/workflow-orchestrator-storage.js.map +1 -1
- 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,25 +60,33 @@ 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
|
|
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
|
|
@@ -65,7 +94,14 @@ class RedisDistributedTransactionStorage {
|
|
|
65
94
|
this.worker = new bullmq_1.Worker(this.queueName, async (job) => {
|
|
66
95
|
this.logger_.debug(`executing job ${job.name} from queue ${this.queueName} with the following data: ${JSON.stringify(job.data)}`);
|
|
67
96
|
if (allowedJobs.includes(job.name)) {
|
|
68
|
-
|
|
97
|
+
try {
|
|
98
|
+
await this.executeTransaction(job.data.workflowId, job.data.transactionId, job.data.transactionMetadata);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (!orchestration_1.SkipExecutionError.isSkipExecutionError(error)) {
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
69
105
|
}
|
|
70
106
|
if (job.name === JobType.SCHEDULE) {
|
|
71
107
|
// Remove repeatable job from the old queue since now we have a queue dedicated to scheduled jobs
|
|
@@ -77,16 +113,101 @@ class RedisDistributedTransactionStorage {
|
|
|
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
|
+
let currentStepsIsAsync = false;
|
|
171
|
+
const targetStates = isFlowInvoking
|
|
172
|
+
? new Set([
|
|
173
|
+
utils_1.TransactionStepState.INVOKING,
|
|
174
|
+
utils_1.TransactionStepState.DONE,
|
|
175
|
+
utils_1.TransactionStepState.FAILED,
|
|
176
|
+
])
|
|
177
|
+
: new Set([utils_1.TransactionStepState.COMPENSATING]);
|
|
178
|
+
for (let i = stepsArray.length - 1; i >= 0; i--) {
|
|
179
|
+
const step = stepsArray[i];
|
|
180
|
+
if (step.id === "_root") {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
const isTargetState = targetStates.has(step.invoke?.state);
|
|
184
|
+
if (isTargetState && !currentStep) {
|
|
185
|
+
currentStep = step;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
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?.async === true) {
|
|
196
|
+
currentStepsIsAsync = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!(isNotStarted || isFinished || isWaitingToCompensate) &&
|
|
202
|
+
!currentStepsIsAsync &&
|
|
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
|
-
|
|
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
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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;
|
|
166
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
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return new orchestration_1.TransactionCheckpoint(flow ?? trx.execution, trx.context?.data, errors ?? trx.context?.errors);
|
|
167
297
|
}
|
|
168
|
-
return
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
key,
|
|
184
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
383
|
+
await (0, utils_1.promiseAll)([this.saveToDb(data, retentionTime), execPipeline()]);
|
|
198
384
|
}
|
|
385
|
+
return data;
|
|
199
386
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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(),
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|