@dbos-inc/dbos-sdk 2.6.2-preview → 2.6.7-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.
Files changed (45) hide show
  1. package/dist/schemas/user_db_schema.d.ts +3 -3
  2. package/dist/schemas/user_db_schema.d.ts.map +1 -1
  3. package/dist/src/dbos-executor.d.ts +12 -3
  4. package/dist/src/dbos-executor.d.ts.map +1 -1
  5. package/dist/src/dbos-executor.js +205 -129
  6. package/dist/src/dbos-executor.js.map +1 -1
  7. package/dist/src/dbos-runtime/cli.d.ts.map +1 -1
  8. package/dist/src/dbos-runtime/cli.js +2 -1
  9. package/dist/src/dbos-runtime/cli.js.map +1 -1
  10. package/dist/src/dbos-runtime/debug.d.ts +1 -1
  11. package/dist/src/dbos-runtime/debug.d.ts.map +1 -1
  12. package/dist/src/dbos-runtime/debug.js +3 -3
  13. package/dist/src/dbos-runtime/debug.js.map +1 -1
  14. package/dist/src/dbos-runtime/workflow_management.d.ts +3 -3
  15. package/dist/src/dbos-runtime/workflow_management.d.ts.map +1 -1
  16. package/dist/src/dbos-runtime/workflow_management.js +3 -3
  17. package/dist/src/dbos-runtime/workflow_management.js.map +1 -1
  18. package/dist/src/dbos.d.ts +4 -0
  19. package/dist/src/dbos.d.ts.map +1 -1
  20. package/dist/src/dbos.js +16 -8
  21. package/dist/src/dbos.js.map +1 -1
  22. package/dist/src/decorators.d.ts +7 -1
  23. package/dist/src/decorators.d.ts.map +1 -1
  24. package/dist/src/decorators.js +73 -13
  25. package/dist/src/decorators.js.map +1 -1
  26. package/dist/src/error.d.ts +1 -1
  27. package/dist/src/error.d.ts.map +1 -1
  28. package/dist/src/error.js +1 -2
  29. package/dist/src/error.js.map +1 -1
  30. package/dist/src/system_database.d.ts.map +1 -1
  31. package/dist/src/system_database.js +13 -2
  32. package/dist/src/system_database.js.map +1 -1
  33. package/dist/src/testing/testing_runtime.d.ts +3 -4
  34. package/dist/src/testing/testing_runtime.d.ts.map +1 -1
  35. package/dist/src/testing/testing_runtime.js +4 -4
  36. package/dist/src/testing/testing_runtime.js.map +1 -1
  37. package/dist/src/utils.d.ts +1 -1
  38. package/dist/src/utils.d.ts.map +1 -1
  39. package/dist/src/utils.js +1 -1
  40. package/dist/src/utils.js.map +1 -1
  41. package/dist/src/workflow.d.ts +5 -0
  42. package/dist/src/workflow.d.ts.map +1 -1
  43. package/dist/src/workflow.js.map +1 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +1 -1
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.DBOSExecutor = exports.OperationType = exports.dbosNull = void 0;
29
+ exports.DBOSExecutor = exports.OperationType = exports.DebugMode = exports.dbosNull = void 0;
30
30
  const error_1 = require("./error");
31
31
  const workflow_1 = require("./workflow");
32
32
  const transaction_1 = require("./transaction");
@@ -52,6 +52,12 @@ const wfqueue_1 = require("./wfqueue");
52
52
  const debugpoint_1 = require("./debugpoint");
53
53
  const crypto = __importStar(require("crypto"));
54
54
  exports.dbosNull = {};
