pg-workflows 0.8.3 → 0.10.0
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/client.entry.cjs +73 -14
- package/dist/client.entry.d.cts +37 -10
- package/dist/client.entry.d.ts +37 -10
- package/dist/client.entry.js +1 -1
- package/dist/client.entry.js.map +9 -9
- package/dist/index.cjs +455 -113
- package/dist/index.d.cts +80 -21
- package/dist/index.d.ts +80 -21
- package/dist/index.js +390 -101
- package/dist/index.js.map +11 -11
- package/dist/shared/{chunk-2xy8z3xp.js → chunk-nygamc7b.js} +80 -21
- package/dist/shared/chunk-nygamc7b.js.map +16 -0
- package/package.json +1 -1
- package/dist/shared/chunk-2xy8z3xp.js.map +0 -16
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
DEFAULT_PGBOSS_SCHEMA,
|
|
3
3
|
PAUSE_EVENT_NAME,
|
|
4
4
|
StepType,
|
|
5
|
+
WORKFLOW_RUN_DLQ_QUEUE_NAME,
|
|
5
6
|
WORKFLOW_RUN_QUEUE_NAME,
|
|
6
7
|
WorkflowClient,
|
|
7
8
|
WorkflowEngineError,
|
|
@@ -11,11 +12,16 @@ import {
|
|
|
11
12
|
getWorkflowRun,
|
|
12
13
|
getWorkflowRuns,
|
|
13
14
|
insertWorkflowRun,
|
|
15
|
+
invokeChildWorkflowTimelineKey,
|
|
16
|
+
isInvokeChildWorkflowTimelineEntry,
|
|
14
17
|
runMigrations,
|
|
15
18
|
updateWorkflowRun,
|
|
19
|
+
validateResourceId,
|
|
20
|
+
validateWorkflowId,
|
|
21
|
+
waitForTimelineKey,
|
|
16
22
|
withPostgresTransaction,
|
|
17
23
|
workflow
|
|
18
|
-
} from "./shared/chunk-
|
|
24
|
+
} from "./shared/chunk-nygamc7b.js";
|
|
19
25
|
// src/duration.ts
|
|
20
26
|
import parse from "parse-duration";
|
|
21
27
|
var MS_PER_SECOND = 1000;
|
|
@@ -91,7 +97,7 @@ function parseWorkflowHandler(handler) {
|
|
|
91
97
|
const propertyAccess = node.expression;
|
|
92
98
|
const objectName = propertyAccess.expression.getText(sourceFile);
|
|
93
99
|
const methodName = propertyAccess.name.text;
|
|
94
|
-
if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll")) {
|
|
100
|
+
if (objectName === "step" && (methodName === "run" || methodName === "waitFor" || methodName === "pause" || methodName === "waitUntil" || methodName === "delay" || methodName === "sleep" || methodName === "poll" || methodName === "invokeChildWorkflow")) {
|
|
95
101
|
const firstArg = node.arguments[0];
|
|
96
102
|
if (firstArg) {
|
|
97
103
|
const { id, isDynamic } = extractStepId(firstArg);
|
|
@@ -124,13 +130,21 @@ var StepTypeToIcon = {
|
|
|
124
130
|
["pause" /* PAUSE */]: "⏸",
|
|
125
131
|
["waitUntil" /* WAIT_UNTIL */]: "⏲",
|
|
126
132
|
["delay" /* DELAY */]: "⏱",
|
|
127
|
-
["poll" /* POLL */]: "↻"
|
|
133
|
+
["poll" /* POLL */]: "↻",
|
|
134
|
+
["invokeChildWorkflow" /* INVOKE_CHILD_WORKFLOW */]: "↪"
|
|
128
135
|
};
|
|
129
136
|
var defaultLogger = {
|
|
130
137
|
log: (_message) => console.warn(_message),
|
|
131
138
|
error: (message, error) => console.error(message, error)
|
|
132
139
|
};
|
|
133
140
|
var defaultExpireInSeconds = process.env.WORKFLOW_RUN_EXPIRE_IN_SECONDS ? Number.parseInt(process.env.WORKFLOW_RUN_EXPIRE_IN_SECONDS, 10) : 5 * 60;
|
|
141
|
+
var retrySendOptions = (maxRetries) => ({
|
|
142
|
+
retryLimit: maxRetries,
|
|
143
|
+
retryBackoff: true,
|
|
144
|
+
retryDelay: 1
|
|
145
|
+
});
|
|
146
|
+
var getInvokeChildWorkflowEventName = (childRunId) => `__invoke_child_workflow_completed:${childRunId}`;
|
|
147
|
+
var defaultHeartbeatSeconds = process.env.WORKFLOW_RUN_HEARTBEAT_SECONDS ? Number.parseInt(process.env.WORKFLOW_RUN_HEARTBEAT_SECONDS, 10) : 30;
|
|
134
148
|
|
|
135
149
|
class WorkflowEngine {
|
|
136
150
|
boss;
|
|
@@ -164,7 +178,10 @@ class WorkflowEngine {
|
|
|
164
178
|
}
|
|
165
179
|
this.db = this.boss.getDb();
|
|
166
180
|
}
|
|
167
|
-
async start(asEngine = true, {
|
|
181
|
+
async start(asEngine = true, {
|
|
182
|
+
batchSize = 1,
|
|
183
|
+
heartbeatSeconds = defaultHeartbeatSeconds
|
|
184
|
+
} = {}) {
|
|
168
185
|
if (this._started) {
|
|
169
186
|
return;
|
|
170
187
|
}
|
|
@@ -175,13 +192,21 @@ class WorkflowEngine {
|
|
|
175
192
|
await this.registerWorkflow(workflow2);
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
|
-
|
|
195
|
+
const mainQueueOptions = {
|
|
196
|
+
retryLimit: 0,
|
|
197
|
+
deadLetter: WORKFLOW_RUN_DLQ_QUEUE_NAME,
|
|
198
|
+
heartbeatSeconds
|
|
199
|
+
};
|
|
200
|
+
await this.boss.createQueue(WORKFLOW_RUN_DLQ_QUEUE_NAME, { retryLimit: 0 });
|
|
201
|
+
await this.boss.createQueue(WORKFLOW_RUN_QUEUE_NAME, mainQueueOptions);
|
|
202
|
+
await this.boss.updateQueue(WORKFLOW_RUN_QUEUE_NAME, mainQueueOptions);
|
|
179
203
|
const numWorkers = +(process.env.WORKFLOW_RUN_WORKERS ?? 3);
|
|
180
204
|
if (asEngine) {
|
|
181
|
-
|
|
182
|
-
await this.boss.work(WORKFLOW_RUN_QUEUE_NAME, { pollingIntervalSeconds: 0.5, batchSize }, (job) => this.handleWorkflowRun(job));
|
|
205
|
+
await Promise.all(Array.from({ length: numWorkers }, (_, i) => this.boss.work(WORKFLOW_RUN_QUEUE_NAME, { pollingIntervalSeconds: 0.5, batchSize, includeMetadata: true }, (jobs) => this.handleWorkflowRun(jobs)).then(() => {
|
|
183
206
|
this.logger.log(`Worker ${i + 1}/${numWorkers} started for queue ${WORKFLOW_RUN_QUEUE_NAME}`);
|
|
184
|
-
}
|
|
207
|
+
})));
|
|
208
|
+
await this.boss.work(WORKFLOW_RUN_DLQ_QUEUE_NAME, { pollingIntervalSeconds: 0.5, batchSize: 1 }, (jobs) => this.handleWorkflowRunDlq(jobs));
|
|
209
|
+
this.logger.log(`Worker started for queue ${WORKFLOW_RUN_DLQ_QUEUE_NAME}`);
|
|
185
210
|
}
|
|
186
211
|
this._started = true;
|
|
187
212
|
this.logger.log("Workflow engine started!");
|
|
@@ -224,29 +249,57 @@ class WorkflowEngine {
|
|
|
224
249
|
this.workflows.clear();
|
|
225
250
|
return this;
|
|
226
251
|
}
|
|
227
|
-
|
|
228
|
-
let workflowId;
|
|
229
|
-
let input;
|
|
230
|
-
let resourceId;
|
|
231
|
-
let idempotencyKey;
|
|
232
|
-
let options;
|
|
252
|
+
resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg) {
|
|
233
253
|
if (typeof refOrParams === "function" && "id" in refOrParams) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
workflowId = params.workflowId;
|
|
242
|
-
input = params.input;
|
|
243
|
-
resourceId = params.resourceId;
|
|
244
|
-
idempotencyKey = params.idempotencyKey;
|
|
245
|
-
options = params.options;
|
|
254
|
+
return {
|
|
255
|
+
workflowId: refOrParams.id,
|
|
256
|
+
input: inputArg,
|
|
257
|
+
options: optionsArg,
|
|
258
|
+
resourceId: optionsArg?.resourceId,
|
|
259
|
+
idempotencyKey: optionsArg?.idempotencyKey
|
|
260
|
+
};
|
|
246
261
|
}
|
|
262
|
+
const params = refOrParams;
|
|
263
|
+
return {
|
|
264
|
+
workflowId: params.workflowId,
|
|
265
|
+
input: params.input,
|
|
266
|
+
resourceId: params.resourceId ?? params.options?.resourceId,
|
|
267
|
+
idempotencyKey: params.idempotencyKey ?? params.options?.idempotencyKey,
|
|
268
|
+
options: params.options
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async startWorkflow(refOrParams, inputArg, optionsArg) {
|
|
272
|
+
const { workflowId, input, resourceId, idempotencyKey, options } = this.resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg);
|
|
247
273
|
if (!this._started) {
|
|
248
274
|
await this.start(false, { batchSize: options?.batchSize ?? 1 });
|
|
249
275
|
}
|
|
276
|
+
const { run } = await this.createWorkflowRun({
|
|
277
|
+
workflowId,
|
|
278
|
+
input,
|
|
279
|
+
resourceId,
|
|
280
|
+
idempotencyKey,
|
|
281
|
+
options
|
|
282
|
+
});
|
|
283
|
+
this.logger.log("Started workflow run", {
|
|
284
|
+
runId: run.id,
|
|
285
|
+
workflowId
|
|
286
|
+
});
|
|
287
|
+
return run;
|
|
288
|
+
}
|
|
289
|
+
async createWorkflowRun({
|
|
290
|
+
workflowId,
|
|
291
|
+
input,
|
|
292
|
+
resourceId,
|
|
293
|
+
idempotencyKey,
|
|
294
|
+
options,
|
|
295
|
+
parentRunId,
|
|
296
|
+
parentStepId,
|
|
297
|
+
parentResourceId,
|
|
298
|
+
enqueue = true,
|
|
299
|
+
db
|
|
300
|
+
}) {
|
|
301
|
+
validateWorkflowId(workflowId);
|
|
302
|
+
validateResourceId(resourceId);
|
|
250
303
|
const workflow2 = this.workflows.get(workflowId);
|
|
251
304
|
if (!workflow2) {
|
|
252
305
|
throw new WorkflowEngineError(`Unknown workflow ${workflowId}`);
|
|
@@ -263,37 +316,60 @@ class WorkflowEngine {
|
|
|
263
316
|
}
|
|
264
317
|
}
|
|
265
318
|
const initialStepId = workflow2.steps[0]?.id ?? "__start__";
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
};
|
|
285
|
-
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
286
|
-
startAfter: new Date,
|
|
287
|
-
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
|
|
288
|
-
});
|
|
319
|
+
const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : workflow2.timeout ? new Date(Date.now() + workflow2.timeout) : null;
|
|
320
|
+
const insertRun = async (targetDb) => await insertWorkflowRun({
|
|
321
|
+
resourceId,
|
|
322
|
+
workflowId,
|
|
323
|
+
currentStepId: initialStepId,
|
|
324
|
+
status: "running" /* RUNNING */,
|
|
325
|
+
input,
|
|
326
|
+
maxRetries: options?.retries ?? workflow2.retries ?? 0,
|
|
327
|
+
timeoutAt,
|
|
328
|
+
idempotencyKey,
|
|
329
|
+
parentRunId,
|
|
330
|
+
parentStepId,
|
|
331
|
+
parentResourceId
|
|
332
|
+
}, targetDb);
|
|
333
|
+
const insertAndEnqueue = async (targetDb) => {
|
|
334
|
+
const result = await insertRun(targetDb);
|
|
335
|
+
if (enqueue && result.created) {
|
|
336
|
+
await this.enqueueWorkflowRun(result.run, options, targetDb);
|
|
289
337
|
}
|
|
290
|
-
return
|
|
291
|
-
}
|
|
292
|
-
this.
|
|
338
|
+
return result;
|
|
339
|
+
};
|
|
340
|
+
const { run, created } = db ? await insertAndEnqueue(db) : await withPostgresTransaction(this.boss.getDb(), insertAndEnqueue, this.pool);
|
|
341
|
+
return { run, created };
|
|
342
|
+
}
|
|
343
|
+
async enqueueWorkflowRun(run, options, db) {
|
|
344
|
+
const job = {
|
|
293
345
|
runId: run.id,
|
|
294
|
-
|
|
346
|
+
resourceId: run.resourceId ?? undefined,
|
|
347
|
+
workflowId: run.workflowId,
|
|
348
|
+
input: run.input
|
|
349
|
+
};
|
|
350
|
+
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
351
|
+
startAfter: new Date,
|
|
352
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
|
|
353
|
+
...retrySendOptions(run.maxRetries),
|
|
354
|
+
...db ? { db } : {}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async notifyParentOfChildTerminalRun(childRun) {
|
|
358
|
+
if (!childRun.parentRunId || !childRun.parentStepId) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const parentRun = await getWorkflowRun({
|
|
362
|
+
runId: childRun.parentRunId,
|
|
363
|
+
resourceId: childRun.parentResourceId ?? undefined
|
|
364
|
+
}, { db: this.db });
|
|
365
|
+
if (!parentRun || parentRun.status === "completed" /* COMPLETED */ || parentRun.status === "failed" /* FAILED */ || parentRun.status === "cancelled" /* CANCELLED */) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
await this.triggerEvent({
|
|
369
|
+
runId: parentRun.id,
|
|
370
|
+
resourceId: parentRun.resourceId ?? undefined,
|
|
371
|
+
eventName: getInvokeChildWorkflowEventName(childRun.id)
|
|
295
372
|
});
|
|
296
|
-
return run;
|
|
297
373
|
}
|
|
298
374
|
async pauseWorkflow({
|
|
299
375
|
runId,
|
|
@@ -325,6 +401,9 @@ class WorkflowEngine {
|
|
|
325
401
|
if (current.status !== "paused" /* PAUSED */) {
|
|
326
402
|
throw new WorkflowEngineError(`Cannot resume workflow run in '${current.status}' status, must be 'paused'`, current.workflowId, runId);
|
|
327
403
|
}
|
|
404
|
+
if (this.getInvokeChildWorkflowStepEntry(current.timeline, current.currentStepId)) {
|
|
405
|
+
return current;
|
|
406
|
+
}
|
|
328
407
|
return this.triggerEvent({
|
|
329
408
|
runId,
|
|
330
409
|
resourceId,
|
|
@@ -344,6 +423,9 @@ class WorkflowEngine {
|
|
|
344
423
|
return run;
|
|
345
424
|
}
|
|
346
425
|
const stepId = run.currentStepId;
|
|
426
|
+
if (this.getInvokeChildWorkflowStepEntry(run.timeline, stepId)) {
|
|
427
|
+
return run;
|
|
428
|
+
}
|
|
347
429
|
const waitForStep = this.getWaitForStepEntry(run.timeline, stepId);
|
|
348
430
|
if (!waitForStep) {
|
|
349
431
|
return run;
|
|
@@ -392,6 +474,7 @@ class WorkflowEngine {
|
|
|
392
474
|
expectedStatuses: ["pending" /* PENDING */, "running" /* RUNNING */, "paused" /* PAUSED */]
|
|
393
475
|
});
|
|
394
476
|
this.logger.log(`cancelled workflow run with id ${runId}`);
|
|
477
|
+
await this.notifyParentOfChildTerminalRun(run);
|
|
395
478
|
return run;
|
|
396
479
|
}
|
|
397
480
|
async triggerEvent({
|
|
@@ -415,7 +498,8 @@ class WorkflowEngine {
|
|
|
415
498
|
}
|
|
416
499
|
};
|
|
417
500
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
418
|
-
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
|
|
501
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds,
|
|
502
|
+
...retrySendOptions(run.maxRetries)
|
|
419
503
|
});
|
|
420
504
|
this.logger.log(`event ${eventName} sent for workflow run with id ${runId}`);
|
|
421
505
|
return run;
|
|
@@ -494,7 +578,7 @@ class WorkflowEngine {
|
|
|
494
578
|
return run.resourceId ?? undefined;
|
|
495
579
|
}
|
|
496
580
|
async handleWorkflowRun([job]) {
|
|
497
|
-
const { runId = "", resourceId, workflowId = "",
|
|
581
|
+
const { runId = "", resourceId, workflowId = "", event } = job?.data ?? {};
|
|
498
582
|
let run;
|
|
499
583
|
let scopedResourceId;
|
|
500
584
|
try {
|
|
@@ -517,6 +601,14 @@ class WorkflowEngine {
|
|
|
517
601
|
throw new WorkflowEngineError(`Workflow run ${runId} does not match job workflowId ${workflowId}`, workflowId, runId);
|
|
518
602
|
}
|
|
519
603
|
scopedResourceId = this.resolveScopedResourceId(resourceId, run);
|
|
604
|
+
if (job?.retryCount !== undefined && run.retryCount !== job.retryCount) {
|
|
605
|
+
await this.updateRun({
|
|
606
|
+
runId,
|
|
607
|
+
resourceId: scopedResourceId,
|
|
608
|
+
data: { retryCount: job.retryCount }
|
|
609
|
+
});
|
|
610
|
+
run = { ...run, retryCount: job.retryCount };
|
|
611
|
+
}
|
|
520
612
|
if (run.status === "cancelled" /* CANCELLED */) {
|
|
521
613
|
this.logger.log(`Workflow run ${runId} is cancelled, skipping`);
|
|
522
614
|
return;
|
|
@@ -620,6 +712,22 @@ class WorkflowEngine {
|
|
|
620
712
|
}
|
|
621
713
|
const timeoutMs = options?.timeout ? parseDuration(options.timeout) : undefined;
|
|
622
714
|
return this.pollStep({ run, stepId, conditionFn, intervalMs, timeoutMs });
|
|
715
|
+
},
|
|
716
|
+
invokeChildWorkflow: async (stepId, refOrParams, inputArg, optionsArg) => {
|
|
717
|
+
if (!run) {
|
|
718
|
+
throw new WorkflowEngineError("Missing workflow run", workflowId, runId);
|
|
719
|
+
}
|
|
720
|
+
const resolvedChildCall = this.resolveWorkflowRunParameters(refOrParams, inputArg, optionsArg);
|
|
721
|
+
const childWorkflowInvocation = {
|
|
722
|
+
run,
|
|
723
|
+
stepId,
|
|
724
|
+
workflowId: resolvedChildCall.workflowId,
|
|
725
|
+
input: resolvedChildCall.input,
|
|
726
|
+
options: resolvedChildCall.options,
|
|
727
|
+
resourceId: resolvedChildCall.resourceId,
|
|
728
|
+
idempotencyKey: resolvedChildCall.idempotencyKey
|
|
729
|
+
};
|
|
730
|
+
return this.invokeChildWorkflowStep(childWorkflowInvocation);
|
|
623
731
|
}
|
|
624
732
|
};
|
|
625
733
|
let step = { ...baseStep };
|
|
@@ -644,7 +752,7 @@ class WorkflowEngine {
|
|
|
644
752
|
const shouldComplete = run.status === "running" /* RUNNING */ && (noParsedSteps || isLastParsedStep || hasPluginSteps && result !== undefined);
|
|
645
753
|
if (shouldComplete) {
|
|
646
754
|
const normalizedResult = result === undefined ? {} : result;
|
|
647
|
-
await this.updateRun({
|
|
755
|
+
const completedRun = await this.updateRun({
|
|
648
756
|
runId,
|
|
649
757
|
resourceId: scopedResourceId,
|
|
650
758
|
data: {
|
|
@@ -654,56 +762,244 @@ class WorkflowEngine {
|
|
|
654
762
|
jobId: job?.id
|
|
655
763
|
}
|
|
656
764
|
});
|
|
765
|
+
await this.notifyParentOfChildTerminalRun(completedRun);
|
|
657
766
|
this.logger.log("Workflow run completed.", {
|
|
658
767
|
runId,
|
|
659
768
|
workflowId
|
|
660
769
|
});
|
|
661
770
|
}
|
|
662
771
|
} catch (error) {
|
|
663
|
-
if (run && run.retryCount < run.maxRetries) {
|
|
664
|
-
await this.updateRun({
|
|
665
|
-
runId,
|
|
666
|
-
resourceId: scopedResourceId,
|
|
667
|
-
data: {
|
|
668
|
-
retryCount: run.retryCount + 1,
|
|
669
|
-
jobId: job?.id
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
const retryDelay = 2 ** run.retryCount * 1000;
|
|
673
|
-
const pgBossJob = {
|
|
674
|
-
runId,
|
|
675
|
-
resourceId: scopedResourceId,
|
|
676
|
-
workflowId,
|
|
677
|
-
input
|
|
678
|
-
};
|
|
679
|
-
await this.boss?.send("workflow-run", pgBossJob, {
|
|
680
|
-
startAfter: new Date(Date.now() + retryDelay),
|
|
681
|
-
expireInSeconds: defaultExpireInSeconds
|
|
682
|
-
});
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
772
|
if (runId) {
|
|
686
|
-
await this.updateRun({
|
|
773
|
+
const updatedRun = await this.updateRun({
|
|
687
774
|
runId,
|
|
688
775
|
resourceId: scopedResourceId,
|
|
689
776
|
data: {
|
|
690
|
-
status: "failed" /* FAILED */,
|
|
691
777
|
error: error instanceof Error ? error.message : String(error),
|
|
692
778
|
jobId: job?.id
|
|
693
779
|
}
|
|
694
780
|
});
|
|
781
|
+
if (updatedRun.status === "completed" /* COMPLETED */ || updatedRun.status === "failed" /* FAILED */ || updatedRun.status === "cancelled" /* CANCELLED */) {
|
|
782
|
+
await this.notifyParentOfChildTerminalRun(updatedRun);
|
|
783
|
+
}
|
|
695
784
|
}
|
|
696
785
|
throw error;
|
|
697
786
|
}
|
|
698
787
|
}
|
|
788
|
+
async handleWorkflowRunDlq([job]) {
|
|
789
|
+
const { runId } = job?.data ?? {};
|
|
790
|
+
if (!runId)
|
|
791
|
+
return;
|
|
792
|
+
const run = await getWorkflowRun({ runId }, { db: this.db });
|
|
793
|
+
if (!run || run.status !== "running" /* RUNNING */)
|
|
794
|
+
return;
|
|
795
|
+
const failedRun = await this.updateRun({
|
|
796
|
+
runId,
|
|
797
|
+
resourceId: run.resourceId ?? undefined,
|
|
798
|
+
data: {
|
|
799
|
+
status: "failed" /* FAILED */,
|
|
800
|
+
error: run.error ?? "Workflow run worker died or job expired before completion"
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
await this.notifyParentOfChildTerminalRun(failedRun);
|
|
804
|
+
this.logger.log("Marked stuck workflow run as failed", {
|
|
805
|
+
runId,
|
|
806
|
+
workflowId: run.workflowId
|
|
807
|
+
});
|
|
808
|
+
}
|
|
699
809
|
getCachedStepEntry(timeline, stepId) {
|
|
700
810
|
const stepEntry = timeline[stepId];
|
|
701
811
|
return stepEntry && typeof stepEntry === "object" && "output" in stepEntry ? stepEntry : null;
|
|
702
812
|
}
|
|
703
813
|
getWaitForStepEntry(timeline, stepId) {
|
|
704
|
-
const entry = timeline[
|
|
814
|
+
const entry = timeline[waitForTimelineKey(stepId)];
|
|
705
815
|
return entry && typeof entry === "object" && "waitFor" in entry ? entry : null;
|
|
706
816
|
}
|
|
817
|
+
getInvokeChildWorkflowStepEntry(timeline, stepId) {
|
|
818
|
+
const entry = timeline[invokeChildWorkflowTimelineKey(stepId)];
|
|
819
|
+
return isInvokeChildWorkflowTimelineEntry(entry) ? entry : null;
|
|
820
|
+
}
|
|
821
|
+
getCompletedChildOutput(childRun) {
|
|
822
|
+
return childRun.output === undefined ? {} : childRun.output;
|
|
823
|
+
}
|
|
824
|
+
throwForNonCompletedChild(childRun) {
|
|
825
|
+
throw new WorkflowEngineError(`Child workflow ${childRun.workflowId} ${childRun.status}${childRun.error ? `: ${childRun.error}` : ""}`, childRun.workflowId, childRun.id);
|
|
826
|
+
}
|
|
827
|
+
assertInvokeChildWorkflowStepOwnership({
|
|
828
|
+
childRun,
|
|
829
|
+
parentRun,
|
|
830
|
+
stepId,
|
|
831
|
+
workflowId
|
|
832
|
+
}) {
|
|
833
|
+
const expectedParentResourceId = parentRun.resourceId ?? null;
|
|
834
|
+
const matches = childRun.workflowId === workflowId && childRun.parentRunId === parentRun.id && childRun.parentStepId === stepId && childRun.parentResourceId === expectedParentResourceId;
|
|
835
|
+
if (!matches) {
|
|
836
|
+
throw new WorkflowEngineError(`Idempotency key resolved to workflow run ${childRun.id}, which does not belong to invokeChildWorkflow step '${stepId}'`, workflowId, parentRun.id);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
async invokeChildWorkflowStep({
|
|
840
|
+
run,
|
|
841
|
+
stepId,
|
|
842
|
+
workflowId,
|
|
843
|
+
input,
|
|
844
|
+
resourceId,
|
|
845
|
+
idempotencyKey,
|
|
846
|
+
options
|
|
847
|
+
}) {
|
|
848
|
+
let invokeOutput;
|
|
849
|
+
let hasInvokeOutput = false;
|
|
850
|
+
const childResourceId = resourceId ?? run.resourceId ?? undefined;
|
|
851
|
+
const childIdempotencyKey = idempotencyKey;
|
|
852
|
+
await withPostgresTransaction(this.db, async (db) => {
|
|
853
|
+
const lockedRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
|
|
854
|
+
if (lockedRun.status === "cancelled" /* CANCELLED */ || lockedRun.status === "paused" /* PAUSED */ || lockedRun.status === "failed" /* FAILED */) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const lockedCached = this.getCachedStepEntry(lockedRun.timeline, stepId);
|
|
858
|
+
if (lockedCached?.output !== undefined) {
|
|
859
|
+
invokeOutput = lockedCached.output;
|
|
860
|
+
hasInvokeOutput = true;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const lockedInvoke = this.getInvokeChildWorkflowStepEntry(lockedRun.timeline, stepId);
|
|
864
|
+
if (lockedInvoke) {
|
|
865
|
+
const existingChildResourceId = "childResourceId" in lockedInvoke.invokeChildWorkflow ? lockedInvoke.invokeChildWorkflow.childResourceId ?? undefined : childResourceId;
|
|
866
|
+
const existingChildRun = await this.getRun({
|
|
867
|
+
runId: lockedInvoke.invokeChildWorkflow.childRunId,
|
|
868
|
+
resourceId: existingChildResourceId
|
|
869
|
+
});
|
|
870
|
+
if (existingChildRun.status === "completed" /* COMPLETED */) {
|
|
871
|
+
invokeOutput = this.getCompletedChildOutput(existingChildRun);
|
|
872
|
+
hasInvokeOutput = true;
|
|
873
|
+
await this.updateRun({
|
|
874
|
+
runId: run.id,
|
|
875
|
+
resourceId: run.resourceId ?? undefined,
|
|
876
|
+
data: {
|
|
877
|
+
timeline: merge(lockedRun.timeline, {
|
|
878
|
+
[stepId]: {
|
|
879
|
+
output: invokeOutput,
|
|
880
|
+
timestamp: new Date
|
|
881
|
+
}
|
|
882
|
+
})
|
|
883
|
+
}
|
|
884
|
+
}, { db });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (existingChildRun.status === "failed" /* FAILED */ || existingChildRun.status === "cancelled" /* CANCELLED */) {
|
|
888
|
+
this.throwForNonCompletedChild(existingChildRun);
|
|
889
|
+
}
|
|
890
|
+
await this.pauseRunForWait({
|
|
891
|
+
run: lockedRun,
|
|
892
|
+
stepId,
|
|
893
|
+
eventName: getInvokeChildWorkflowEventName(existingChildRun.id),
|
|
894
|
+
skipOutput: true,
|
|
895
|
+
db
|
|
896
|
+
});
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const result = await this.createWorkflowRun({
|
|
900
|
+
workflowId,
|
|
901
|
+
input,
|
|
902
|
+
resourceId: childResourceId,
|
|
903
|
+
idempotencyKey: childIdempotencyKey,
|
|
904
|
+
options,
|
|
905
|
+
parentRunId: run.id,
|
|
906
|
+
parentStepId: stepId,
|
|
907
|
+
parentResourceId: run.resourceId ?? undefined,
|
|
908
|
+
enqueue: true,
|
|
909
|
+
db
|
|
910
|
+
});
|
|
911
|
+
const childRun = result.run;
|
|
912
|
+
if (!result.created) {
|
|
913
|
+
this.assertInvokeChildWorkflowStepOwnership({
|
|
914
|
+
childRun,
|
|
915
|
+
parentRun: lockedRun,
|
|
916
|
+
stepId,
|
|
917
|
+
workflowId
|
|
918
|
+
});
|
|
919
|
+
if (childRun.status === "completed" /* COMPLETED */) {
|
|
920
|
+
invokeOutput = this.getCompletedChildOutput(childRun);
|
|
921
|
+
hasInvokeOutput = true;
|
|
922
|
+
await this.updateRun({
|
|
923
|
+
runId: run.id,
|
|
924
|
+
resourceId: run.resourceId ?? undefined,
|
|
925
|
+
data: {
|
|
926
|
+
timeline: merge(lockedRun.timeline, {
|
|
927
|
+
[invokeChildWorkflowTimelineKey(stepId)]: {
|
|
928
|
+
invokeChildWorkflow: {
|
|
929
|
+
childRunId: childRun.id,
|
|
930
|
+
childWorkflowId: childRun.workflowId,
|
|
931
|
+
childResourceId: childRun.resourceId
|
|
932
|
+
},
|
|
933
|
+
timestamp: new Date
|
|
934
|
+
},
|
|
935
|
+
[stepId]: {
|
|
936
|
+
output: invokeOutput,
|
|
937
|
+
timestamp: new Date
|
|
938
|
+
}
|
|
939
|
+
})
|
|
940
|
+
}
|
|
941
|
+
}, { db });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (childRun.status === "failed" /* FAILED */ || childRun.status === "cancelled" /* CANCELLED */) {
|
|
945
|
+
this.throwForNonCompletedChild(childRun);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
await this.pauseRunForWait({
|
|
949
|
+
run: lockedRun,
|
|
950
|
+
stepId,
|
|
951
|
+
eventName: getInvokeChildWorkflowEventName(childRun.id),
|
|
952
|
+
skipOutput: true,
|
|
953
|
+
db,
|
|
954
|
+
timeline: merge(lockedRun.timeline, {
|
|
955
|
+
[invokeChildWorkflowTimelineKey(stepId)]: {
|
|
956
|
+
invokeChildWorkflow: {
|
|
957
|
+
childRunId: childRun.id,
|
|
958
|
+
childWorkflowId: childRun.workflowId,
|
|
959
|
+
childResourceId: childRun.resourceId
|
|
960
|
+
},
|
|
961
|
+
timestamp: new Date
|
|
962
|
+
}
|
|
963
|
+
})
|
|
964
|
+
});
|
|
965
|
+
}, this.pool);
|
|
966
|
+
if (hasInvokeOutput) {
|
|
967
|
+
return invokeOutput;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async pauseRunForWait({
|
|
971
|
+
run,
|
|
972
|
+
stepId,
|
|
973
|
+
eventName,
|
|
974
|
+
timeoutEvent,
|
|
975
|
+
skipOutput,
|
|
976
|
+
db,
|
|
977
|
+
timeline
|
|
978
|
+
}) {
|
|
979
|
+
const baseTimeline = timeline ?? run.timeline;
|
|
980
|
+
const waitFor = {};
|
|
981
|
+
if (eventName)
|
|
982
|
+
waitFor.eventName = eventName;
|
|
983
|
+
if (timeoutEvent)
|
|
984
|
+
waitFor.timeoutEvent = timeoutEvent;
|
|
985
|
+
if (skipOutput)
|
|
986
|
+
waitFor.skipOutput = true;
|
|
987
|
+
await this.updateRun({
|
|
988
|
+
runId: run.id,
|
|
989
|
+
resourceId: run.resourceId ?? undefined,
|
|
990
|
+
data: {
|
|
991
|
+
status: "paused" /* PAUSED */,
|
|
992
|
+
currentStepId: stepId,
|
|
993
|
+
pausedAt: new Date,
|
|
994
|
+
timeline: merge(baseTimeline, {
|
|
995
|
+
[waitForTimelineKey(stepId)]: {
|
|
996
|
+
waitFor,
|
|
997
|
+
timestamp: new Date
|
|
998
|
+
}
|
|
999
|
+
})
|
|
1000
|
+
}
|
|
1001
|
+
}, { db });
|
|
1002
|
+
}
|
|
707
1003
|
async runStep({
|
|
708
1004
|
stepId,
|
|
709
1005
|
run,
|
|
@@ -792,21 +1088,7 @@ ${error.stack}` : String(error)
|
|
|
792
1088
|
const timeoutEvent = timeoutDate ? `__timeout_${stepId}` : undefined;
|
|
793
1089
|
await withPostgresTransaction(this.db, async (db) => {
|
|
794
1090
|
const freshRun = await this.getRun({ runId: run.id, resourceId: run.resourceId ?? undefined }, { exclusiveLock: true, db });
|
|
795
|
-
return this.
|
|
796
|
-
runId: run.id,
|
|
797
|
-
resourceId: run.resourceId ?? undefined,
|
|
798
|
-
data: {
|
|
799
|
-
status: "paused" /* PAUSED */,
|
|
800
|
-
currentStepId: stepId,
|
|
801
|
-
pausedAt: new Date,
|
|
802
|
-
timeline: merge(freshRun.timeline, {
|
|
803
|
-
[`${stepId}-wait-for`]: {
|
|
804
|
-
waitFor: { eventName, timeoutEvent },
|
|
805
|
-
timestamp: new Date
|
|
806
|
-
}
|
|
807
|
-
})
|
|
808
|
-
}
|
|
809
|
-
}, { db });
|
|
1091
|
+
return this.pauseRunForWait({ run: freshRun, stepId, eventName, timeoutEvent, db });
|
|
810
1092
|
}, this.pool);
|
|
811
1093
|
if (timeoutDate && timeoutEvent) {
|
|
812
1094
|
try {
|
|
@@ -819,7 +1101,8 @@ ${error.stack}` : String(error)
|
|
|
819
1101
|
};
|
|
820
1102
|
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
821
1103
|
startAfter: timeoutDate.getTime() <= Date.now() ? new Date : timeoutDate,
|
|
822
|
-
expireInSeconds: defaultExpireInSeconds
|
|
1104
|
+
expireInSeconds: defaultExpireInSeconds,
|
|
1105
|
+
...retrySendOptions(run.maxRetries)
|
|
823
1106
|
});
|
|
824
1107
|
} catch (error) {
|
|
825
1108
|
await this.updateRun({
|
|
@@ -919,7 +1202,7 @@ ${error.stack}` : String(error)
|
|
|
919
1202
|
pausedAt: new Date,
|
|
920
1203
|
timeline: merge(freshRun.timeline, {
|
|
921
1204
|
[`${stepId}-poll`]: { startedAt: startedAt.toISOString() },
|
|
922
|
-
[
|
|
1205
|
+
[waitForTimelineKey(stepId)]: {
|
|
923
1206
|
waitFor: { timeoutEvent: pollEvent, skipOutput: true },
|
|
924
1207
|
timestamp: new Date
|
|
925
1208
|
}
|
|
@@ -936,7 +1219,8 @@ ${error.stack}` : String(error)
|
|
|
936
1219
|
event: { name: pollEvent, data: {} }
|
|
937
1220
|
}, {
|
|
938
1221
|
startAfter: new Date(Date.now() + intervalMs),
|
|
939
|
-
expireInSeconds: defaultExpireInSeconds
|
|
1222
|
+
expireInSeconds: defaultExpireInSeconds,
|
|
1223
|
+
...retrySendOptions(run.maxRetries)
|
|
940
1224
|
});
|
|
941
1225
|
} catch (error) {
|
|
942
1226
|
await this.updateRun({
|
|
@@ -979,6 +1263,9 @@ ${error.stack}` : String(error)
|
|
|
979
1263
|
statuses,
|
|
980
1264
|
workflowId
|
|
981
1265
|
}) {
|
|
1266
|
+
if (workflowId)
|
|
1267
|
+
validateWorkflowId(workflowId);
|
|
1268
|
+
validateResourceId(resourceId);
|
|
982
1269
|
return getWorkflowRuns({
|
|
983
1270
|
resourceId,
|
|
984
1271
|
startingAfter,
|
|
@@ -991,6 +1278,8 @@ ${error.stack}` : String(error)
|
|
|
991
1278
|
}
|
|
992
1279
|
export {
|
|
993
1280
|
workflow,
|
|
1281
|
+
validateWorkflowId,
|
|
1282
|
+
validateResourceId,
|
|
994
1283
|
parseDuration,
|
|
995
1284
|
createWorkflowRef,
|
|
996
1285
|
WorkflowStatus,
|
|
@@ -1001,5 +1290,5 @@ export {
|
|
|
1001
1290
|
StepType
|
|
1002
1291
|
};
|
|
1003
1292
|
|
|
1004
|
-
//# debugId=
|
|
1293
|
+
//# debugId=F3273B34BE68C60E64756E2164756E21
|
|
1005
1294
|
//# sourceMappingURL=index.js.map
|