@dbos-inc/dbos-sdk 2.1.5-preview.g539c9d794d → 2.1.12-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/compose.yaml +19 -0
- package/dbos-config.schema.json +2 -11
- package/dist/src/context.d.ts +2 -0
- package/dist/src/context.d.ts.map +1 -1
- package/dist/src/context.js +16 -1
- package/dist/src/context.js.map +1 -1
- package/dist/src/dbos-executor.d.ts +9 -12
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +403 -90
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos-runtime/cli.d.ts +1 -0
- package/dist/src/dbos-runtime/cli.d.ts.map +1 -1
- package/dist/src/dbos-runtime/cli.js +13 -2
- package/dist/src/dbos-runtime/cli.js.map +1 -1
- package/dist/src/dbos-runtime/config.d.ts +8 -7
- package/dist/src/dbos-runtime/config.d.ts.map +1 -1
- package/dist/src/dbos-runtime/config.js +26 -18
- package/dist/src/dbos-runtime/config.js.map +1 -1
- package/dist/src/dbos-runtime/db_connection.d.ts +10 -0
- package/dist/src/dbos-runtime/db_connection.d.ts.map +1 -0
- package/dist/src/dbos-runtime/db_connection.js +59 -0
- package/dist/src/dbos-runtime/db_connection.js.map +1 -0
- package/dist/src/dbos-runtime/db_wizard.d.ts.map +1 -1
- package/dist/src/dbos-runtime/db_wizard.js +10 -14
- package/dist/src/dbos-runtime/db_wizard.js.map +1 -1
- package/dist/src/dbos-runtime/migrate.d.ts.map +1 -1
- package/dist/src/dbos-runtime/migrate.js +2 -3
- package/dist/src/dbos-runtime/migrate.js.map +1 -1
- package/dist/src/dbos-runtime/reset.d.ts +4 -0
- package/dist/src/dbos-runtime/reset.d.ts.map +1 -0
- package/dist/src/dbos-runtime/reset.js +39 -0
- package/dist/src/dbos-runtime/reset.js.map +1 -0
- package/dist/src/dbos.d.ts +2 -0
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +50 -1
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/error.d.ts +3 -0
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +10 -2
- package/dist/src/error.js.map +1 -1
- package/dist/src/eventreceiver.d.ts +2 -0
- package/dist/src/eventreceiver.d.ts.map +1 -1
- package/dist/src/httpServer/handler.js.map +1 -1
- package/dist/src/procedure.d.ts +3 -4
- package/dist/src/procedure.d.ts.map +1 -1
- package/dist/src/procedure.js +3 -1
- package/dist/src/procedure.js.map +1 -1
- package/dist/src/step.d.ts +1 -2
- package/dist/src/step.d.ts.map +1 -1
- package/dist/src/step.js.map +1 -1
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +31 -4
- package/dist/src/system_database.js.map +1 -1
- package/dist/src/testing/testing_runtime.js.map +1 -1
- package/dist/src/transaction.d.ts +1 -2
- package/dist/src/transaction.d.ts.map +1 -1
- package/dist/src/transaction.js.map +1 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +1 -14
- package/dist/src/utils.js.map +1 -1
- package/dist/src/workflow.d.ts +1 -13
- package/dist/src/workflow.d.ts.map +1 -1
- package/dist/src/workflow.js +4 -322
- package/dist/src/workflow.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/src/debugger/debug_workflow.d.ts +0 -56
- package/dist/src/debugger/debug_workflow.d.ts.map +0 -1
- package/dist/src/debugger/debug_workflow.js +0 -388
- package/dist/src/debugger/debug_workflow.js.map +0 -1
@@ -20,10 +20,10 @@ const decorators_1 = require("./decorators");
|
|
20
20
|
const api_1 = require("@opentelemetry/api");
|
21
21
|
const knex_1 = __importDefault(require("knex"));
|
22
22
|
const context_1 = require("./context");
|
23
|
-
const debug_workflow_1 = require("./debugger/debug_workflow");
|
24
23
|
const serialize_error_1 = require("serialize-error");
|
25
24
|
const utils_1 = require("./utils");
|
26
25
|
const node_path_1 = __importDefault(require("node:path"));
|
26
|
+
const procedure_1 = require("./procedure");
|
27
27
|
const lodash_1 = require("lodash");
|
28
28
|
const wfqueue_1 = require("./wfqueue");
|
29
29
|
const debugpoint_1 = require("./debugpoint");
|
@@ -279,7 +279,6 @@ class DBOSExecutor {
|
|
279
279
|
}
|
280
280
|
this.logger.debug(`Loaded ${length} ORM entities`);
|
281
281
|
}
|
282
|
-
await ((0, user_database_1.createDBIfDoesNotExist)(this.config.poolConfig, this.logger));
|
283
282
|
this.configureDbClient();
|
284
283
|
if (!this.userDatabase) {
|
285
284
|
this.logger.error("No user database configured!");
|
@@ -354,24 +353,6 @@ class DBOSExecutor {
|
|
354
353
|
this.logger.error(`Unknown notice severity: ${msg.severity} - ${msg.message}`);
|
355
354
|
}
|
356
355
|
}
|
357
|
-
async callProcedure(proc, args) {
|
358
|
-
const client = await this.procedurePool.connect();
|
359
|
-
const log = (msg) => this.#logNotice(msg);
|
360
|
-
const procClassName = this.getProcedureClassName(proc);
|
361
|
-
const plainProcName = `${procClassName}_${proc.name}_p`;
|
362
|
-
const procName = this.config.appVersion
|
363
|
-
? `v${this.config.appVersion}_${plainProcName}`
|
364
|
-
: plainProcName;
|
365
|
-
const sql = `CALL "${procName}"(${args.map((_v, i) => `$${i + 1}`).join()});`;
|
366
|
-
try {
|
367
|
-
client.on('notice', log);
|
368
|
-
return await client.query(sql, args).then(value => value.rows);
|
369
|
-
}
|
370
|
-
finally {
|
371
|
-
client.off('notice', log);
|
372
|
-
client.release();
|
373
|
-
}
|
374
|
-
}
|
375
356
|
async destroy() {
|
376
357
|
if (this.pendingWorkflowMap.size > 0) {
|
377
358
|
this.logger.info("Waiting for pending workflows to finish.");
|
@@ -501,9 +482,6 @@ class DBOSExecutor {
|
|
501
482
|
}
|
502
483
|
// TODO: getProcedureInfoByNames??
|
503
484
|
async workflow(wf, params, ...args) {
|
504
|
-
if (this.debugMode) {
|
505
|
-
return this.debugWorkflow(wf, params, undefined, undefined, ...args);
|
506
|
-
}
|
507
485
|
return this.internalWorkflow(wf, params, undefined, undefined, ...args);
|
508
486
|
}
|
509
487
|
// If callerUUID and functionID are set, it means the workflow is invoked from within a workflow.
|
@@ -547,35 +525,57 @@ class DBOSExecutor {
|
|
547
525
|
if ((wCtxt.tempWfOperationType !== TempWorkflowType.transaction
|
548
526
|
&& wCtxt.tempWfOperationType !== TempWorkflowType.procedure)
|
549
527
|
|| params.queueName !== undefined) {
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
528
|
+
if (this.debugMode) {
|
529
|
+
const wfStatus = await this.systemDatabase.getWorkflowStatus(workflowUUID);
|
530
|
+
const wfInputs = await this.systemDatabase.getWorkflowInputs(workflowUUID);
|
531
|
+
if (!wfStatus || !wfInputs) {
|
532
|
+
throw new error_1.DBOSDebuggerError(`Failed to find inputs for workflow UUID ${workflowUUID}`);
|
533
|
+
}
|
534
|
+
// Make sure we use the same input.
|
535
|
+
if (utils_1.DBOSJSON.stringify(args) !== utils_1.DBOSJSON.stringify(wfInputs)) {
|
536
|
+
throw new error_1.DBOSDebuggerError(`Detected different inputs for workflow UUID ${workflowUUID}.\n Received: ${utils_1.DBOSJSON.stringify(args)}\n Original: ${utils_1.DBOSJSON.stringify(wfInputs)}`);
|
537
|
+
}
|
538
|
+
status = wfStatus.status;
|
539
|
+
}
|
540
|
+
else {
|
541
|
+
// TODO: Make this transactional (and with the queue step below)
|
542
|
+
const ires = await this.systemDatabase.initWorkflowStatus(internalStatus, args);
|
543
|
+
args = ires.args;
|
544
|
+
status = ires.status;
|
545
|
+
await (0, debugpoint_1.debugTriggerPoint)(debugpoint_1.DEBUG_TRIGGER_WORKFLOW_ENQUEUE);
|
546
|
+
}
|
555
547
|
}
|
556
548
|
const runWorkflow = async () => {
|
557
549
|
let result;
|
558
550
|
// Execute the workflow.
|
559
551
|
try {
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
else {
|
566
|
-
cresult = await wf.call(params.configuredInstance, ...args);
|
567
|
-
}
|
552
|
+
const callResult = await (0, context_1.runWithWorkflowContext)(wCtxt, async () => {
|
553
|
+
const callPromise = passContext
|
554
|
+
? wf.call(params.configuredInstance, wCtxt, ...args)
|
555
|
+
: wf.call(params.configuredInstance, ...args);
|
556
|
+
return await callPromise;
|
568
557
|
});
|
569
|
-
|
558
|
+
if (this.debugMode) {
|
559
|
+
const recordedResult = await this.systemDatabase.getWorkflowResult(workflowUUID);
|
560
|
+
if (utils_1.DBOSJSON.stringify(callResult) !== utils_1.DBOSJSON.stringify(recordedResult)) {
|
561
|
+
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)}`);
|
562
|
+
}
|
563
|
+
result = recordedResult;
|
564
|
+
}
|
565
|
+
else {
|
566
|
+
result = callResult;
|
567
|
+
}
|
570
568
|
internalStatus.output = result;
|
571
569
|
internalStatus.status = workflow_1.StatusString.SUCCESS;
|
572
|
-
if (internalStatus.queueName) {
|
570
|
+
if (internalStatus.queueName && !this.debugMode) {
|
573
571
|
// Now... the workflow isn't certainly done.
|
574
572
|
// But waiting this long is for concurrency control anyway,
|
575
573
|
// so it is probably done enough.
|
576
574
|
await this.systemDatabase.dequeueWorkflow(workflowUUID, this.#getQueueByName(internalStatus.queueName));
|
577
575
|
}
|
578
|
-
this.
|
576
|
+
if (!this.debugMode) {
|
577
|
+
this.systemDatabase.bufferWorkflowOutput(workflowUUID, internalStatus);
|
578
|
+
}
|
579
579
|
wCtxt.span.setStatus({ code: api_1.SpanStatusCode.OK });
|
580
580
|
}
|
581
581
|
catch (err) {
|
@@ -596,10 +596,12 @@ class DBOSExecutor {
|
|
596
596
|
}
|
597
597
|
internalStatus.error = utils_1.DBOSJSON.stringify((0, serialize_error_1.serializeError)(e));
|
598
598
|
internalStatus.status = workflow_1.StatusString.ERROR;
|
599
|
-
if (internalStatus.queueName) {
|
599
|
+
if (internalStatus.queueName && !this.debugMode) {
|
600
600
|
await this.systemDatabase.dequeueWorkflow(workflowUUID, this.#getQueueByName(internalStatus.queueName));
|
601
601
|
}
|
602
|
-
|
602
|
+
if (!this.debugMode) {
|
603
|
+
await this.systemDatabase.recordWorkflowError(workflowUUID, internalStatus);
|
604
|
+
}
|
603
605
|
// TODO: Log errors, but not in the tests when they're expected.
|
604
606
|
wCtxt.span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: e.message });
|
605
607
|
throw err;
|
@@ -611,7 +613,9 @@ class DBOSExecutor {
|
|
611
613
|
|| wCtxt.tempWfOperationType === TempWorkflowType.procedure) {
|
612
614
|
// For single-transaction workflows, asynchronously record inputs.
|
613
615
|
// We must buffer inputs after workflow status is buffered/flushed because workflow_inputs table has a foreign key reference to the workflow_status table.
|
614
|
-
this.
|
616
|
+
if (!this.debugMode) {
|
617
|
+
this.systemDatabase.bufferWorkflowInputs(workflowUUID, args);
|
618
|
+
}
|
615
619
|
}
|
616
620
|
}
|
617
621
|
// Asynchronously flush the result buffer.
|
@@ -620,7 +624,7 @@ class DBOSExecutor {
|
|
620
624
|
}
|
621
625
|
return result;
|
622
626
|
};
|
623
|
-
if (status !== 'SUCCESS' && status !== 'ERROR' && (params.queueName === undefined || params.executeWorkflow)) {
|
627
|
+
if (this.debugMode || (status !== 'SUCCESS' && status !== 'ERROR' && (params.queueName === undefined || params.executeWorkflow))) {
|
624
628
|
const workflowPromise = runWorkflow();
|
625
629
|
// Need to await for the workflow and capture errors.
|
626
630
|
const awaitWorkflowPromise = workflowPromise
|
@@ -636,7 +640,7 @@ class DBOSExecutor {
|
|
636
640
|
return new workflow_1.InvokedHandle(this.systemDatabase, workflowPromise, workflowUUID, wf.name, callerUUID, callerFunctionID);
|
637
641
|
}
|
638
642
|
else {
|
639
|
-
if (params.queueName && status === 'ENQUEUED') {
|
643
|
+
if (params.queueName && status === 'ENQUEUED' && !this.debugMode) {
|
640
644
|
await this.systemDatabase.enqueueWorkflow(workflowUUID, this.#getQueueByName(params.queueName));
|
641
645
|
}
|
642
646
|
return new workflow_1.RetrievedHandle(this.systemDatabase, workflowUUID, callerUUID, callerFunctionID);
|
@@ -649,45 +653,135 @@ class DBOSExecutor {
|
|
649
653
|
return q;
|
650
654
|
}
|
651
655
|
/**
|
652
|
-
*
|
656
|
+
* Retrieve the transaction snapshot information of the current transaction
|
657
|
+
*/
|
658
|
+
static async #retrieveSnapshot(query) {
|
659
|
+
const rows = await query("SELECT pg_current_snapshot()::text as txn_snapshot;", []);
|
660
|
+
return rows[0].txn_snapshot;
|
661
|
+
}
|
662
|
+
/**
|
663
|
+
* Check if an operation has already executed in a workflow.
|
664
|
+
* If it previously executed successfully, return its output.
|
665
|
+
* If it previously executed and threw an error, throw that error.
|
666
|
+
* Otherwise, return DBOSNull.
|
667
|
+
* Also return the transaction snapshot information of this current transaction.
|
653
668
|
*/
|
654
|
-
async
|
655
|
-
//
|
656
|
-
|
657
|
-
|
669
|
+
async #checkExecution(query, workflowUUID, funcID) {
|
670
|
+
// Note: we read the current snapshot, not the recorded one!
|
671
|
+
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]);
|
672
|
+
if (rows.length === 0 || rows.length > 2) {
|
673
|
+
this.logger.error("Unexpected! This should never happen. Returned rows: " + rows.toString());
|
674
|
+
throw new error_1.DBOSError("This should never happen. Returned rows: " + rows.toString());
|
675
|
+
}
|
676
|
+
const res = {
|
677
|
+
output: exports.dbosNull,
|
678
|
+
txn_snapshot: ""
|
679
|
+
};
|
680
|
+
// recorded=false row will be first because we used ORDER BY.
|
681
|
+
res.txn_snapshot = rows[0].txn_snapshot;
|
682
|
+
if (rows.length === 2) {
|
683
|
+
if (utils_1.DBOSJSON.parse(rows[1].error) !== null) {
|
684
|
+
throw (0, serialize_error_1.deserializeError)(utils_1.DBOSJSON.parse(rows[1].error));
|
685
|
+
}
|
686
|
+
else {
|
687
|
+
res.output = utils_1.DBOSJSON.parse(rows[1].output);
|
688
|
+
}
|
658
689
|
}
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
690
|
+
return res;
|
691
|
+
}
|
692
|
+
/**
|
693
|
+
* Write a operation's output to the database.
|
694
|
+
*/
|
695
|
+
async #recordOutput(query, workflowUUID, funcID, txnSnapshot, output, isKeyConflict) {
|
696
|
+
if (this.debugMode) {
|
697
|
+
throw new error_1.DBOSDebuggerError("Cannot record output in debug mode.");
|
663
698
|
}
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
699
|
+
try {
|
700
|
+
const serialOutput = utils_1.DBOSJSON.stringify(output);
|
701
|
+
const rows = await query("INSERT INTO dbos.transaction_outputs (workflow_uuid, function_id, output, txn_id, txn_snapshot, created_at) VALUES ($1, $2, $3, (select pg_current_xact_id_if_assigned()::text), $4, $5) RETURNING txn_id;", [workflowUUID, funcID, serialOutput, txnSnapshot, Date.now()]);
|
702
|
+
return rows[0].txn_id;
|
703
|
+
}
|
704
|
+
catch (error) {
|
705
|
+
if (isKeyConflict(error)) {
|
706
|
+
// Serialization and primary key conflict (Postgres).
|
707
|
+
throw new error_1.DBOSWorkflowConflictUUIDError(workflowUUID);
|
708
|
+
}
|
709
|
+
else {
|
710
|
+
throw error;
|
711
|
+
}
|
712
|
+
}
|
713
|
+
}
|
714
|
+
/**
|
715
|
+
* Record an error in an operation to the database.
|
716
|
+
*/
|
717
|
+
async #recordError(query, workflowUUID, funcID, txnSnapshot, err, isKeyConflict) {
|
718
|
+
if (this.debugMode) {
|
719
|
+
throw new error_1.DBOSDebuggerError("Cannot record error in debug mode.");
|
720
|
+
}
|
721
|
+
try {
|
722
|
+
const serialErr = utils_1.DBOSJSON.stringify((0, serialize_error_1.serializeError)(err));
|
723
|
+
await query("INSERT INTO dbos.transaction_outputs (workflow_uuid, function_id, error, txn_id, txn_snapshot, created_at) VALUES ($1, $2, $3, null, $4, $5) RETURNING txn_id;", [workflowUUID, funcID, serialErr, txnSnapshot, Date.now()]);
|
724
|
+
}
|
725
|
+
catch (error) {
|
726
|
+
if (isKeyConflict(error)) {
|
727
|
+
// Serialization and primary key conflict (Postgres).
|
728
|
+
throw new error_1.DBOSWorkflowConflictUUIDError(workflowUUID);
|
729
|
+
}
|
730
|
+
else {
|
731
|
+
throw error;
|
732
|
+
}
|
733
|
+
}
|
734
|
+
}
|
735
|
+
/**
|
736
|
+
* Write all entries in the workflow result buffer to the database.
|
737
|
+
* If it encounters a primary key error, this indicates a concurrent execution with the same UUID, so throw an DBOSError.
|
738
|
+
*/
|
739
|
+
async #flushResultBuffer(query, resultBuffer, workflowUUID, isKeyConflict) {
|
740
|
+
const funcIDs = Array.from(resultBuffer.keys());
|
741
|
+
if (funcIDs.length === 0) {
|
742
|
+
return;
|
743
|
+
}
|
744
|
+
if (this.debugMode) {
|
745
|
+
throw new error_1.DBOSDebuggerError("Cannot flush result buffer in debug mode.");
|
746
|
+
}
|
747
|
+
funcIDs.sort();
|
748
|
+
try {
|
749
|
+
let sqlStmt = "INSERT INTO dbos.transaction_outputs (workflow_uuid, function_id, output, error, txn_id, txn_snapshot, created_at) VALUES ";
|
750
|
+
let paramCnt = 1;
|
751
|
+
const values = [];
|
752
|
+
for (const funcID of funcIDs) {
|
753
|
+
// Capture output and also transaction snapshot information.
|
754
|
+
// Initially, no txn_id because no queries executed.
|
755
|
+
const recorded = resultBuffer.get(funcID);
|
756
|
+
const output = recorded.output;
|
757
|
+
const txnSnapshot = recorded.txn_snapshot;
|
758
|
+
const createdAt = recorded.created_at;
|
759
|
+
if (paramCnt > 1) {
|
760
|
+
sqlStmt += ", ";
|
686
761
|
}
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
762
|
+
sqlStmt += `($${paramCnt++}, $${paramCnt++}, $${paramCnt++}, $${paramCnt++}, null, $${paramCnt++}, $${paramCnt++})`;
|
763
|
+
values.push(workflowUUID, funcID, utils_1.DBOSJSON.stringify(output), utils_1.DBOSJSON.stringify(null), txnSnapshot, createdAt);
|
764
|
+
}
|
765
|
+
this.logger.debug(sqlStmt);
|
766
|
+
await query(sqlStmt, values);
|
767
|
+
}
|
768
|
+
catch (error) {
|
769
|
+
if (isKeyConflict(error)) {
|
770
|
+
// Serialization and primary key conflict (Postgres).
|
771
|
+
throw new error_1.DBOSWorkflowConflictUUIDError(workflowUUID);
|
772
|
+
}
|
773
|
+
else {
|
774
|
+
throw error;
|
775
|
+
}
|
776
|
+
}
|
777
|
+
}
|
778
|
+
flushResultBuffer(client, resultBuffer, workflowUUID) {
|
779
|
+
const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
|
780
|
+
return this.#flushResultBuffer(func, resultBuffer, workflowUUID, (error) => this.userDatabase.isKeyConflictError(error));
|
781
|
+
}
|
782
|
+
#flushResultBufferProc(client, resultBuffer, workflowUUID) {
|
783
|
+
const func = (sql, args) => client.query(sql, args).then(v => v.rows);
|
784
|
+
return this.#flushResultBuffer(func, resultBuffer, workflowUUID, user_database_1.pgNodeIsKeyConflictError);
|
691
785
|
}
|
692
786
|
async transaction(txn, params, ...args) {
|
693
787
|
// Create a workflow and call transaction.
|
@@ -729,7 +823,8 @@ class DBOSExecutor {
|
|
729
823
|
// If the UUID is preset, it is possible this execution previously happened. Check, and return its original result if it did.
|
730
824
|
// 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.
|
731
825
|
if (wfCtx.presetUUID) {
|
732
|
-
const
|
826
|
+
const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
|
827
|
+
const check = await this.#checkExecution(func, workflowUUID, funcId);
|
733
828
|
txn_snapshot = check.txn_snapshot;
|
734
829
|
if (check.output !== exports.dbosNull) {
|
735
830
|
tCtxt.span.setAttribute("cached", true);
|
@@ -740,11 +835,15 @@ class DBOSExecutor {
|
|
740
835
|
}
|
741
836
|
else {
|
742
837
|
// Collect snapshot information for read-only transactions and non-preset UUID transactions, if not already collected above
|
743
|
-
|
838
|
+
const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
|
839
|
+
txn_snapshot = await DBOSExecutor.#retrieveSnapshot(func);
|
840
|
+
}
|
841
|
+
if (this.debugMode) {
|
842
|
+
throw new error_1.DBOSDebuggerError(`Failed to find inputs for workflow UUID ${workflowUUID}`);
|
744
843
|
}
|
745
844
|
// For non-read-only transactions, flush the result buffer.
|
746
845
|
if (!readOnly) {
|
747
|
-
await
|
846
|
+
await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
|
748
847
|
}
|
749
848
|
// Execute the user's transaction.
|
750
849
|
let cresult;
|
@@ -773,7 +872,8 @@ class DBOSExecutor {
|
|
773
872
|
else {
|
774
873
|
try {
|
775
874
|
// Synchronously record the output of write transactions and obtain the transaction ID.
|
776
|
-
const
|
875
|
+
const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
|
876
|
+
const pg_txn_id = await this.#recordOutput(func, wfCtx.workflowUUID, funcId, txn_snapshot, result, (error) => this.userDatabase.isKeyConflictError(error));
|
777
877
|
tCtxt.span.setAttribute("pg_txn_id", pg_txn_id);
|
778
878
|
wfCtx.resultBuffer.clear();
|
779
879
|
}
|
@@ -796,6 +896,9 @@ class DBOSExecutor {
|
|
796
896
|
return result;
|
797
897
|
}
|
798
898
|
catch (err) {
|
899
|
+
if (this.debugMode) {
|
900
|
+
throw err;
|
901
|
+
}
|
799
902
|
if (this.userDatabase.isRetriableTransactionError(err)) {
|
800
903
|
// serialization_failure in PostgreSQL
|
801
904
|
span.addEvent("TXN SERIALIZATION FAILURE", { "retryWaitMillis": retryWaitMillis }, performance.now());
|
@@ -808,8 +911,9 @@ class DBOSExecutor {
|
|
808
911
|
// Record and throw other errors.
|
809
912
|
const e = err;
|
810
913
|
await this.userDatabase.transaction(async (client) => {
|
811
|
-
await
|
812
|
-
|
914
|
+
await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
|
915
|
+
const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
|
916
|
+
await this.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, (error) => this.userDatabase.isKeyConflictError(error));
|
813
917
|
}, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
|
814
918
|
wfCtx.resultBuffer.clear();
|
815
919
|
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: e.message });
|
@@ -822,15 +926,215 @@ class DBOSExecutor {
|
|
822
926
|
// Create a workflow and call procedure.
|
823
927
|
const temp_workflow = async (ctxt, ...args) => {
|
824
928
|
const ctxtImpl = ctxt;
|
825
|
-
return
|
929
|
+
return this.callProcedureFunction(proc, ctxtImpl, ...args);
|
826
930
|
};
|
827
|
-
return (await this.workflow(temp_workflow, {
|
931
|
+
return await (await this.workflow(temp_workflow, {
|
932
|
+
...params,
|
828
933
|
tempWfType: TempWorkflowType.procedure,
|
829
934
|
tempWfName: (0, decorators_1.getRegisteredMethodName)(proc),
|
830
935
|
tempWfClass: (0, decorators_1.getRegisteredMethodClassName)(proc),
|
831
936
|
}, ...args)).getResult();
|
832
937
|
}
|
833
|
-
async
|
938
|
+
async callProcedureFunction(proc, wfCtx, ...args) {
|
939
|
+
const procInfo = this.getProcedureInfo(proc);
|
940
|
+
if (procInfo === undefined) {
|
941
|
+
throw new error_1.DBOSNotRegisteredError(proc.name);
|
942
|
+
}
|
943
|
+
const executeLocally = this.debugMode || (procInfo.config.executeLocally ?? false);
|
944
|
+
const funcId = wfCtx.functionIDGetIncrement();
|
945
|
+
const span = this.tracer.startSpan(proc.name, {
|
946
|
+
operationUUID: wfCtx.workflowUUID,
|
947
|
+
operationType: exports.OperationType.PROCEDURE,
|
948
|
+
authenticatedUser: wfCtx.authenticatedUser,
|
949
|
+
assumedRole: wfCtx.assumedRole,
|
950
|
+
authenticatedRoles: wfCtx.authenticatedRoles,
|
951
|
+
readOnly: procInfo.config.readOnly ?? false,
|
952
|
+
isolationLevel: procInfo.config.isolationLevel,
|
953
|
+
executeLocally,
|
954
|
+
}, wfCtx.span);
|
955
|
+
try {
|
956
|
+
const result = executeLocally
|
957
|
+
? await this.#callProcedureFunctionLocal(proc, args, wfCtx, span, procInfo, funcId)
|
958
|
+
: await this.#callProcedureFunctionRemote(proc, args, wfCtx, span, procInfo.config, funcId);
|
959
|
+
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
960
|
+
return result;
|
961
|
+
}
|
962
|
+
catch (e) {
|
963
|
+
const { message } = e;
|
964
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message });
|
965
|
+
throw e;
|
966
|
+
}
|
967
|
+
finally {
|
968
|
+
this.tracer.endSpan(span);
|
969
|
+
}
|
970
|
+
}
|
971
|
+
async #callProcedureFunctionLocal(proc, args, wfCtx, span, procInfo, funcId) {
|
972
|
+
let retryWaitMillis = 1;
|
973
|
+
const backoffFactor = 1.5;
|
974
|
+
const maxRetryWaitMs = 2000; // Maximum wait 2 seconds.
|
975
|
+
const readOnly = procInfo.config.readOnly ?? false;
|
976
|
+
while (true) {
|
977
|
+
let txn_snapshot = "invalid";
|
978
|
+
const wrappedProcedure = async (client) => {
|
979
|
+
const ctxt = new procedure_1.StoredProcedureContextImpl(client, wfCtx, span, this.logger, funcId, proc.name);
|
980
|
+
if (wfCtx.presetUUID) {
|
981
|
+
const func = (sql, args) => this.procedurePool.query(sql, args).then(v => v.rows);
|
982
|
+
const check = await this.#checkExecution(func, wfCtx.workflowUUID, funcId);
|
983
|
+
txn_snapshot = check.txn_snapshot;
|
984
|
+
if (check.output !== exports.dbosNull) {
|
985
|
+
ctxt.span.setAttribute("cached", true);
|
986
|
+
ctxt.span.setStatus({ code: api_1.SpanStatusCode.OK });
|
987
|
+
this.tracer.endSpan(ctxt.span);
|
988
|
+
return check.output;
|
989
|
+
}
|
990
|
+
}
|
991
|
+
else {
|
992
|
+
// Collect snapshot information for read-only transactions and non-preset UUID transactions, if not already collected above
|
993
|
+
const func = (sql, args) => this.procedurePool.query(sql, args).then(v => v.rows);
|
994
|
+
txn_snapshot = await DBOSExecutor.#retrieveSnapshot(func);
|
995
|
+
}
|
996
|
+
if (this.debugMode) {
|
997
|
+
throw new error_1.DBOSDebuggerError(`Failed to find inputs for workflow UUID ${wfCtx.workflowUUID}`);
|
998
|
+
}
|
999
|
+
// For non-read-only transactions, flush the result buffer.
|
1000
|
+
if (!readOnly) {
|
1001
|
+
await this.#flushResultBufferProc(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
|
1002
|
+
}
|
1003
|
+
let cresult;
|
1004
|
+
if (procInfo.registration.passContext) {
|
1005
|
+
await (0, context_1.runWithStoredProcContext)(ctxt, async () => {
|
1006
|
+
cresult = await proc(ctxt, ...args);
|
1007
|
+
});
|
1008
|
+
}
|
1009
|
+
else {
|
1010
|
+
await (0, context_1.runWithStoredProcContext)(ctxt, async () => {
|
1011
|
+
const pf = proc;
|
1012
|
+
cresult = await pf(...args);
|
1013
|
+
});
|
1014
|
+
}
|
1015
|
+
const result = cresult;
|
1016
|
+
if (readOnly) {
|
1017
|
+
// Buffer the output of read-only transactions instead of synchronously writing it.
|
1018
|
+
const readOutput = {
|
1019
|
+
output: result,
|
1020
|
+
txn_snapshot: txn_snapshot,
|
1021
|
+
created_at: Date.now(),
|
1022
|
+
};
|
1023
|
+
wfCtx.resultBuffer.set(funcId, readOutput);
|
1024
|
+
}
|
1025
|
+
else {
|
1026
|
+
// Synchronously record the output of write transactions and obtain the transaction ID.
|
1027
|
+
const func = (sql, args) => client.query(sql, args).then(v => v.rows);
|
1028
|
+
const pg_txn_id = await this.#recordOutput(func, wfCtx.workflowUUID, funcId, txn_snapshot, result, user_database_1.pgNodeIsKeyConflictError);
|
1029
|
+
// const pg_txn_id = await wfCtx.recordOutputProc<R>(client, funcId, txn_snapshot, result);
|
1030
|
+
ctxt.span.setAttribute("pg_txn_id", pg_txn_id);
|
1031
|
+
wfCtx.resultBuffer.clear();
|
1032
|
+
}
|
1033
|
+
return result;
|
1034
|
+
};
|
1035
|
+
try {
|
1036
|
+
const result = await this.invokeStoredProcFunction(wrappedProcedure, { isolationLevel: procInfo.config.isolationLevel });
|
1037
|
+
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
1038
|
+
return result;
|
1039
|
+
}
|
1040
|
+
catch (err) {
|
1041
|
+
if (this.userDatabase.isRetriableTransactionError(err)) {
|
1042
|
+
// serialization_failure in PostgreSQL
|
1043
|
+
span.addEvent("TXN SERIALIZATION FAILURE", { "retryWaitMillis": retryWaitMillis }, performance.now());
|
1044
|
+
// Retry serialization failures.
|
1045
|
+
await (0, utils_1.sleepms)(retryWaitMillis);
|
1046
|
+
retryWaitMillis *= backoffFactor;
|
1047
|
+
retryWaitMillis = retryWaitMillis < maxRetryWaitMs ? retryWaitMillis : maxRetryWaitMs;
|
1048
|
+
continue;
|
1049
|
+
}
|
1050
|
+
// Record and throw other errors.
|
1051
|
+
const e = err;
|
1052
|
+
await this.invokeStoredProcFunction(async (client) => {
|
1053
|
+
await this.#flushResultBufferProc(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
|
1054
|
+
const func = (sql, args) => client.query(sql, args).then(v => v.rows);
|
1055
|
+
await this.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, user_database_1.pgNodeIsKeyConflictError);
|
1056
|
+
}, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
|
1057
|
+
await this.userDatabase.transaction(async (client) => {
|
1058
|
+
await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
|
1059
|
+
const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
|
1060
|
+
await this.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, (error) => this.userDatabase.isKeyConflictError(error));
|
1061
|
+
}, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
|
1062
|
+
wfCtx.resultBuffer.clear();
|
1063
|
+
throw err;
|
1064
|
+
}
|
1065
|
+
}
|
1066
|
+
}
|
1067
|
+
async #callProcedureFunctionRemote(proc, args, wfCtx, span, config, funcId) {
|
1068
|
+
if (this.debugMode) {
|
1069
|
+
throw new error_1.DBOSDebuggerError("Can't invoke stored procedure in debug mode.");
|
1070
|
+
}
|
1071
|
+
const readOnly = config.readOnly ?? false;
|
1072
|
+
const $jsonCtx = {
|
1073
|
+
request: wfCtx.request,
|
1074
|
+
authenticatedUser: wfCtx.authenticatedUser,
|
1075
|
+
authenticatedRoles: wfCtx.authenticatedRoles,
|
1076
|
+
assumedRole: wfCtx.assumedRole,
|
1077
|
+
};
|
1078
|
+
// Note, node-pg converts JS arrays to postgres array literals, so must call JSON.strigify on
|
1079
|
+
// args and bufferedResults before being passed to #invokeStoredProc
|
1080
|
+
const $args = [wfCtx.workflowUUID, funcId, wfCtx.presetUUID, $jsonCtx, null, JSON.stringify(args)];
|
1081
|
+
if (!readOnly) {
|
1082
|
+
// function_id, output, txn_snapshot, created_at
|
1083
|
+
const bufferedResults = new Array();
|
1084
|
+
for (const [functionID, { output, txn_snapshot, created_at }] of wfCtx.resultBuffer.entries()) {
|
1085
|
+
bufferedResults.push([functionID, output, txn_snapshot, created_at]);
|
1086
|
+
}
|
1087
|
+
// sort by function ID
|
1088
|
+
bufferedResults.sort((a, b) => a[0] - b[0]);
|
1089
|
+
$args.unshift(bufferedResults.length > 0 ? JSON.stringify(bufferedResults) : null);
|
1090
|
+
}
|
1091
|
+
const [{ return_value }] = await this.#invokeStoredProc(proc, $args);
|
1092
|
+
const { error, output, txn_snapshot, txn_id, created_at } = return_value;
|
1093
|
+
// buffered results are persisted in r/w stored procs, even if it returns an error
|
1094
|
+
if (!readOnly) {
|
1095
|
+
wfCtx.resultBuffer.clear();
|
1096
|
+
}
|
1097
|
+
// if the stored proc returns an error, deserialize and throw it.
|
1098
|
+
// stored proc saves the error in tx_output before returning
|
1099
|
+
if (error) {
|
1100
|
+
throw (0, serialize_error_1.deserializeError)(error);
|
1101
|
+
}
|
1102
|
+
// if txn_snapshot is provided, the output needs to be buffered
|
1103
|
+
if (readOnly && txn_snapshot) {
|
1104
|
+
wfCtx.resultBuffer.set(funcId, {
|
1105
|
+
output,
|
1106
|
+
txn_snapshot,
|
1107
|
+
created_at: created_at ?? Date.now(),
|
1108
|
+
});
|
1109
|
+
}
|
1110
|
+
if (!readOnly) {
|
1111
|
+
wfCtx.resultBuffer.clear();
|
1112
|
+
}
|
1113
|
+
if (txn_id) {
|
1114
|
+
span.setAttribute("pg_txn_id", txn_id);
|
1115
|
+
}
|
1116
|
+
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
1117
|
+
return output;
|
1118
|
+
}
|
1119
|
+
async #invokeStoredProc(proc, args) {
|
1120
|
+
const client = await this.procedurePool.connect();
|
1121
|
+
const log = (msg) => this.#logNotice(msg);
|
1122
|
+
const procClassName = this.getProcedureClassName(proc);
|
1123
|
+
const plainProcName = `${procClassName}_${proc.name}_p`;
|
1124
|
+
const procName = this.config.appVersion
|
1125
|
+
? `v${this.config.appVersion}_${plainProcName}`
|
1126
|
+
: plainProcName;
|
1127
|
+
const sql = `CALL "${procName}"(${args.map((_v, i) => `$${i + 1}`).join()});`;
|
1128
|
+
try {
|
1129
|
+
client.on('notice', log);
|
1130
|
+
return await client.query(sql, args).then(value => value.rows);
|
1131
|
+
}
|
1132
|
+
finally {
|
1133
|
+
client.off('notice', log);
|
1134
|
+
client.release();
|
1135
|
+
}
|
1136
|
+
}
|
1137
|
+
async invokeStoredProcFunction(func, config) {
|
834
1138
|
const client = await this.procedurePool.connect();
|
835
1139
|
try {
|
836
1140
|
const readOnly = config.readOnly ?? false;
|
@@ -889,7 +1193,7 @@ class DBOSExecutor {
|
|
889
1193
|
}, wfCtx.span);
|
890
1194
|
const ctxt = new step_1.StepContextImpl(wfCtx, funcID, span, this.logger, commInfo.config, stepFn.name);
|
891
1195
|
await this.userDatabase.transaction(async (client) => {
|
892
|
-
await
|
1196
|
+
await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
|
893
1197
|
}, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
|
894
1198
|
wfCtx.resultBuffer.clear();
|
895
1199
|
// Check if this execution previously happened, returning its original result if it did.
|
@@ -900,6 +1204,9 @@ class DBOSExecutor {
|
|
900
1204
|
this.tracer.endSpan(ctxt.span);
|
901
1205
|
return check;
|
902
1206
|
}
|
1207
|
+
if (this.debugMode) {
|
1208
|
+
throw new error_1.DBOSDebuggerError(`Failed to find recorded output for workflow UUID: ${wfCtx.workflowUUID}`);
|
1209
|
+
}
|
903
1210
|
// Execute the step function. If it throws an exception, retry with exponential backoff.
|
904
1211
|
// After reaching the maximum number of retries, throw an DBOSError.
|
905
1212
|
let result = exports.dbosNull;
|
@@ -1045,6 +1352,9 @@ class DBOSExecutor {
|
|
1045
1352
|
* It runs to completion all pending workflows that were executing when the previous executor failed.
|
1046
1353
|
*/
|
1047
1354
|
async recoverPendingWorkflows(executorIDs = ["local"]) {
|
1355
|
+
if (this.debugMode) {
|
1356
|
+
throw new error_1.DBOSDebuggerError("Cannot recover pending workflows in debug mode.");
|
1357
|
+
}
|
1048
1358
|
const pendingWorkflows = [];
|
1049
1359
|
for (const execID of executorIDs) {
|
1050
1360
|
if (execID === "local" && process.env.DBOS__VMID) {
|
@@ -1196,13 +1506,16 @@ class DBOSExecutor {
|
|
1196
1506
|
* Periodically flush the workflow output buffer to the system database.
|
1197
1507
|
*/
|
1198
1508
|
async flushWorkflowBuffers() {
|
1199
|
-
if (this.initialized) {
|
1509
|
+
if (this.initialized && !this.debugMode) {
|
1200
1510
|
await this.flushWorkflowResultBuffer();
|
1201
1511
|
await this.systemDatabase.flushWorkflowSystemBuffers();
|
1202
1512
|
}
|
1203
1513
|
this.isFlushingBuffers = false;
|
1204
1514
|
}
|
1205
1515
|
async flushWorkflowResultBuffer() {
|
1516
|
+
if (this.debugMode) {
|
1517
|
+
throw new error_1.DBOSDebuggerError(`Cannot flush workflow result buffer in debug mode.`);
|
1518
|
+
}
|
1206
1519
|
const localBuffer = new Map(this.workflowResultBuffer);
|
1207
1520
|
this.workflowResultBuffer.clear();
|
1208
1521
|
const totalSize = localBuffer.size;
|