@dbos-inc/dbos-sdk 4.25.14 → 4.26.3-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.
@@ -699,7 +699,7 @@ class SystemDatabase {
699
699
  async recordWorkflowOutput(workflowID, status) {
700
700
  const client = await this.pool.connect();
701
701
  try {
702
- await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.SUCCESS, { output: status.output });
702
+ return await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.SUCCESS, { output: status.output });
703
703
  }
704
704
  finally {
705
705
  client.release();
@@ -708,36 +708,30 @@ class SystemDatabase {
708
708
  async recordWorkflowError(workflowID, status) {
709
709
  const client = await this.pool.connect();
710
710
  try {
711
- await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.ERROR, { error: status.error });
711
+ return await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.ERROR, { error: status.error });
712
712
  }
713
713
  finally {
714
714
  client.release();
715
715
  }
716
716
  }
717
- // Record a workflow's terminal outcome (SUCCESS or ERROR), but never overwrite
718
- // the terminal CANCELLED status: a workflow can be cancelled during its final
719
- // step, and if so it must not be able to subsequently complete. If the
720
- // workflow is cancelled, abort the function so it does not complete. This
721
- // mirrors the cancellation check done before each step.
717
+ // Record a workflow's terminal outcome (SUCCESS or ERROR), reporting whether
718
+ // the write landed. The write applies only to a PENDING row: a run owns its
719
+ // workflow's outcome exactly as long as the row says that run is what the
720
+ // workflow is doing. (Note: this does not prevent a write when another
721
+ // concurrent execution is already running and the status is PENDING. However,
722
+ // both executions should be deterministic and idempotent.)
723
+ //
724
+ // Returning false means the row was CANCELLED, dead-lettered, already
725
+ // terminal, handed to another execution (ENQUEUED/DELAYED, e.g. by a
726
+ // concurrent resume), or deleted; the caller resolves which by awaiting the
727
+ // recorded outcome.
722
728
  async #recordWorkflowOutcome(client, workflowID, status, outcome) {
723
- let cancelled = false;
724
- try {
725
- await client.query('BEGIN');
726
- await this.updateWorkflowStatus(client, workflowID, status, {
727
- update: { ...outcome, resetDeduplicationID: true, setCompletedAt: true },
728
- where: { notStatus: workflow_1.StatusString.CANCELLED },
729
- throwOnFailure: false,
730
- });
731
- cancelled = (await this.getWorkflowStatusValue(client, workflowID)) === workflow_1.StatusString.CANCELLED;
732
- await client.query('COMMIT');
733
- }
734
- catch (e) {
735
- await client.query('ROLLBACK');
736
- throw e;
737
- }
738
- if (cancelled) {
739
- throw new error_1.DBOSWorkflowCancelledError(workflowID);
740
- }
729
+ const rowCount = await this.updateWorkflowStatus(client, workflowID, status, {
730
+ update: { ...outcome, resetDeduplicationID: true, setCompletedAt: true },
731
+ where: { status: workflow_1.StatusString.PENDING },
732
+ throwOnFailure: false,
733
+ });
734
+ return rowCount > 0;
741
735
  }
