@dbos-inc/dbos-sdk 4.19.8 → 4.20.5-preview

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.
@@ -517,9 +517,7 @@ class SystemDatabase {
517
517
  async recordWorkflowOutput(workflowID, status) {
518
518
  const client = await this.pool.connect();
519
519
  try {
520
- await this.updateWorkflowStatus(client, workflowID, workflow_1.StatusString.SUCCESS, {
521
- update: { output: status.output, resetDeduplicationID: true, setCompletedAt: true },
522
- });
520
+ await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.SUCCESS, { output: status.output });
523
521
  }
524
522
  finally {
525
523
  client.release();
@@ -528,14 +526,37 @@ class SystemDatabase {
528
526
  async recordWorkflowError(workflowID, status) {
529
527
  const client = await this.pool.connect();
530
528
  try {
531
- await this.updateWorkflowStatus(client, workflowID, workflow_1.StatusString.ERROR, {
532
- update: { error: status.error, resetDeduplicationID: true, setCompletedAt: true },
533
- });
529
+ await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.ERROR, { error: status.error });
534
530
  }
535
531
  finally {
536
532
  client.release();
537
533
  }
538
534
  }
535
+ // Record a workflow's terminal outcome (SUCCESS or ERROR), but never overwrite
536
+ // the terminal CANCELLED status: a workflow can be cancelled during its final
537
+ // step, and if so it must not be able to subsequently complete. If the
538
+ // workflow is cancelled, abort the function so it does not complete. This
539
+ // mirrors the cancellation check done before each step.
540
+ async #recordWorkflowOutcome(client, workflowID, status, outcome) {
541
+ let cancelled = false;
542
+ try {
543
+ await client.query('BEGIN');
544
+ await this.updateWorkflowStatus(client, workflowID, status, {
545
+ update: { ...outcome, resetDeduplicationID: true, setCompletedAt: true },
546
+ where: { notStatus: workflow_1.StatusString.CANCELLED },
547
+ throwOnFailure: false,
548
+ });
549
+ cancelled = (await this.getWorkflowStatusValue(client, workflowID)) === workflow_1.StatusString.CANCELLED;
550
+ await client.query('COMMIT');
551
+ }
552
+ catch (e) {
553
+ await client.query('ROLLBACK');
554
+ throw e;
555
+ }
556
+ if (cancelled) {
557
+ throw new error_1.DBOSWorkflowCancelledError(workflowID);
558
+ }
559
+ }
539
560
  async getPendingWorkflows(executorID, appVersion) {
540
561
  const getWorkflows = await this.pool.query(`SELECT workflow_uuid, queue_name
541
562
  FROM "${this.schemaName}".workflow_status
@@ -673,7 +694,24 @@ class SystemDatabase {
673
694
  return { isPatched: true, hasEntry: true };
674
695
  }
675
696
  // ==================== Workflow Management ====================
676
- async cancelWorkflows(workflowIDs) {
697
+ async cancelWorkflows(workflowIDs, cancelChildren = false) {
698
+ if (!cancelChildren) {
699
+ await this.#cancelWorkflows(workflowIDs);
700
+ return;
701
+ }
702
+ // Cascade cancellation to child workflows level by level.
703
+ const visited = new Set(workflowIDs);
704
+ let frontier = workflowIDs;
705
+ while (frontier.length > 0) {
706
+ await this.#cancelWorkflows(frontier);
707
+ const children = await this.#getDirectChildren(frontier);
708
+ frontier = children.filter((id) => !visited.has(id));
709
+ for (const id of frontier) {
710
+ visited.add(id);
711
+ }
712
+ }
713
+ }
714
+ async #cancelWorkflows(workflowIDs) {
677
715
  await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
678
716
  SET status = $1, queue_name = NULL, deduplication_id = NULL, started_at_epoch_ms = NULL,
679
717
  updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint,
@@ -718,32 +756,28 @@ class SystemDatabase {
718
756
  WHERE workflow_uuid = $3
719
757
  AND status = $4`, [delayUntilEpochMS, Date.now(), workflowID, workflow_1.StatusString.DELAYED]);
720
758
  }
759
+ // Get the immediate (one-level) child workflow IDs for a set of workflows.
760
+ async #getDirectChildren(workflowIDs) {
761
+ if (workflowIDs.length === 0) {
762
+ return [];
763
+ }
764
+ const result = await this.pool.query(`SELECT workflow_uuid
765
+ FROM "${this.schemaName}".workflow_status
766
+ WHERE parent_workflow_id = ANY($1)`, [workflowIDs]);
767
+ return result.rows.map((row) => row.workflow_uuid);
768
+ }
721
769
  async getWorkflowChildren(workflowID) {
722
770
  // BFS to find all descendant workflows
723
- const visited = new Set([workflowID]);
724
- const queue = [workflowID];
725
- const children = [];
726
- const client = await this.pool.connect();
727
- try {
728
- while (queue.length > 0) {
729
- const batch = queue.splice(0, queue.length);
730
- const result = await client.query(`SELECT DISTINCT child_workflow_id
731
- FROM "${this.schemaName}".operation_outputs
732
- WHERE workflow_uuid = ANY($1)
733
- AND child_workflow_id IS NOT NULL`, [batch]);
734
- for (const row of result.rows) {
735
- if (!visited.has(row.child_workflow_id)) {
736
- visited.add(row.child_workflow_id);
737
- queue.push(row.child_workflow_id);
738
- children.push(row.child_workflow_id);
739
- }
740
- }
771
+ const descendants = new Set();
772
+ let frontier = [workflowID];
773
+ while (frontier.length > 0) {
774
+ const children = await this.#getDirectChildren(frontier);
775
+ frontier = children.filter((id) => !descendants.has(id));
776
+ for (const id of frontier) {
777
+ descendants.add(id);
741
778
  }
742
779
  }
743
- finally {
744
- client.release();
745
- }
746
- return children;
780
+ return [...descendants];
747
781
  }
748
782
  async deleteWorkflows(workflowIDs, deleteChildren = false) {
749
783
  const allIds = [...workflowIDs];
@@ -1151,9 +1185,10 @@ class SystemDatabase {
1151
1185
  //throw new Error('Message notification map is not empty - shutdown is not clean.');
1152
1186
  }
1153
1187
  }
1154
- async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID) {
1188
+ async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID, pollingIntervalMs) {
1155
1189
  const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
1156
1190
  let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
1191
+ const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalResultMs;
1157
1192
  while (true) {
1158
1193
  let resolveNotification;
1159
1194
  const statusPromise = new Promise((resolve) => {
@@ -1203,14 +1238,14 @@ class SystemDatabase {
1203
1238
  let timeoutPromise = Promise.resolve();
1204
1239
  let timeoutCancel = () => { };
1205
1240
  if (timerFuncID !== undefined && callerID !== undefined && timeoutms !== undefined) {
1206
- const { promise, cancel, endTime } = await this.#durableSleep(callerID, timerFuncID, timeoutms, this.dbPollingIntervalResultMs);
1241
+ const { promise, cancel, endTime } = await this.#durableSleep(callerID, timerFuncID, timeoutms, pollIntervalMs);
1207
1242
  finishTime = endTime;
1208
1243
  timeoutPromise = promise;
1209
1244
  timeoutCancel = cancel;
1210
1245
  }
1211
1246
  else {
1212
- let poll = finishTime ? finishTime - ct : this.dbPollingIntervalResultMs;
1213
- poll = Math.min(this.dbPollingIntervalResultMs, poll);
1247
+ let poll = finishTime ? finishTime - ct : pollIntervalMs;
1248
+ poll = Math.min(pollIntervalMs, poll);
1214
1249
  const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
1215
1250
  timeoutPromise = promise;
1216
1251
  timeoutCancel = cancel;
@@ -1229,8 +1264,9 @@ class SystemDatabase {
1229
1264
  }
1230
1265
  }
1231
1266
  }
1232
- async awaitFirstWorkflowId(workflowIds, callerID) {
1267
+ async awaitFirstWorkflowId(workflowIds, callerID, pollingIntervalMs) {
1233
1268
  const placeholders = workflowIds.map((_, i) => `$${i + 1}`).join(', ');
1269
+ const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalResultMs;
1234
1270
  while (true) {
1235
1271
  let resolveNotification;
1236
1272
  const wakeupPromise = new Promise((resolve) => {
@@ -1251,7 +1287,7 @@ class SystemDatabase {
1251
1287
  if (rows.length > 0) {
1252
1288
  return rows[0].workflow_uuid;
1253
1289
  }
1254
- const { promise: sleepPromise, cancel: sleepCancel } = (0, utils_1.cancellableSleep)(this.dbPollingIntervalResultMs);
1290
+ const { promise: sleepPromise, cancel: sleepCancel } = (0, utils_1.cancellableSleep)(pollIntervalMs);
1255
1291
  try {
1256
1292
  await Promise.race([wakeupPromise, sleepPromise]);
1257
1293
  }
@@ -1344,7 +1380,7 @@ class SystemDatabase {
1344
1380
  throw err;
1345
1381
  }
1346
1382
  }
1347
- async recv(workflowID, functionID, timeoutFunctionID, topic, timeoutSeconds = dbos_executor_1.DBOSExecutor.defaultNotificationTimeoutSec) {
1383
+ async recv(workflowID, functionID, timeoutFunctionID, topic, timeoutSeconds = dbos_executor_1.DBOSExecutor.defaultNotificationTimeoutSec, pollingIntervalMs) {
1348
1384
  topic = topic ?? this.nullTopic;
1349
1385
  const startTime = Date.now();
1350
1386
  // First, check for previous executions.
@@ -1357,6 +1393,7 @@ class SystemDatabase {
1357
1393
  }
1358
1394
  const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
1359
1395
  let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
1396
+ const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalEventMs;
1360
1397
  while (true) {
1361
1398
  // register the key with the global notifications listener.
1362
1399
  let resolveNotification;
@@ -1380,14 +1417,14 @@ class SystemDatabase {
1380
1417
  let timeoutPromise = Promise.resolve();
1381
1418
  let timeoutCancel = () => { };
1382
1419
  if (timeoutms) {
1383
- const { promise, cancel, endTime } = await this.#durableSleep(workflowID, timeoutFunctionID, timeoutms, this.dbPollingIntervalEventMs);
1420
+ const { promise, cancel, endTime } = await this.#durableSleep(workflowID, timeoutFunctionID, timeoutms, pollIntervalMs);
1384
1421
  timeoutPromise = promise;
1385
1422
  timeoutCancel = cancel;
1386
1423
  finishTime = endTime;
1387
1424
  }
1388
1425
  else {
1389
- let poll = finishTime ? finishTime - ct : this.dbPollingIntervalEventMs;
1390
- poll = Math.min(this.dbPollingIntervalEventMs, poll);
1426
+ let poll = finishTime ? finishTime - ct : pollIntervalMs;
1427
+ poll = Math.min(pollIntervalMs, poll);
1391
1428
  const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
1392
1429
  timeoutPromise = promise;
1393
1430
  timeoutCancel = cancel;
@@ -1475,7 +1512,7 @@ class SystemDatabase {
1475
1512
  client.release();
1476
1513
  }
1477
1514
  }
1478
- async getEvent(workflowID, key, timeoutSeconds, callerWorkflow) {
1515
+ async getEvent(workflowID, key, timeoutSeconds, callerWorkflow, pollingIntervalMs) {
1479
1516
  const startTime = Date.now();
1480
1517
  // Check if the operation has been done before for OAOO (only do this inside a workflow).
1481
1518
  if (callerWorkflow) {
@@ -1493,6 +1530,7 @@ class SystemDatabase {
1493
1530
  const payloadKey = `${workflowID}::${key}`;
1494
1531
  const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
1495
1532
  let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
1533
+ const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalEventMs;
1496
1534
  // Register the key with the global notifications listener first... we do not want to look in the DB first
1497
1535
  // or that would cause a timing hole.
1498
1536
  while (true) {
@@ -1525,14 +1563,14 @@ class SystemDatabase {
1525
1563
  let timeoutPromise = Promise.resolve();
1526
1564
  let timeoutCancel = () => { };
1527
1565
  if (callerWorkflow && timeoutms) {
1528
- const { promise, cancel, endTime } = await this.#durableSleep(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutms, this.dbPollingIntervalEventMs);
1566
+ const { promise, cancel, endTime } = await this.#durableSleep(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutms, pollIntervalMs);
1529
1567
  timeoutPromise = promise;
1530
1568
  timeoutCancel = cancel;
1531
1569
  finishTime = endTime;
1532
1570
  }
1533
1571
  else {
1534
- let poll = finishTime ? finishTime - ct : this.dbPollingIntervalEventMs;
1535
- poll = Math.min(this.dbPollingIntervalEventMs, poll);
1572
+ let poll = finishTime ? finishTime - ct : pollIntervalMs;
1573
+ poll = Math.min(pollIntervalMs, poll);
1536
1574
  const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
1537
1575
  timeoutPromise = promise;
1538
1576
  timeoutCancel = cancel;
@@ -2745,6 +2783,10 @@ class SystemDatabase {
2745
2783
  const param = args.push(where.status);
2746
2784
  whereClause += ` AND status=$${param}`;
2747
2785
  }
2786
+ if (where.notStatus) {
2787
+ const param = args.push(where.notStatus);
2788
+ whereClause += ` AND status!=$${param}`;
2789
+ }
2748
2790
  const result = await client.query(`UPDATE "${this.schemaName}".workflow_status ${setClause} ${whereClause}`, args);
2749
2791
  const throwOnFailure = options.throwOnFailure ?? true;
2750
2792
  if (throwOnFailure && result.rowCount !== 1) {
@@ -3000,13 +3042,13 @@ __decorate([
3000
3042
  __decorate([
3001
3043
  dbRetry(),
3002
3044
  __metadata("design:type", Function),
3003
- __metadata("design:paramtypes", [String, Number, String, Number]),
3045
+ __metadata("design:paramtypes", [String, Number, String, Number, Number]),
3004
3046
  __metadata("design:returntype", Promise)
3005
3047
  ], SystemDatabase.prototype, "awaitWorkflowResult", null);
3006
3048
  __decorate([
3007
3049
  dbRetry(),
3008
3050
  __metadata("design:type", Function),
3009
- __metadata("design:paramtypes", [Array, String]),
3051
+ __metadata("design:paramtypes", [Array, String, Number]),
3010
3052
  __metadata("design:returntype", Promise)
3011
3053
  ], SystemDatabase.prototype, "awaitFirstWorkflowId", null);
3012
3054
  __decorate([
@@ -3030,7 +3072,7 @@ __decorate([
3030
3072
  __decorate([
3031
3073
  dbRetry(),
3032
3074
  __metadata("design:type", Function),
3033
- __metadata("design:paramtypes", [String, Number, Number, String, Number]),
3075
+ __metadata("design:paramtypes", [String, Number, Number, String, Number, Number]),
3034
3076
  __metadata("design:returntype", Promise)
3035
3077
  ], SystemDatabase.prototype, "recv", null);
3036
3078
  __decorate([
@@ -3042,7 +3084,7 @@ __decorate([
3042
3084
  __decorate([
3043
3085
  dbRetry(),
3044
3086
  __metadata("design:type", Function),
3045
- __metadata("design:paramtypes", [String, String, Number, Object]),
3087
+ __metadata("design:paramtypes", [String, String, Number, Object, Number]),
3046
3088
  __metadata("design:returntype", Promise)
3047
3089
  ], SystemDatabase.prototype, "getEvent", null);
3048
3090
  __decorate([