@dbos-inc/dbos-sdk 2.6.2-preview → 2.6.11-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.
- package/dist/schemas/system_db_schema.d.ts +4 -0
- package/dist/schemas/system_db_schema.d.ts.map +1 -1
- package/dist/schemas/user_db_schema.d.ts +3 -3
- package/dist/schemas/user_db_schema.d.ts.map +1 -1
- package/dist/src/conductor/conductor.d.ts +15 -0
- package/dist/src/conductor/conductor.d.ts.map +1 -0
- package/dist/src/conductor/conductor.js +229 -0
- package/dist/src/conductor/conductor.js.map +1 -0
- package/dist/src/conductor/protocol.d.ts +162 -0
- package/dist/src/conductor/protocol.d.ts.map +1 -0
- package/dist/src/conductor/protocol.js +231 -0
- package/dist/src/conductor/protocol.js.map +1 -0
- package/dist/src/dbos-executor.d.ts +12 -3
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +212 -136
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos-runtime/cli.d.ts.map +1 -1
- package/dist/src/dbos-runtime/cli.js +2 -1
- package/dist/src/dbos-runtime/cli.js.map +1 -1
- package/dist/src/dbos-runtime/config.d.ts.map +1 -1
- package/dist/src/dbos-runtime/config.js +4 -1
- package/dist/src/dbos-runtime/config.js.map +1 -1
- package/dist/src/dbos-runtime/debug.d.ts +1 -1
- package/dist/src/dbos-runtime/debug.d.ts.map +1 -1
- package/dist/src/dbos-runtime/debug.js +3 -3
- package/dist/src/dbos-runtime/debug.js.map +1 -1
- package/dist/src/dbos-runtime/workflow_management.d.ts +8 -8
- package/dist/src/dbos-runtime/workflow_management.d.ts.map +1 -1
- package/dist/src/dbos-runtime/workflow_management.js +7 -6
- package/dist/src/dbos-runtime/workflow_management.js.map +1 -1
- package/dist/src/dbos.d.ts +10 -2
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +64 -34
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/decorators.d.ts +7 -1
- package/dist/src/decorators.d.ts.map +1 -1
- package/dist/src/decorators.js +73 -13
- package/dist/src/decorators.js.map +1 -1
- package/dist/src/error.d.ts +1 -1
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +1 -2
- package/dist/src/error.js.map +1 -1
- package/dist/src/system_database.d.ts +16 -6
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +124 -79
- package/dist/src/system_database.js.map +1 -1
- package/dist/src/testing/testing_runtime.d.ts +3 -4
- package/dist/src/testing/testing_runtime.d.ts.map +1 -1
- package/dist/src/testing/testing_runtime.js +4 -4
- package/dist/src/testing/testing_runtime.js.map +1 -1
- package/dist/src/utils.d.ts +6 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +33 -2
- package/dist/src/utils.js.map +1 -1
- package/dist/src/workflow.d.ts +5 -0
- package/dist/src/workflow.d.ts.map +1 -1
- package/dist/src/workflow.js +3 -1
- package/dist/src/workflow.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -2
@@ -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 =
|
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.
|
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.
|
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.
|
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.
|
311
|
-
if (!this.
|
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.
|
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.
|
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
|
}
|
@@ -522,9 +542,9 @@ class DBOSExecutor {
|
|
522
542
|
const internalStatus = {
|
523
543
|
workflowUUID: workflowUUID,
|
524
544
|
status: params.queueName !== undefined ? workflow_1.StatusString.ENQUEUED : workflow_1.StatusString.PENDING,
|
525
|
-
|
526
|
-
|
527
|
-
|
545
|
+
workflowName: wf.name,
|
546
|
+
workflowClassName: wCtxt.isTempWorkflow ? '' : (0, decorators_1.getRegisteredMethodClassName)(wf),
|
547
|
+
workflowConfigName: params.configuredInstance?.name || '',
|
528
548
|
queueName: params.queueName,
|
529
549
|
authenticatedUser: wCtxt.authenticatedUser,
|
530
550
|
output: undefined,
|
@@ -532,15 +552,15 @@ class DBOSExecutor {
|
|
532
552
|
assumedRole: wCtxt.assumedRole,
|
533
553
|
authenticatedRoles: wCtxt.authenticatedRoles,
|
534
554
|
request: wCtxt.request,
|
535
|
-
|
555
|
+
executorId: wCtxt.executorID,
|
536
556
|
applicationVersion: utils_1.globalParams.appVersion,
|
537
557
|
applicationID: wCtxt.applicationID,
|
538
558
|
createdAt: Date.now(), // Remember the start time of this workflow
|
539
559
|
maxRetries: wCtxt.maxRecoveryAttempts,
|
540
560
|
};
|
541
561
|
if (wCtxt.isTempWorkflow) {
|
542
|
-
internalStatus.
|
543
|
-
internalStatus.
|
562
|
+
internalStatus.workflowName = `${DBOSExecutor.tempWorkflowName}-${wCtxt.tempWfOperationType}-${wCtxt.tempWfOperationName}`;
|
563
|
+
internalStatus.workflowClassName = params.tempWfClass ?? '';
|
544
564
|
}
|
545
565
|
let status = undefined;
|
546
566
|
// Synchronously set the workflow's status to PENDING and record workflow inputs (for non single-transaction workflows).
|
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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 });
|
@@ -631,14 +651,14 @@ class DBOSExecutor {
|
|
631
651
|
this.logger.error(e);
|
632
652
|
e.dbos_already_logged = true;
|
633
653
|
if (wCtxt.isTempWorkflow) {
|
634
|
-
internalStatus.
|
654
|
+
internalStatus.workflowName = `${DBOSExecutor.tempWorkflowName}-${wCtxt.tempWfOperationType}-${wCtxt.tempWfOperationName}`;
|
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.
|
658
|
+
if (internalStatus.queueName && !this.isDebugging) {
|
639
659
|
await this.systemDatabase.dequeueWorkflow(workflowUUID, this.#getQueueByName(internalStatus.queueName));
|
640
660
|
}
|
641
|
-
if (!this.
|
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.
|
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.
|
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.
|
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,
|
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
|
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
|
-
|
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
|
-
|
725
|
-
|
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
|
-
|
743
|
+
return { result: utils_1.DBOSJSON.parse(rows[1].output), txn_snapshot, txn_id: txn_id ?? undefined };
|
729
744
|
}
|
730
745
|
}
|
731
|
-
|
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.
|
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.
|
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.
|
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
|
874
|
-
|
875
|
-
txn_snapshot =
|
876
|
-
if (
|
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
|
-
|
879
|
-
|
880
|
-
|
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
|
-
|
886
|
-
txn_snapshot = await DBOSExecutor.#retrieveSnapshot(func);
|
915
|
+
txn_snapshot = await DBOSExecutor.#retrieveSnapshot(queryFunc);
|
887
916
|
}
|
888
|
-
if (this.
|
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
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
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
|
-
|
903
|
-
|
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
|
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
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
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.
|
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
|
1035
|
-
|
1036
|
-
txn_snapshot =
|
1037
|
-
if (
|
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
|
-
|
1040
|
-
|
1041
|
-
|
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
|
-
|
1047
|
-
txn_snapshot = await DBOSExecutor.#retrieveSnapshot(func);
|
1103
|
+
txn_snapshot = await DBOSExecutor.#retrieveSnapshot(queryFunc);
|
1048
1104
|
}
|
1049
|
-
if (this.
|
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
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
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
|
-
|
1063
|
-
|
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.
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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);
|