742
736
  async getPendingWorkflows(executorID, appVersion) {
743
737
  const getWorkflows = await this.pool.query(`SELECT workflow_uuid
@@ -1487,7 +1481,12 @@ class SystemDatabase {
1487
1481
  async #checkIfCanceledLimited(workflowID) {
1488
1482
  await this.#pollWithLimiter(() => this.#checkIfCanceled(this.pool, workflowID));
1489
1483
  }
1490
- async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID, pollingIntervalMs) {
1484
+ // A missing row normally means the workflow has not been inserted yet, so
1485
+ // polling for it is correct. Callers that know the row must already exist
1486
+ // (e.g. a run parking on an outcome it just failed to write) pass
1487
+ // `failIfMissing` to fail fast with DBOSNonExistentWorkflowError instead of
1488
+ // polling forever.
1489
+ async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID, pollingIntervalMs, failIfMissing) {
1491
1490
  const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
1492
1491
  let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
1493
1492
  const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalResultMs;
@@ -1499,33 +1498,37 @@ class SystemDatabase {
1499
1498
  while (true) {
1500
1499
  if (callerID)
1501
1500
  await this.#checkIfCanceledLimited(callerID);
1501
+ let rows;
1502
1502
  try {
1503
- const { rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT status, output, error, serialization FROM "${this.schemaName}".workflow_status
1504
- WHERE workflow_uuid=$1`, [workflowID]));
1505
- if (rows.length > 0) {
1506
- const status = rows[0].status;
1507
- if (status === workflow_1.StatusString.SUCCESS) {
1508
- return { output: rows[0].output, serialization: rows[0].serialization };
1509
- }
1510
- else if (status === workflow_1.StatusString.ERROR) {
1511
- return { error: rows[0].error, serialization: rows[0].serialization };
1512
- }
1513
- else if (status === workflow_1.StatusString.CANCELLED) {
1514
- return { cancelled: true };
1515
- }
1516
- else if (status === workflow_1.StatusString.MAX_RECOVERY_ATTEMPTS_EXCEEDED) {
1517
- return { maxRecoveryAttemptsExceeded: true };
1518
- }
1519
- else {
1520
- // Status is not actionable
1521
- }
1522
- }
1503
+ ({ rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT status, output, error, serialization FROM "${this.schemaName}".workflow_status
1504
+ WHERE workflow_uuid=$1`, [workflowID])));
1523
1505
  }
1524
1506
  catch (e) {
1525
1507
  const err = e;
1526
1508
  this.logger.error(`Exception from system database: ${err}`, err);
1527
1509
  throw err;
1528
1510
  }
1511
+ if (rows.length > 0) {
1512
+ const status = rows[0].status;
1513
+ if (status === workflow_1.StatusString.SUCCESS) {
1514
+ return { output: rows[0].output, serialization: rows[0].serialization };
1515
+ }
1516
+ else if (status === workflow_1.StatusString.ERROR) {
1517
+ return { error: rows[0].error, serialization: rows[0].serialization };
1518
+ }
1519
+ else if (status === workflow_1.StatusString.CANCELLED) {
1520
+ return { cancelled: true };
1521
+ }
1522
+ else if (status === workflow_1.StatusString.MAX_RECOVERY_ATTEMPTS_EXCEEDED) {
1523
+ return { maxRecoveryAttemptsExceeded: true };
1524
+ }
1525
+ else {
1526
+ // Status is not actionable
1527
+ }
1528
+ }
1529
+ else if (failIfMissing) {
1530
+ throw new error_1.DBOSNonExistentWorkflowError(`Workflow ${workflowID} does not exist`);
1531
+ }
1529
1532
  const ct = Date.now();
1530
1533
  if (finishTime && ct > finishTime)
1531
1534
  return undefined; // Time's up
@@ -3334,6 +3337,7 @@ class SystemDatabase {
3334
3337
  if (throwOnFailure && result.rowCount !== 1) {
3335
3338
  throw new error_1.DBOSWorkflowConflictError(`Attempt to record transition of nonexistent workflow ${workflowID}`);
3336
3339
  }
3340
+ return result.rowCount ?? 0;
3337
3341
  }
3338
3342
  async recordOperationResultInternal(client, workflowID, functionID, functionName, checkConflict, startTimeEpochMs, endTimeEpochMs, options = {}) {
3339
3343
  try {
@@ -3589,9 +3593,15 @@ __decorate([
3589
3593
  __metadata("design:returntype", Promise)
3590
3594
  ], SystemDatabase.prototype, "debounceDelayedWorkflowStandalone", null);
3591
3595
  __decorate([
3592
- dbRetry(),
3596
+ dbRetry()
3597
+ // A missing row normally means the workflow has not been inserted yet, so
3598
+ // polling for it is correct. Callers that know the row must already exist
3599
+ // (e.g. a run parking on an outcome it just failed to write) pass
3600
+ // `failIfMissing` to fail fast with DBOSNonExistentWorkflowError instead of
3601
+ // polling forever.
3602
+ ,
3593
3603
  __metadata("design:type", Function),
3594
- __metadata("design:paramtypes", [String, Number, String, Number, Number]),
3604
+ __metadata("design:paramtypes", [String, Number, String, Number, Number, Boolean]),
3595
3605
  __metadata("design:returntype", Promise)
3596
3606
  ], SystemDatabase.prototype, "awaitWorkflowResult", null);
3597
3607
  __decorate([