55
+ var DebugMode;
56
+ (function (DebugMode) {
57
+ DebugMode[DebugMode["DISABLED"] = 0] = "DISABLED";
58
+ DebugMode[DebugMode["ENABLED"] = 1] = "ENABLED";
59
+ DebugMode[DebugMode["TIME_TRAVEL"] = 2] = "TIME_TRAVEL";
60
+ })(DebugMode || (exports.DebugMode = DebugMode = {}));
55
61
  exports.OperationType = {
56
62
  HANDLER: 'handler',
57
63
  WORKFLOW: 'workflow',
@@ -105,6 +111,20 @@ class DBOSExecutor {
105
111
  isFlushingBuffers = false;
106
112
  static defaultNotificationTimeoutSec = 60;
107
113
  debugMode;
114
+ get isDebugging() {
115
+ switch (this.debugMode) {
116
+ case DebugMode.DISABLED:
117
+ return false;
118
+ case DebugMode.ENABLED:
119
+ case DebugMode.TIME_TRAVEL:
120
+ return true;
121
+ default: {
122
+ const _never = this.debugMode;
123
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
124
+ throw new Error(`Unexpected DBOS debug mode: ${this.debugMode}`);
125
+ }
126
+ }
127
+ }
108
128
  static systemDBSchemaName = 'dbos';
109
129
  logger;
110
130
  tracer;
@@ -117,9 +137,9 @@ class DBOSExecutor {
117
137
  executorID = utils_1.globalParams.executorID;
118
138
  static globalInstance = undefined;
119
139
  /* WORKFLOW EXECUTOR LIFE CYCLE MANAGEMENT */
120
- constructor(config, systemDatabase) {
140
+ constructor(config, { systemDatabase, debugMode } = {}) {
121
141
  this.config = config;
122
- this.debugMode = config.debugMode ?? false;
142
+ this.debugMode = debugMode ?? DebugMode.DISABLED;
123
143
  // Set configured environment variables
124
144
  if (config.env) {
125
145
  for (const [key, value] of Object.entries(config.env)) {
@@ -141,7 +161,7 @@ class DBOSExecutor {
141
161
  }
142
162
  this.logger = new logs_1.GlobalLogger(this.telemetryCollector, this.config.telemetry?.logs);
143
163
  this.tracer = new traces_1.Tracer(this.telemetryCollector);
144
- if (this.debugMode) {
164
+ if (this.isDebugging) {
145
165
  this.logger.info('Running in debug mode!');
146
166
  }
147
167
  this.procedurePool = new pg_1.Pool(this.config.poolConfig);
@@ -154,7 +174,7 @@ class DBOSExecutor {
154
174
  this.systemDatabase = new system_database_1.PostgresSystemDatabase(this.config.poolConfig, this.config.system_database, this.logger);
155
175
  }
156
176
  this.flushBufferID = setInterval(() => {
157
- if (!this.debugMode && !this.isFlushingBuffers) {
177
+ if (!this.isDebugging && !this.isFlushingBuffers) {
158
178
  this.isFlushingBuffers = true;
159
179
  void this.flushWorkflowBuffers();
160
180
  }
@@ -295,7 +315,7 @@ class DBOSExecutor {
295
315
  }
296
316
  this.logger.debug(`Loaded ${length} ORM entities`);
297
317
  }
298
- if (!this.debugMode) {
318
+ if (!this.isDebugging) {
299
319
  await (0, user_database_1.createDBIfDoesNotExist)(this.config.poolConfig, this.logger);
300
320
  }
301
321
  this.configureDbClient();
@@ -307,8 +327,8 @@ class DBOSExecutor {
307
327
  this.#registerClass(cls);
308
328
  }
309
329
  // Debug mode doesn't need to initialize the DBs. Everything should appear to be read-only.
310
- await this.userDatabase.init(this.debugMode);
311
- if (!this.debugMode) {
330
+ await this.userDatabase.init(this.isDebugging);
331
+ if (!this.isDebugging) {
312
332
  await this.systemDatabase.init();
313
333
  }
314
334
  }
@@ -331,7 +351,7 @@ class DBOSExecutor {
331
351
  }
332
352
  this.initialized = true;
333
353
  // Only execute init code if under non-debug mode
334
- if (!this.debugMode) {
354
+ if (!this.isDebugging) {
335
355
  for (const cls of classes) {
336
356
  // Init its configurations
337
357
  const creg = (0, decorators_1.getOrCreateClassRegistration)(cls);
@@ -384,7 +404,7 @@ class DBOSExecutor {
384
404
  await Promise.allSettled(this.pendingWorkflowMap.values());
385
405
  }
386
406
  clearInterval(this.flushBufferID);
387
- if (!this.debugMode && !this.isFlushingBuffers) {
407
+ if (!this.isDebugging && !this.isFlushingBuffers) {
388
408
  // Don't flush the buffers if we're already flushing them in the background.
389
409
  await this.flushWorkflowBuffers();
390
410
  }
@@ -548,7 +568,7 @@ class DBOSExecutor {
548
568
  if ((wCtxt.tempWfOperationType !== TempWorkflowType.transaction &&
549
569
  wCtxt.tempWfOperationType !== TempWorkflowType.procedure) ||
550
570
  params.queueName !== undefined) {
551
- if (this.debugMode) {
571
+ if (this.isDebugging) {
552
572
  const wfStatus = await this.systemDatabase.getWorkflowStatus(workflowUUID);
553
573
  const wfInputs = await this.systemDatabase.getWorkflowInputs(workflowUUID);
554
574
  if (!wfStatus || !wfInputs) {
@@ -578,7 +598,7 @@ class DBOSExecutor {
578
598
  : wf.call(params.configuredInstance, ...args);
579
599
  return await callPromise;
580
600
  });
581
- if (this.debugMode) {
601
+ if (this.isDebugging) {
582
602
  const recordedResult = await this.systemDatabase.getWorkflowResult(workflowUUID);
583
603
  if (!resultsMatch(recordedResult, callResult)) {
584
604
  this.logger.error(`Detect different output for the workflow UUID ${workflowUUID}!\n Received: ${utils_1.DBOSJSON.stringify(callResult)}\n Original: ${utils_1.DBOSJSON.stringify(recordedResult)}`);
@@ -596,13 +616,13 @@ class DBOSExecutor {
596
616
  }
597
617
  internalStatus.output = result;
598
618
  internalStatus.status = workflow_1.StatusString.SUCCESS;
599
- if (internalStatus.queueName && !this.debugMode) {
619
+ if (internalStatus.queueName && !this.isDebugging) {
600
620
  // Now... the workflow isn't certainly done.
601
621
  // But waiting this long is for concurrency control anyway,
602
622
  // so it is probably done enough.
603
623
  await this.systemDatabase.dequeueWorkflow(workflowUUID, this.#getQueueByName(internalStatus.queueName));
604
624
  }
605
- if (!this.debugMode) {
625
+ if (!this.isDebugging) {
606
626
  this.systemDatabase.bufferWorkflowOutput(workflowUUID, internalStatus);
607
627
  }
608
628
  wCtxt.span.setStatus({ code: api_1.SpanStatusCode.OK });
@@ -618,7 +638,7 @@ class DBOSExecutor {
618
638
  else if (err instanceof error_1.DBOSWorkflowCancelledError) {
619
639
  internalStatus.error = err.message;
620
640
  internalStatus.status = workflow_1.StatusString.CANCELLED;
621
- if (!this.debugMode) {
641
+ if (!this.isDebugging) {
622
642
  await this.systemDatabase.setWorkflowStatus(workflowUUID, workflow_1.StatusString.CANCELLED, false);
623
643
  }
624
644
  wCtxt.span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
@@ -635,10 +655,10 @@ class DBOSExecutor {
635
655
  }
636
656
  internalStatus.error = utils_1.DBOSJSON.stringify((0, serialize_error_1.serializeError)(e));
637
657
  internalStatus.status = workflow_1.StatusString.ERROR;
638
- if (internalStatus.queueName && !this.debugMode) {
658
+ if (internalStatus.queueName && !this.isDebugging) {
639
659
  await this.systemDatabase.dequeueWorkflow(workflowUUID, this.#getQueueByName(internalStatus.queueName));
640
660
  }
641
- if (!this.debugMode) {
661
+ if (!this.isDebugging) {
642
662
  await this.systemDatabase.recordWorkflowError(workflowUUID, internalStatus);
643
663
  }
644
664
  // TODO: Log errors, but not in the tests when they're expected.
@@ -652,7 +672,7 @@ class DBOSExecutor {
652
672
  wCtxt.tempWfOperationType === TempWorkflowType.procedure) {
653
673
  // For single-transaction workflows, asynchronously record inputs.
654
674
  // We must buffer inputs after workflow status is buffered/flushed because workflow_inputs table has a foreign key reference to the workflow_status table.
655
- if (!this.debugMode) {
675
+ if (!this.isDebugging) {
656
676
  this.systemDatabase.bufferWorkflowInputs(workflowUUID, args);
657
677
  }
658
678
  }
@@ -663,7 +683,7 @@ class DBOSExecutor {
663
683
  }
664
684
  return result;
665
685
  };
666
- if (this.debugMode ||
686
+ if (this.isDebugging ||
667
687
  (status !== 'SUCCESS' && status !== 'ERROR' && (params.queueName === undefined || params.executeWorkflow))) {
668
688
  const workflowPromise = runWorkflow();
669
689
  // Need to await for the workflow and capture errors.
@@ -680,7 +700,7 @@ class DBOSExecutor {
680
700
  return new workflow_1.InvokedHandle(this.systemDatabase, workflowPromise, workflowUUID, wf.name, callerUUID, callerFunctionID);
681
701
  }
682
702
  else {
683
- if (params.queueName && status === 'ENQUEUED' && !this.debugMode) {
703
+ if (params.queueName && status === 'ENQUEUED' && !this.isDebugging) {
684
704
  await this.systemDatabase.enqueueWorkflow(workflowUUID, this.#getQueueByName(params.queueName));
685
705
  }
686
706
  return new workflow_1.RetrievedHandle(this.systemDatabase, workflowUUID, callerUUID, callerFunctionID);
@@ -702,39 +722,37 @@ class DBOSExecutor {
702
722
  /**
703
723
  * Check if an operation has already executed in a workflow.
704
724
  * If it previously executed successfully, return its output.
705
- * If it previously executed and threw an error, throw that error.
725
+ * If it previously executed and threw an error, return that error.
706
726
  * Otherwise, return DBOSNull.
707
- * Also return the transaction snapshot information of this current transaction.
727
+ * Also return the transaction snapshot and id information of the original or current transaction.
708
728
  */
709
729
  async #checkExecution(query, workflowUUID, funcID) {
710
- // Note: we read the current snapshot, not the recorded one!
711
- const rows = await query('(SELECT output, error, txn_snapshot, true as recorded FROM dbos.transaction_outputs WHERE workflow_uuid=$1 AND function_id=$2 UNION ALL SELECT null as output, null as error, pg_current_snapshot()::text as txn_snapshot, false as recorded) ORDER BY recorded', [workflowUUID, funcID]);
730
+ const rows = await query('(SELECT output, error, txn_snapshot, txn_id, true as recorded FROM dbos.transaction_outputs WHERE workflow_uuid=$1 AND function_id=$2 UNION ALL SELECT null as output, null as error, pg_current_snapshot()::text as txn_snapshot, null as txn_id, false as recorded) ORDER BY recorded', [workflowUUID, funcID]);
712
731
  if (rows.length === 0 || rows.length > 2) {
713
732
  const returnedRows = JSON.stringify(rows);
714
733
  this.logger.error('Unexpected! This should never happen. Returned rows: ' + returnedRows);
715
734
  throw new error_1.DBOSError('This should never happen. Returned rows: ' + returnedRows);
716
735
  }
717
- const res = {
718
- output: exports.dbosNull,
719
- txn_snapshot: '',
720
- };
721
- // recorded=false row will be first because we used ORDER BY.
722
- res.txn_snapshot = rows[0].txn_snapshot;
723
736
  if (rows.length === 2) {
724
- if (utils_1.DBOSJSON.parse(rows[1].error) !== null) {
725
- throw (0, serialize_error_1.deserializeError)(utils_1.DBOSJSON.parse(rows[1].error));
737
+ const { txn_snapshot, txn_id } = rows[1];
738
+ const error = utils_1.DBOSJSON.parse(rows[1].error);
739
+ if (error) {
740
+ return { result: (0, serialize_error_1.deserializeError)(error), txn_snapshot, txn_id: txn_id ?? undefined };
726
741
  }
727
742
  else {
728
- res.output = utils_1.DBOSJSON.parse(rows[1].output);
743
+ return { result: utils_1.DBOSJSON.parse(rows[1].output), txn_snapshot, txn_id: txn_id ?? undefined };
729
744
  }
730
745
  }
731
- return res;
746
+ else {
747
+ const { txn_snapshot } = rows[0];
748
+ return { result: exports.dbosNull, txn_snapshot, txn_id: undefined };
749
+ }
732
750
  }
733
751
  /**
734
752
  * Write a operation's output to the database.
735
753
  */
736
754
  async #recordOutput(query, workflowUUID, funcID, txnSnapshot, output, isKeyConflict) {
737
- if (this.debugMode) {
755
+ if (this.isDebugging) {
738
756
  throw new error_1.DBOSDebuggerError('Cannot record output in debug mode.');
739
757
  }
740
758
  try {
@@ -756,7 +774,7 @@ class DBOSExecutor {
756
774
  * Record an error in an operation to the database.
757
775
  */
758
776
  async #recordError(query, workflowUUID, funcID, txnSnapshot, err, isKeyConflict) {
759
- if (this.debugMode) {
777
+ if (this.isDebugging) {
760
778
  throw new error_1.DBOSDebuggerError('Cannot record error in debug mode.');
761
779
  }
762
780
  try {
@@ -782,7 +800,7 @@ class DBOSExecutor {
782
800
  if (funcIDs.length === 0) {
783
801
  return;
784
802
  }
785
- if (this.debugMode) {
803
+ if (this.isDebugging) {
786
804
  throw new error_1.DBOSDebuggerError('Cannot flush result buffer in debug mode.');
787
805
  }
788
806
  funcIDs.sort();
@@ -869,43 +887,71 @@ class DBOSExecutor {
869
887
  const tCtxt = new transaction_1.TransactionContextImpl(this.userDatabase.getName(), client, wfCtx, span, this.logger, funcId, txn.name);
870
888
  // If the UUID is preset, it is possible this execution previously happened. Check, and return its original result if it did.
871
889
  // Note: It is possible to retrieve a generated ID from a workflow handle, run a concurrent execution, and cause trouble for yourself. We recommend against this.
890
+ let prevResult = exports.dbosNull;
891
+ const queryFunc = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
872
892
  if (wfCtx.presetUUID) {
873
- const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
874
- const check = await this.#checkExecution(func, workflowUUID, funcId);
875
- txn_snapshot = check.txn_snapshot;
876
- if (check.output !== exports.dbosNull) {
893
+ const executionResult = await this.#checkExecution(queryFunc, workflowUUID, funcId);
894
+ prevResult = executionResult.result;
895
+ txn_snapshot = executionResult.txn_snapshot;
896
+ if (prevResult !== exports.dbosNull) {
877
897
  tCtxt.span.setAttribute('cached', true);
878
- tCtxt.span.setStatus({ code: api_1.SpanStatusCode.OK });
879
- this.tracer.endSpan(tCtxt.span);
880
- return check.output;
898
+ if (this.debugMode === DebugMode.TIME_TRAVEL) {
899
+ // for time travel debugging, navigate the proxy to the time of this transaction's snapshot
900
+ await queryFunc(`--proxy:${executionResult.txn_id ?? ''}:${txn_snapshot}`, []);
901
+ }
902
+ else {
903
+ // otherwise, return/throw the previous result
904
+ if (prevResult instanceof Error) {
905
+ throw prevResult;
906
+ }
907
+ else {
908
+ return prevResult;
909
+ }
910
+ }
881
911
  }
882
912
  }
883
913
  else {
884
914
  // Collect snapshot information for read-only transactions and non-preset UUID transactions, if not already collected above
885
- const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
886
- txn_snapshot = await DBOSExecutor.#retrieveSnapshot(func);
915
+ txn_snapshot = await DBOSExecutor.#retrieveSnapshot(queryFunc);
887
916
  }
888
- if (this.debugMode) {
917
+ if (this.isDebugging && prevResult === exports.dbosNull) {
889
918
  throw new error_1.DBOSDebuggerError(`Failed to find the recorded output for the transaction: workflow UUID ${workflowUUID}, step number ${funcId}`);
890
919
  }
891
920
  // For non-read-only transactions, flush the result buffer.
892
- if (!readOnly) {
921
+ if (!this.isDebugging && !readOnly) {
893
922
  await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
894
923
  }
895
924
  // Execute the user's transaction.
896
- let cresult;
897
- if (txnInfo.registration.passContext) {
898
- await (0, context_1.runWithTransactionContext)(tCtxt, async () => {
899
- cresult = await txn.call(clsinst, tCtxt, ...args);
900
- });
925
+ const result = await (async function () {
926
+ try {
927
+ return await (0, context_1.runWithTransactionContext)(tCtxt, async () => {
928
+ if (txnInfo.registration.passContext) {
929
+ return await txn.call(clsinst, tCtxt, ...args);
930
+ }
931
+ else {
932
+ const tf = txn;
933
+ return await tf.call(clsinst, ...args);
934
+ }
935
+ });
936
+ }
937
+ catch (e) {
938
+ return e instanceof Error ? e : new Error(`${e}`);
939
+ }
940
+ })();
941
+ if (this.isDebugging) {
942
+ if (prevResult instanceof Error) {
943
+ throw prevResult;
944
+ }
945
+ const prevResultJson = utils_1.DBOSJSON.stringify(prevResult);
946
+ const resultJson = utils_1.DBOSJSON.stringify(result);
947
+ if (prevResultJson !== resultJson) {
948
+ this.logger.error(`Detected different transaction output than the original one!\n Result: ${resultJson}\n Original: ${utils_1.DBOSJSON.stringify(prevResultJson)}`);
949
+ }
950
+ return prevResult;
901
951
  }
902
- else {
903
- await (0, context_1.runWithTransactionContext)(tCtxt, async () => {
904
- const tf = txn;
905
- cresult = await tf.call(clsinst, ...args);
906
- });
952
+ if (result instanceof Error) {
953
+ throw result;
907
954
  }
908
- const result = cresult;
909
955
  // Record the execution, commit, and return.
910
956
  if (readOnly) {
911
957
  // Buffer the output of read-only transactions instead of synchronously writing it.
@@ -919,8 +965,7 @@ class DBOSExecutor {
919
965
  else {
920
966
  try {
921
967
  // Synchronously record the output of write transactions and obtain the transaction ID.
922
- const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
923
- const pg_txn_id = await this.#recordOutput(func, wfCtx.workflowUUID, funcId, txn_snapshot, result, (error) => this.userDatabase.isKeyConflictError(error));
968
+ const pg_txn_id = await this.#recordOutput(queryFunc, wfCtx.workflowUUID, funcId, txn_snapshot, result, (error) => this.userDatabase.isKeyConflictError(error));
924
969
  tCtxt.span.setAttribute('pg_txn_id', pg_txn_id);
925
970
  wfCtx.resultBuffer.clear();
926
971
  }
@@ -943,26 +988,26 @@ class DBOSExecutor {
943
988
  return result;
944
989
  }
945
990
  catch (err) {
946
- if (this.debugMode) {
947
- throw err;
948
- }
949
- if (this.userDatabase.isRetriableTransactionError(err)) {
950
- // serialization_failure in PostgreSQL
951
- span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
952
- // Retry serialization failures.
953
- await (0, utils_1.sleepms)(retryWaitMillis);
954
- retryWaitMillis *= backoffFactor;
955
- retryWaitMillis = retryWaitMillis < maxRetryWaitMs ? retryWaitMillis : maxRetryWaitMs;
956
- continue;
957
- }
958
- // Record and throw other errors.
959
991
  const e = err;
960
- await this.userDatabase.transaction(async (client) => {
961
- await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
962
- const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
963
- await this.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, (error) => this.userDatabase.isKeyConflictError(error));
964
- }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
965
- wfCtx.resultBuffer.clear();
992
+ if (!this.debugMode) {
993
+ if (this.userDatabase.isRetriableTransactionError(err)) {
994
+ // serialization_failure in PostgreSQL
995
+ span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
996
+ // Retry serialization failures.
997
+ await (0, utils_1.sleepms)(retryWaitMillis);
998
+ retryWaitMillis *= backoffFactor;
999
+ retryWaitMillis = retryWaitMillis < maxRetryWaitMs ? retryWaitMillis : maxRetryWaitMs;
1000
+ continue;
1001
+ }
1002
+ // Record and throw other errors.
1003
+ const e = err;
1004
+ await this.userDatabase.transaction(async (client) => {
1005
+ await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
1006
+ const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
1007
+ await this.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, (error) => this.userDatabase.isKeyConflictError(error));
1008
+ }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
1009
+ wfCtx.resultBuffer.clear();
1010
+ }
966
1011
  span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: e.message });
967
1012
  this.tracer.endSpan(span);
968
1013
  throw err;
@@ -990,7 +1035,7 @@ class DBOSExecutor {
990
1035
  if (this.workflowCancellationMap.get(wfCtx.workflowUUID) === true) {
991
1036
  throw new error_1.DBOSWorkflowCancelledError(wfCtx.workflowUUID);
992
1037
  }
993
- const executeLocally = this.debugMode || (procInfo.config.executeLocally ?? false);
1038
+ const executeLocally = this.isDebugging || (procInfo.config.executeLocally ?? false);
994
1039
  const funcId = wfCtx.functionIDGetIncrement();
995
1040
  const span = this.tracer.startSpan(proc.name, {
996
1041
  operationUUID: wfCtx.workflowUUID,
@@ -1030,42 +1075,71 @@ class DBOSExecutor {
1030
1075
  let txn_snapshot = 'invalid';
1031
1076
  const wrappedProcedure = async (client) => {
1032
1077
  const ctxt = new procedure_1.StoredProcedureContextImpl(client, wfCtx, span, this.logger, funcId, proc.name);
1078
+ let prevResult = exports.dbosNull;
1079
+ const queryFunc = (sql, args) => this.procedurePool.query(sql, args).then((v) => v.rows);
1033
1080
  if (wfCtx.presetUUID) {
1034
- const func = (sql, args) => this.procedurePool.query(sql, args).then((v) => v.rows);
1035
- const check = await this.#checkExecution(func, wfCtx.workflowUUID, funcId);
1036
- txn_snapshot = check.txn_snapshot;
1037
- if (check.output !== exports.dbosNull) {
1081
+ const executionResult = await this.#checkExecution(queryFunc, wfCtx.workflowUUID, funcId);
1082
+ prevResult = executionResult.result;
1083
+ txn_snapshot = executionResult.txn_snapshot;
1084
+ if (prevResult !== exports.dbosNull) {
1038
1085
  ctxt.span.setAttribute('cached', true);
1039
- ctxt.span.setStatus({ code: api_1.SpanStatusCode.OK });
1040
- this.tracer.endSpan(ctxt.span);
1041
- return check.output;
1086
+ if (this.debugMode === DebugMode.TIME_TRAVEL) {
1087
+ // for time travel debugging, navigate the proxy to the time of this transaction's snapshot
1088
+ await queryFunc(`--proxy:${executionResult.txn_id ?? ''}:${txn_snapshot}`, []);
1089
+ }
1090
+ else {
1091
+ // otherwise, return/throw the previous result
1092
+ if (prevResult instanceof Error) {
1093
+ throw prevResult;
1094
+ }
1095
+ else {
1096
+ return prevResult;
1097
+ }
1098
+ }
1042
1099
  }
1043
1100
  }
1044
1101
  else {
1045
1102
  // Collect snapshot information for read-only transactions and non-preset UUID transactions, if not already collected above
1046
- const func = (sql, args) => this.procedurePool.query(sql, args).then((v) => v.rows);
1047
- txn_snapshot = await DBOSExecutor.#retrieveSnapshot(func);
1103
+ txn_snapshot = await DBOSExecutor.#retrieveSnapshot(queryFunc);
1048
1104
  }
1049
- if (this.debugMode) {
1105
+ if (this.isDebugging && prevResult === exports.dbosNull) {
1050
1106
  throw new error_1.DBOSDebuggerError(`Failed to find the recorded output for the procedure: workflow UUID ${wfCtx.workflowUUID}, step number ${funcId}`);
1051
1107
  }
1052
1108
  // For non-read-only transactions, flush the result buffer.
1053
- if (!readOnly) {
1109
+ if (!this.isDebugging && !readOnly) {
1054
1110
  await this.#flushResultBufferProc(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
1055
1111
  }
1056
- let cresult;
1057
- if (procInfo.registration.passContext) {
1058
- await (0, context_1.runWithStoredProcContext)(ctxt, async () => {
1059
- cresult = await proc(ctxt, ...args);
1060
- });
1112
+ // Execute the user's transaction.
1113
+ const result = await (async function () {
1114
+ try {
1115
+ return await (0, context_1.runWithStoredProcContext)(ctxt, async () => {
1116
+ if (procInfo.registration.passContext) {
1117
+ return await proc(ctxt, ...args);
1118
+ }
1119
+ else {
1120
+ const pf = proc;
1121
+ return await pf(...args);
1122
+ }
1123
+ });
1124
+ }
1125
+ catch (e) {
1126
+ return e instanceof Error ? e : new Error(`${e}`);
1127
+ }
1128
+ })();
1129
+ if (this.isDebugging) {
1130
+ if (prevResult instanceof Error) {
1131
+ throw prevResult;
1132
+ }
1133
+ const prevResultJson = utils_1.DBOSJSON.stringify(prevResult);
1134
+ const resultJson = utils_1.DBOSJSON.stringify(result);
1135
+ if (prevResultJson !== resultJson) {
1136
+ this.logger.error(`Detected different transaction output than the original one!\n Result: ${resultJson}\n Original: ${utils_1.DBOSJSON.stringify(prevResultJson)}`);
1137
+ }
1138
+ return prevResult;
1061
1139
  }
1062
- else {
1063
- await (0, context_1.runWithStoredProcContext)(ctxt, async () => {
1064
- const pf = proc;
1065
- cresult = await pf(...args);
1066
- });
1140
+ if (result instanceof Error) {
1141
+ throw result;
1067
1142
  }
1068
- const result = cresult;
1069
1143
  if (readOnly) {
1070
1144
  // Buffer the output of read-only transactions instead of synchronously writing it.
1071
1145
  const readOutput = {
@@ -1093,34 +1167,36 @@ class DBOSExecutor {
1093
1167
  return result;
1094
1168
  }
1095
1169
  catch (err) {
1096
- if (this.userDatabase.isRetriableTransactionError(err)) {
1097
- // serialization_failure in PostgreSQL
1098
- span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
1099
- // Retry serialization failures.
1100
- await (0, utils_1.sleepms)(retryWaitMillis);
1101
- retryWaitMillis *= backoffFactor;
1102
- retryWaitMillis = retryWaitMillis < maxRetryWaitMs ? retryWaitMillis : maxRetryWaitMs;
1103
- continue;
1170
+ if (!this.isDebugging) {
1171
+ if (this.userDatabase.isRetriableTransactionError(err)) {
1172
+ // serialization_failure in PostgreSQL
1173
+ span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
1174
+ // Retry serialization failures.
1175
+ await (0, utils_1.sleepms)(retryWaitMillis);
1176
+ retryWaitMillis *= backoffFactor;
1177
+ retryWaitMillis = retryWaitMillis < maxRetryWaitMs ? retryWaitMillis : maxRetryWaitMs;
1178
+ continue;
1179
+ }
1180
+ // Record and throw other errors.
1181
+ const e = err;
1182
+ await this.invokeStoredProcFunction(async (client) => {
1183
+ await this.#flushResultBufferProc(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
1184
+ const func = (sql, args) => client.query(sql, args).then((v) => v.rows);
1185
+ await this.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, user_database_1.pgNodeIsKeyConflictError);
1186
+ }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
1187
+ await this.userDatabase.transaction(async (client) => {
1188
+ await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
1189
+ const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
1190
+ await this.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, (error) => this.userDatabase.isKeyConflictError(error));
1191
+ }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
1192
+ wfCtx.resultBuffer.clear();
1104
1193
  }
1105
- // Record and throw other errors.
1106
- const e = err;
1107
- await this.invokeStoredProcFunction(async (client) => {
1108
- await this.#flushResultBufferProc(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
1109
- const func = (sql, args) => client.query(sql, args).then((v) => v.rows);
1110
- await this.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, user_database_1.pgNodeIsKeyConflictError);
1111
- }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
1112
- await this.userDatabase.transaction(async (client) => {
1113
- await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
1114
- const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
1115
- await this.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, (error) => this.userDatabase.isKeyConflictError(error));
1116
- }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
1117
- wfCtx.resultBuffer.clear();
1118
1194
  throw err;
1119
1195
  }
1120
1196
  }
1121
1197
  }
1122
1198
  async #callProcedureFunctionRemote(proc, args, wfCtx, span, config, funcId) {
1123
- if (this.debugMode) {
1199
+ if (this.isDebugging) {
1124
1200
  throw new error_1.DBOSDebuggerError("Can't invoke stored procedure in debug mode.");
1125
1201
  }
1126
1202
  const readOnly = config.readOnly ?? false;
@@ -1263,7 +1339,7 @@ class DBOSExecutor {
1263
1339
  this.tracer.endSpan(ctxt.span);
1264
1340
  return check;
1265
1341
  }
1266
- if (this.debugMode) {
1342
+ if (this.isDebugging) {
1267
1343
  throw new error_1.DBOSDebuggerError(`Failed to find the recorded output for the step: workflow UUID: ${wfCtx.workflowUUID}, step number: ${funcID}`);
1268
1344
  }
1269
1345
  // Execute the step function. If it throws an exception, retry with exponential backoff.
@@ -1418,7 +1494,7 @@ class DBOSExecutor {
1418
1494
  * It runs to completion all pending workflows that were executing when the previous executor failed.
1419
1495
  */
1420
1496
  async recoverPendingWorkflows(executorIDs = ['local']) {
1421
- if (this.debugMode) {
1497
+ if (this.isDebugging) {
1422
1498
  throw new error_1.DBOSDebuggerError('Cannot recover pending workflows in debug mode.');
1423
1499
  }
1424
1500
  const handlerArray = [];
@@ -1602,14 +1678,14 @@ class DBOSExecutor {
1602
1678
  * Periodically flush the workflow output buffer to the system database.
1603
1679
  */
1604
1680
  async flushWorkflowBuffers() {
1605
- if (this.initialized && !this.debugMode) {
1681
+ if (this.initialized && !this.isDebugging) {
1606
1682
  await this.flushWorkflowResultBuffer();
1607
1683
  await this.systemDatabase.flushWorkflowSystemBuffers();
1608
1684
  }
1609
1685
  this.isFlushingBuffers = false;
1610
1686
  }
1611
1687
  async flushWorkflowResultBuffer() {
1612
- if (this.debugMode) {
1688
+ if (this.isDebugging) {
1613
1689
  throw new error_1.DBOSDebuggerError(`Cannot flush workflow result buffer in debug mode.`);
1614
1690
  }
1615
1691
  const localBuffer = new Map(this.workflowResultBuffer);