@dbos-inc/dbos-sdk 3.0.46-preview.g649d101bf0 → 3.0.50-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/src/context.d.ts +2 -4
- package/dist/src/context.d.ts.map +1 -1
- package/dist/src/context.js +2 -4
- package/dist/src/context.js.map +1 -1
- package/dist/src/datasource.d.ts.map +1 -1
- package/dist/src/datasource.js +18 -14
- package/dist/src/datasource.js.map +1 -1
- package/dist/src/dbos-executor.d.ts +5 -14
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +175 -129
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos-runtime/config.d.ts.map +1 -1
- package/dist/src/dbos-runtime/config.js +4 -2
- package/dist/src/dbos-runtime/config.js.map +1 -1
- package/dist/src/dbos-runtime/workflow_management.d.ts +2 -2
- package/dist/src/dbos-runtime/workflow_management.d.ts.map +1 -1
- package/dist/src/dbos-runtime/workflow_management.js +5 -3
- package/dist/src/dbos-runtime/workflow_management.js.map +1 -1
- package/dist/src/dbos.d.ts +0 -9
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +50 -133
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/httpServer/middleware.d.ts +0 -8
- package/dist/src/httpServer/middleware.d.ts.map +1 -1
- package/dist/src/httpServer/middleware.js +1 -153
- package/dist/src/httpServer/middleware.js.map +1 -1
- package/dist/src/httpServer/server.d.ts.map +1 -1
- package/dist/src/httpServer/server.js +41 -38
- package/dist/src/httpServer/server.js.map +1 -1
- package/dist/src/telemetry/traces.d.ts +2 -0
- package/dist/src/telemetry/traces.d.ts.map +1 -1
- package/dist/src/telemetry/traces.js +22 -1
- package/dist/src/telemetry/traces.js.map +1 -1
- package/dist/src/user_database.d.ts +1 -1
- package/dist/src/user_database.d.ts.map +1 -1
- package/dist/src/user_database.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -5
@@ -72,30 +72,30 @@ class DBOSExecutor {
|
|
72
72
|
config;
|
73
73
|
initialized;
|
74
74
|
// User Database
|
75
|
-
userDatabase =
|
75
|
+
#userDatabase = undefined;
|
76
|
+
#procedurePool = undefined;
|
76
77
|
// System Database
|
77
78
|
systemDatabase;
|
78
|
-
procedurePool;
|
79
79
|
// Temporary workflows are created by calling transaction/send/recv directly from the executor class
|
80
|
-
static tempWorkflowName = 'temp_workflow';
|
80
|
+
static #tempWorkflowName = 'temp_workflow';
|
81
81
|
telemetryCollector;
|
82
82
|
static defaultNotificationTimeoutSec = 60;
|
83
|
-
debugMode;
|
83
|
+
#debugMode;
|
84
84
|
static systemDBSchemaName = 'dbos';
|
85
85
|
logger;
|
86
86
|
ctxLogger;
|
87
87
|
tracer;
|
88
88
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
89
|
-
typeormEntities = [];
|
90
|
-
drizzleEntities = {};
|
91
|
-
scheduler = new scheduler_1.ScheduledReceiver();
|
92
|
-
wfqEnded = undefined;
|
89
|
+
#typeormEntities = [];
|
90
|
+
#drizzleEntities = {};
|
91
|
+
#scheduler = new scheduler_1.ScheduledReceiver();
|
92
|
+
#wfqEnded = undefined;
|
93
93
|
executorID = utils_1.globalParams.executorID;
|
94
94
|
static globalInstance = undefined;
|
95
95
|
/* WORKFLOW EXECUTOR LIFE CYCLE MANAGEMENT */
|
96
96
|
constructor(config, { systemDatabase, debugMode } = {}) {
|
97
97
|
this.config = config;
|
98
|
-
this
|
98
|
+
this.#debugMode = debugMode ?? false;
|
99
99
|
if (config.telemetry.OTLPExporter) {
|
100
100
|
const OTLPExporter = new exporters_1.TelemetryExporter(config.telemetry.OTLPExporter);
|
101
101
|
this.telemetryCollector = new collector_1.TelemetryCollector(OTLPExporter);
|
@@ -105,12 +105,12 @@ class DBOSExecutor {
|
|
105
105
|
this.telemetryCollector = new collector_1.TelemetryCollector();
|
106
106
|
}
|
107
107
|
this.logger = new logs_1.GlobalLogger(this.telemetryCollector, this.config.telemetry.logs);
|
108
|
-
this.ctxLogger = new logs_1.DBOSContextualLogger(this.logger, () =>
|
108
|
+
this.ctxLogger = new logs_1.DBOSContextualLogger(this.logger, () => api_1.trace.getActiveSpan());
|
109
109
|
this.tracer = new traces_1.Tracer(this.telemetryCollector);
|
110
|
-
if (this
|
110
|
+
if (this.#debugMode) {
|
111
111
|
this.logger.info('Running in debug mode!');
|
112
112
|
}
|
113
|
-
this
|
113
|
+
this.#procedurePool = this.config.userDbClient ? new pg_1.Pool((0, utils_2.getClientConfig)(this.config.databaseUrl)) : undefined;
|
114
114
|
if (systemDatabase) {
|
115
115
|
this.logger.debug('Using provided system database'); // XXX print the name or something
|
116
116
|
this.systemDatabase = systemDatabase;
|
@@ -122,7 +122,7 @@ class DBOSExecutor {
|
|
122
122
|
this.initialized = false;
|
123
123
|
DBOSExecutor.globalInstance = this;
|
124
124
|
}
|
125
|
-
configureDbClient() {
|
125
|
+
#configureDbClient() {
|
126
126
|
const userDbClient = this.config.userDbClient;
|
127
127
|
const userDBConfig = (0, utils_2.getClientConfig)(this.config.databaseUrl);
|
128
128
|
userDBConfig.max = this.config.userDbPoolSize ?? 20;
|
@@ -130,7 +130,7 @@ class DBOSExecutor {
|
|
130
130
|
// TODO: make Prisma work with debugger proxy.
|
131
131
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports
|
132
132
|
const { PrismaClient } = require(node_path_1.default.join(process.cwd(), 'node_modules', '@prisma', 'client')); // Find the prisma client in the node_modules of the current project
|
133
|
-
this
|
133
|
+
this.#userDatabase = new user_database_1.PrismaUserDatabase(
|
134
134
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
135
135
|
new PrismaClient({
|
136
136
|
datasources: {
|
@@ -145,13 +145,13 @@ class DBOSExecutor {
|
|
145
145
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports
|
146
146
|
const DataSourceExports = require('typeorm');
|
147
147
|
try {
|
148
|
-
this
|
148
|
+
this.#userDatabase = new user_database_1.TypeORMDatabase(
|
149
149
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
150
150
|
new DataSourceExports.DataSource({
|
151
151
|
type: 'postgres',
|
152
152
|
url: userDBConfig.connectionString,
|
153
153
|
connectTimeoutMS: userDBConfig.connectionTimeoutMillis,
|
154
|
-
entities: this
|
154
|
+
entities: this.#typeormEntities,
|
155
155
|
poolSize: userDBConfig.max,
|
156
156
|
}));
|
157
157
|
}
|
@@ -170,7 +170,7 @@ class DBOSExecutor {
|
|
170
170
|
max: userDBConfig.max,
|
171
171
|
},
|
172
172
|
};
|
173
|
-
this
|
173
|
+
this.#userDatabase = new user_database_1.KnexUserDatabase((0, knex_1.default)(knexConfig));
|
174
174
|
this.logger.debug('Loaded Knex user database');
|
175
175
|
}
|
176
176
|
else if (userDbClient === user_database_1.UserDatabaseName.DRIZZLE) {
|
@@ -178,13 +178,13 @@ class DBOSExecutor {
|
|
178
178
|
const DrizzleExports = require('drizzle-orm/node-postgres');
|
179
179
|
const drizzlePool = new pg_1.Pool(userDBConfig);
|
180
180
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
181
|
-
const drizzle = DrizzleExports.drizzle(drizzlePool, { schema: this
|
181
|
+
const drizzle = DrizzleExports.drizzle(drizzlePool, { schema: this.#drizzleEntities });
|
182
182
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
183
|
-
this
|
183
|
+
this.#userDatabase = new user_database_1.DrizzleUserDatabase(drizzlePool, drizzle);
|
184
184
|
this.logger.debug('Loaded Drizzle user database');
|
185
185
|
}
|
186
186
|
else {
|
187
|
-
this
|
187
|
+
this.#userDatabase = new user_database_1.PGNodeUserDatabase(userDBConfig);
|
188
188
|
this.logger.debug('Loaded Postgres user database');
|
189
189
|
}
|
190
190
|
}
|
@@ -208,29 +208,31 @@ class DBOSExecutor {
|
|
208
208
|
* With TSORM, we take an array of entities (Function[]) and add them to this.entities:
|
209
209
|
*/
|
210
210
|
if (Array.isArray(reg.ormEntities)) {
|
211
|
-
this
|
211
|
+
this.#typeormEntities = this.#typeormEntities.concat(reg.ormEntities);
|
212
212
|
length = reg.ormEntities.length;
|
213
213
|
}
|
214
214
|
else {
|
215
215
|
/**
|
216
216
|
* With Drizzle, we need to take an object of entities, since the object keys are used to access the entities from ctx.client.query:
|
217
217
|
*/
|
218
|
-
this
|
218
|
+
this.#drizzleEntities = { ...this.#drizzleEntities, ...reg.ormEntities };
|
219
219
|
length = Object.keys(reg.ormEntities).length;
|
220
220
|
}
|
221
221
|
this.logger.debug(`Loaded ${length} ORM entities`);
|
222
222
|
}
|
223
|
-
if (
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
223
|
+
if (this.config.userDbClient) {
|
224
|
+
if (!this.#debugMode) {
|
225
|
+
await (0, user_database_1.createDBIfDoesNotExist)(this.config.databaseUrl, this.logger);
|
226
|
+
}
|
227
|
+
this.#configureDbClient();
|
228
|
+
if (!this.#userDatabase) {
|
229
|
+
this.logger.error('No user database configured!');
|
230
|
+
throw new error_1.DBOSInitializationError('No user database configured!');
|
231
|
+
}
|
232
|
+
// Debug mode doesn't need to initialize the DBs. Everything should appear to be read-only.
|
233
|
+
await this.#userDatabase.init(this.#debugMode);
|
230
234
|
}
|
231
|
-
|
232
|
-
await this.userDatabase.init(this.debugMode);
|
233
|
-
if (!this.debugMode) {
|
235
|
+
if (!this.#debugMode) {
|
234
236
|
await this.systemDatabase.init();
|
235
237
|
}
|
236
238
|
}
|
@@ -253,7 +255,7 @@ class DBOSExecutor {
|
|
253
255
|
}
|
254
256
|
this.initialized = true;
|
255
257
|
// Only execute init code if under non-debug mode
|
256
|
-
if (!this
|
258
|
+
if (!this.#debugMode) {
|
257
259
|
for (const cls of classnames) {
|
258
260
|
// Init its configurations
|
259
261
|
const creg = (0, decorators_1.getClassRegistrationByName)(cls);
|
@@ -306,10 +308,8 @@ class DBOSExecutor {
|
|
306
308
|
try {
|
307
309
|
await this.systemDatabase.awaitRunningWorkflows();
|
308
310
|
await this.systemDatabase.destroy();
|
309
|
-
|
310
|
-
|
311
|
-
}
|
312
|
-
await this.procedurePool.end();
|
311
|
+
await this.#userDatabase?.destroy();
|
312
|
+
await this.#procedurePool?.end();
|
313
313
|
await this.logger.destroy();
|
314
314
|
if (DBOSExecutor.globalInstance === this) {
|
315
315
|
DBOSExecutor.globalInstance = undefined;
|
@@ -321,6 +321,24 @@ class DBOSExecutor {
|
|
321
321
|
throw err;
|
322
322
|
}
|
323
323
|
}
|
324
|
+
async createUserSchema() {
|
325
|
+
if (!this.#userDatabase) {
|
326
|
+
throw new Error('User database not enabled.');
|
327
|
+
}
|
328
|
+
await this.#userDatabase.createSchema();
|
329
|
+
}
|
330
|
+
async dropUserSchema() {
|
331
|
+
if (!this.#userDatabase) {
|
332
|
+
throw new Error('User database not enabled.');
|
333
|
+
}
|
334
|
+
await this.#userDatabase.dropSchema();
|
335
|
+
}
|
336
|
+
async queryUserDbFunction(queryFunction, ...params) {
|
337
|
+
if (!this.#userDatabase) {
|
338
|
+
throw new Error('User database not enabled.');
|
339
|
+
}
|
340
|
+
return await this.#userDatabase.queryFunction(queryFunction, ...params);
|
341
|
+
}
|
324
342
|
// This could return WF, or the function underlying a temp wf
|
325
343
|
#getFunctionInfoFromWFStatus(wf) {
|
326
344
|
const methReg = (0, decorators_1.getFunctionRegistrationByName)(wf.workflowClassName, wf.workflowName);
|
@@ -364,7 +382,7 @@ class DBOSExecutor {
|
|
364
382
|
const pctx = (0, context_1.getCurrentContextStore)();
|
365
383
|
let wConfig = {};
|
366
384
|
const wInfo = (0, decorators_1.getFunctionRegistration)(wf);
|
367
|
-
if (wf.name !== DBOSExecutor
|
385
|
+
if (wf.name !== DBOSExecutor.#tempWorkflowName) {
|
368
386
|
if (!wInfo || !wInfo.workflowConfig) {
|
369
387
|
throw new error_1.DBOSNotRegisteredError(wf.name);
|
370
388
|
}
|
@@ -382,8 +400,8 @@ class DBOSExecutor {
|
|
382
400
|
authenticatedUser: pctx?.authenticatedUser ?? '',
|
383
401
|
authenticatedRoles: pctx?.authenticatedRoles ?? [],
|
384
402
|
assumedRole: pctx?.assumedRole ?? '',
|
385
|
-
}
|
386
|
-
const isTempWorkflow = DBOSExecutor
|
403
|
+
});
|
404
|
+
const isTempWorkflow = DBOSExecutor.#tempWorkflowName === wfname;
|
387
405
|
const internalStatus = {
|
388
406
|
workflowUUID: workflowID,
|
389
407
|
status: params.queueName !== undefined ? workflow_1.StatusString.ENQUEUED : workflow_1.StatusString.PENDING,
|
@@ -408,14 +426,14 @@ class DBOSExecutor {
|
|
408
426
|
priority: priority ?? 0,
|
409
427
|
};
|
410
428
|
if (isTempWorkflow) {
|
411
|
-
internalStatus.workflowName = `${DBOSExecutor
|
429
|
+
internalStatus.workflowName = `${DBOSExecutor.#tempWorkflowName}-${params.tempWfType}-${params.tempWfName}`;
|
412
430
|
internalStatus.workflowClassName = params.tempWfClass ?? '';
|
413
431
|
}
|
414
432
|
let status = undefined;
|
415
433
|
let $deadlineEpochMS = undefined;
|
416
434
|
// Synchronously set the workflow's status to PENDING and record workflow inputs.
|
417
435
|
// We have to do it for all types of workflows because operation_outputs table has a foreign key constraint on workflow status table.
|
418
|
-
if (this
|
436
|
+
if (this.#debugMode) {
|
419
437
|
const wfStatus = await this.systemDatabase.getWorkflowStatus(workflowID);
|
420
438
|
if (!wfStatus) {
|
421
439
|
throw new error_1.DBOSDebuggerError(`Failed to find inputs for workflow UUID ${workflowID}`);
|
@@ -471,7 +489,7 @@ class DBOSExecutor {
|
|
471
489
|
e.dbos_already_logged = true;
|
472
490
|
internalStatus.error = utils_1.DBOSJSON.stringify((0, serialize_error_1.serializeError)(e));
|
473
491
|
internalStatus.status = workflow_1.StatusString.ERROR;
|
474
|
-
if (!exec
|
492
|
+
if (!exec.#debugMode) {
|
475
493
|
await exec.systemDatabase.recordWorkflowError(workflowID, internalStatus);
|
476
494
|
}
|
477
495
|
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: e.message });
|
@@ -480,23 +498,24 @@ class DBOSExecutor {
|
|
480
498
|
let result;
|
481
499
|
// Execute the workflow.
|
482
500
|
try {
|
483
|
-
const callResult = await (
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
501
|
+
const callResult = await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
|
502
|
+
return await (0, context_1.runWithParentContext)(pctx, {
|
503
|
+
presetID,
|
504
|
+
timeoutMS,
|
505
|
+
deadlineEpochMS,
|
506
|
+
workflowId: workflowID,
|
507
|
+
logger: this.ctxLogger,
|
508
|
+
}, () => {
|
509
|
+
const callPromise = wf.call(params.configuredInstance, ...args);
|
510
|
+
if ($deadlineEpochMS === undefined) {
|
511
|
+
return callPromise;
|
512
|
+
}
|
513
|
+
else {
|
514
|
+
return callPromiseWithTimeout(callPromise, $deadlineEpochMS, this.systemDatabase);
|
515
|
+
}
|
516
|
+
});
|
498
517
|
});
|
499
|
-
if (this
|
518
|
+
if (this.#debugMode) {
|
500
519
|
const recordedResult = DBOSExecutor.reviveResultOrError((await this.systemDatabase.awaitWorkflowResult(workflowID)));
|
501
520
|
if (!resultsMatch(recordedResult, callResult)) {
|
502
521
|
this.logger.error(`Detect different output for the workflow UUID ${workflowID}!\n Received: ${utils_1.DBOSJSON.stringify(callResult)}\n Original: ${utils_1.DBOSJSON.stringify(recordedResult)}`);
|
@@ -514,7 +533,7 @@ class DBOSExecutor {
|
|
514
533
|
}
|
515
534
|
internalStatus.output = utils_1.DBOSJSON.stringify(result);
|
516
535
|
internalStatus.status = workflow_1.StatusString.SUCCESS;
|
517
|
-
if (!this
|
536
|
+
if (!this.#debugMode) {
|
518
537
|
await this.systemDatabase.recordWorkflowOutput(workflowID, internalStatus);
|
519
538
|
}
|
520
539
|
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
@@ -550,7 +569,7 @@ class DBOSExecutor {
|
|
550
569
|
}
|
551
570
|
return result;
|
552
571
|
};
|
553
|
-
if (this
|
572
|
+
if (this.#debugMode ||
|
554
573
|
(status !== 'SUCCESS' && status !== 'ERROR' && (params.queueName === undefined || params.executeWorkflow))) {
|
555
574
|
const workflowPromise = runWorkflow();
|
556
575
|
this.systemDatabase.registerRunningWorkflow(workflowID, workflowPromise);
|
@@ -616,7 +635,7 @@ class DBOSExecutor {
|
|
616
635
|
* Write a operation's output to the database.
|
617
636
|
*/
|
618
637
|
async #recordOutput(query, workflowUUID, funcID, txnSnapshot, output, isKeyConflict, function_name) {
|
619
|
-
if (this
|
638
|
+
if (this.#debugMode) {
|
620
639
|
throw new error_1.DBOSDebuggerError('Cannot record output in debug mode.');
|
621
640
|
}
|
622
641
|
try {
|
@@ -638,7 +657,7 @@ class DBOSExecutor {
|
|
638
657
|
* Record an error in an operation to the database.
|
639
658
|
*/
|
640
659
|
async #recordError(query, workflowUUID, funcID, txnSnapshot, err, isKeyConflict, function_name) {
|
641
|
-
if (this
|
660
|
+
if (this.#debugMode) {
|
642
661
|
throw new error_1.DBOSDebuggerError('Cannot record error in debug mode.');
|
643
662
|
}
|
644
663
|
try {
|
@@ -656,12 +675,17 @@ class DBOSExecutor {
|
|
656
675
|
}
|
657
676
|
}
|
658
677
|
async getTransactions(workflowUUID) {
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
678
|
+
if (this.#userDatabase) {
|
679
|
+
const rows = await this.#userDatabase.query(`SELECT function_id, function_name, output, error FROM ${DBOSExecutor.systemDBSchemaName}.transaction_outputs WHERE workflow_uuid=$1`, workflowUUID);
|
680
|
+
for (const row of rows) {
|
681
|
+
row.output = row.output !== null ? utils_1.DBOSJSON.parse(row.output) : null;
|
682
|
+
row.error = row.error !== null ? (0, serialize_error_1.deserializeError)(utils_1.DBOSJSON.parse(row.error)) : null;
|
683
|
+
}
|
684
|
+
return rows;
|
685
|
+
}
|
686
|
+
else {
|
687
|
+
return [];
|
663
688
|
}
|
664
|
-
return rows;
|
665
689
|
}
|
666
690
|
async runTransactionTempWF(txn, params, ...args) {
|
667
691
|
return await (await this.startTransactionTempWF(txn, params, undefined, undefined, ...args)).getResult();
|
@@ -679,6 +703,10 @@ class DBOSExecutor {
|
|
679
703
|
}, callerWFID, callerFunctionID, ...args);
|
680
704
|
}
|
681
705
|
async callTransactionFunction(txn, clsinst, ...args) {
|
706
|
+
const userDB = this.#userDatabase;
|
707
|
+
if (!userDB) {
|
708
|
+
throw new Error('No user database configured for transactions.');
|
709
|
+
}
|
682
710
|
const txnReg = (0, decorators_1.getFunctionRegistration)(txn);
|
683
711
|
if (!txnReg || !txnReg.txnConfig) {
|
684
712
|
throw new error_1.DBOSNotRegisteredError(txn.name);
|
@@ -698,7 +726,7 @@ class DBOSExecutor {
|
|
698
726
|
assumedRole: pctx.assumedRole ?? '',
|
699
727
|
authenticatedRoles: pctx.authenticatedRoles ?? [],
|
700
728
|
isolationLevel: txnReg.txnConfig.isolationLevel,
|
701
|
-
}
|
729
|
+
});
|
702
730
|
while (true) {
|
703
731
|
await this.systemDatabase.checkIfCanceled(wfid);
|
704
732
|
let txn_snapshot = 'invalid';
|
@@ -707,7 +735,7 @@ class DBOSExecutor {
|
|
707
735
|
// If the UUID is preset, it is possible this execution previously happened. Check, and return its original result if it did.
|
708
736
|
// 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.
|
709
737
|
let prevResult = exports.dbosNull;
|
710
|
-
const queryFunc = (sql, args) =>
|
738
|
+
const queryFunc = (sql, args) => userDB.queryWithClient(client, sql, ...args);
|
711
739
|
if (pctx.presetID) {
|
712
740
|
const executionResult = await this.#checkExecution(queryFunc, wfid, funcId, txn.name);
|
713
741
|
prevResult = executionResult.result;
|
@@ -728,32 +756,33 @@ class DBOSExecutor {
|
|
728
756
|
// Collect snapshot information for read-only transactions and non-preset UUID transactions, if not already collected above
|
729
757
|
txn_snapshot = await DBOSExecutor.#retrieveSnapshot(queryFunc);
|
730
758
|
}
|
731
|
-
if (this
|
759
|
+
if (this.#debugMode && prevResult === exports.dbosNull) {
|
732
760
|
throw new error_1.DBOSDebuggerError(`Failed to find the recorded output for the transaction: workflow UUID ${wfid}, step number ${funcId}`);
|
733
761
|
}
|
734
762
|
// Execute the user's transaction.
|
735
763
|
const ctxlog = this.ctxLogger;
|
736
764
|
const result = await (async function () {
|
737
765
|
try {
|
738
|
-
return await (
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
766
|
+
return await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
|
767
|
+
return await (0, context_1.runWithParentContext)(pctx, {
|
768
|
+
authenticatedRoles: pctx?.authenticatedRoles,
|
769
|
+
authenticatedUser: pctx?.authenticatedUser,
|
770
|
+
workflowId: wfid,
|
771
|
+
curTxFunctionId: funcId,
|
772
|
+
parentCtx: pctx,
|
773
|
+
sqlClient: client,
|
774
|
+
logger: ctxlog,
|
775
|
+
}, async () => {
|
776
|
+
const tf = txn;
|
777
|
+
return await tf.call(clsinst, ...args);
|
778
|
+
});
|
750
779
|
});
|
751
780
|
}
|
752
781
|
catch (e) {
|
753
782
|
return e instanceof Error ? e : new Error(`${e}`);
|
754
783
|
}
|
755
784
|
})();
|
756
|
-
if (this
|
785
|
+
if (this.#debugMode) {
|
757
786
|
if (prevResult instanceof Error) {
|
758
787
|
throw prevResult;
|
759
788
|
}
|
@@ -770,11 +799,11 @@ class DBOSExecutor {
|
|
770
799
|
// Record the execution, commit, and return.
|
771
800
|
try {
|
772
801
|
// Synchronously record the output of write transactions and obtain the transaction ID.
|
773
|
-
const pg_txn_id = await this.#recordOutput(queryFunc, wfid, funcId, txn_snapshot, result, (error) =>
|
802
|
+
const pg_txn_id = await this.#recordOutput(queryFunc, wfid, funcId, txn_snapshot, result, (error) => userDB.isKeyConflictError(error), txn.name);
|
774
803
|
span.setAttribute('pg_txn_id', pg_txn_id);
|
775
804
|
}
|
776
805
|
catch (error) {
|
777
|
-
if (
|
806
|
+
if (userDB.isFailedSqlTransactionError(error)) {
|
778
807
|
this.logger.error(`Postgres aborted the ${txn.name} @DBOS.transaction of Workflow ${wfid}, but the function did not raise an exception. Please ensure that the @DBOS.transaction method raises an exception if the database transaction is aborted.`);
|
779
808
|
throw new error_1.DBOSFailedSqlTransactionError(wfid, txn.name);
|
780
809
|
}
|
@@ -785,15 +814,15 @@ class DBOSExecutor {
|
|
785
814
|
return result;
|
786
815
|
};
|
787
816
|
try {
|
788
|
-
const result = await
|
817
|
+
const result = await userDB.transaction(wrappedTransaction, txnReg.txnConfig);
|
789
818
|
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
790
819
|
this.tracer.endSpan(span);
|
791
820
|
return result;
|
792
821
|
}
|
793
822
|
catch (err) {
|
794
823
|
const e = err;
|
795
|
-
if (!prevResultFound && !this
|
796
|
-
if (
|
824
|
+
if (!prevResultFound && !this.#debugMode && !(e instanceof error_1.DBOSUnexpectedStepError)) {
|
825
|
+
if (userDB.isRetriableTransactionError(err)) {
|
797
826
|
// serialization_failure in PostgreSQL
|
798
827
|
span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
|
799
828
|
// Retry serialization failures.
|
@@ -804,9 +833,9 @@ class DBOSExecutor {
|
|
804
833
|
}
|
805
834
|
// Record and throw other errors.
|
806
835
|
const e = err;
|
807
|
-
await
|
808
|
-
const func = (sql, args) =>
|
809
|
-
await this.#recordError(func, wfid, funcId, txn_snapshot, e, (error) =>
|
836
|
+
await userDB.transaction(async (client) => {
|
837
|
+
const func = (sql, args) => userDB.queryWithClient(client, sql, ...args);
|
838
|
+
await this.#recordError(func, wfid, funcId, txn_snapshot, e, (error) => userDB.isKeyConflictError(error), txn.name);
|
810
839
|
}, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
|
811
840
|
}
|
812
841
|
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: e.message });
|
@@ -836,7 +865,7 @@ class DBOSExecutor {
|
|
836
865
|
const pctx = (0, context_1.getCurrentContextStore)();
|
837
866
|
const wfid = pctx.workflowId;
|
838
867
|
await this.systemDatabase.checkIfCanceled(wfid);
|
839
|
-
const executeLocally = this
|
868
|
+
const executeLocally = this.#debugMode || (procConfig.executeLocally ?? false);
|
840
869
|
const funcId = (0, context_1.functionIDGetIncrement)();
|
841
870
|
const span = this.tracer.startSpan(proc.name, {
|
842
871
|
operationUUID: wfid,
|
@@ -847,7 +876,7 @@ class DBOSExecutor {
|
|
847
876
|
authenticatedRoles: pctx.authenticatedRoles ?? [],
|
848
877
|
isolationLevel: procInfo.procConfig.isolationLevel,
|
849
878
|
executeLocally,
|
850
|
-
}
|
879
|
+
});
|
851
880
|
try {
|
852
881
|
const result = executeLocally
|
853
882
|
? await this.#callProcedureFunctionLocal(proc, args, span, procInfo, funcId)
|
@@ -865,6 +894,11 @@ class DBOSExecutor {
|
|
865
894
|
}
|
866
895
|
}
|
867
896
|
async #callProcedureFunctionLocal(proc, args, span, procInfo, funcId) {
|
897
|
+
const procPool = this.#procedurePool;
|
898
|
+
const userDB = this.#userDatabase;
|
899
|
+
if (!procPool || !userDB) {
|
900
|
+
throw new Error('User database not enabled.');
|
901
|
+
}
|
868
902
|
let retryWaitMillis = 1;
|
869
903
|
const backoffFactor = 1.5;
|
870
904
|
const maxRetryWaitMs = 2000; // Maximum wait 2 seconds.
|
@@ -875,7 +909,7 @@ class DBOSExecutor {
|
|
875
909
|
let txn_snapshot = 'invalid';
|
876
910
|
const wrappedProcedure = async (client) => {
|
877
911
|
let prevResult = exports.dbosNull;
|
878
|
-
const queryFunc = (sql, args) =>
|
912
|
+
const queryFunc = (sql, args) => procPool.query(sql, args).then((v) => v.rows);
|
879
913
|
if (pctx.presetID) {
|
880
914
|
const executionResult = await this.#checkExecution(queryFunc, wfid, funcId, proc.name);
|
881
915
|
prevResult = executionResult.result;
|
@@ -895,7 +929,7 @@ class DBOSExecutor {
|
|
895
929
|
// Collect snapshot information for read-only transactions and non-preset UUID transactions, if not already collected above
|
896
930
|
txn_snapshot = await DBOSExecutor.#retrieveSnapshot(queryFunc);
|
897
931
|
}
|
898
|
-
if (this
|
932
|
+
if (this.#debugMode && prevResult === exports.dbosNull) {
|
899
933
|
throw new error_1.DBOSDebuggerError(`Failed to find the recorded output for the procedure: workflow UUID ${wfid}, step number ${funcId}`);
|
900
934
|
}
|
901
935
|
// Execute the user's transaction.
|
@@ -908,23 +942,24 @@ class DBOSExecutor {
|
|
908
942
|
throw new error_1.DBOSInvalidWorkflowTransitionError();
|
909
943
|
if (!(0, context_1.isInWorkflowCtx)(pctx))
|
910
944
|
throw new error_1.DBOSInvalidWorkflowTransitionError();
|
911
|
-
return await (
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
945
|
+
return await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
|
946
|
+
return await (0, context_1.runWithParentContext)(pctx, {
|
947
|
+
curTxFunctionId: funcId,
|
948
|
+
parentCtx: pctx,
|
949
|
+
isInStoredProc: true,
|
950
|
+
sqlClient: client,
|
951
|
+
logger: ctxlog,
|
952
|
+
}, async () => {
|
953
|
+
const pf = proc;
|
954
|
+
return await pf(...args);
|
955
|
+
});
|
921
956
|
});
|
922
957
|
}
|
923
958
|
catch (e) {
|
924
959
|
return e instanceof Error ? e : new Error(`${e}`);
|
925
960
|
}
|
926
961
|
})();
|
927
|
-
if (this
|
962
|
+
if (this.#debugMode) {
|
928
963
|
if (prevResult instanceof Error) {
|
929
964
|
throw prevResult;
|
930
965
|
}
|
@@ -953,8 +988,8 @@ class DBOSExecutor {
|
|
953
988
|
return result;
|
954
989
|
}
|
955
990
|
catch (err) {
|
956
|
-
if (!this
|
957
|
-
if (
|
991
|
+
if (!this.#debugMode) {
|
992
|
+
if (userDB.isRetriableTransactionError(err)) {
|
958
993
|
// serialization_failure in PostgreSQL
|
959
994
|
span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
|
960
995
|
// Retry serialization failures.
|
@@ -969,9 +1004,9 @@ class DBOSExecutor {
|
|
969
1004
|
const func = (sql, args) => client.query(sql, args).then((v) => v.rows);
|
970
1005
|
await this.#recordError(func, wfid, funcId, txn_snapshot, e, user_database_1.pgNodeIsKeyConflictError, proc.name);
|
971
1006
|
}, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
|
972
|
-
await
|
973
|
-
const func = (sql, args) =>
|
974
|
-
await this.#recordError(func, wfid, funcId, txn_snapshot, e, (error) =>
|
1007
|
+
await userDB.transaction(async (client) => {
|
1008
|
+
const func = (sql, args) => userDB.queryWithClient(client, sql, ...args);
|
1009
|
+
await this.#recordError(func, wfid, funcId, txn_snapshot, e, (error) => userDB.isKeyConflictError(error), proc.name);
|
975
1010
|
}, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
|
976
1011
|
}
|
977
1012
|
throw err;
|
@@ -979,7 +1014,7 @@ class DBOSExecutor {
|
|
979
1014
|
}
|
980
1015
|
}
|
981
1016
|
async #callProcedureFunctionRemote(proc, args, span, config, funcId) {
|
982
|
-
if (this
|
1017
|
+
if (this.#debugMode) {
|
983
1018
|
throw new error_1.DBOSDebuggerError("Can't invoke stored procedure in debug mode.");
|
984
1019
|
}
|
985
1020
|
const pctx = (0, context_1.getCurrentContextStore)();
|
@@ -1013,7 +1048,10 @@ class DBOSExecutor {
|
|
1013
1048
|
return output;
|
1014
1049
|
}
|
1015
1050
|
async #invokeStoredProc(proc, args) {
|
1016
|
-
|
1051
|
+
if (!this.#procedurePool) {
|
1052
|
+
throw new Error('User Database not enabled.');
|
1053
|
+
}
|
1054
|
+
const client = await this.#procedurePool.connect();
|
1017
1055
|
const log = (msg) => this.#logNotice(msg);
|
1018
1056
|
const procname = (0, decorators_1.getRegisteredFunctionFullName)(proc);
|
1019
1057
|
const plainProcName = `${procname.className}_${procname.name}_p`;
|
@@ -1029,7 +1067,10 @@ class DBOSExecutor {
|
|
1029
1067
|
}
|
1030
1068
|
}
|
1031
1069
|
async invokeStoredProcFunction(func, config) {
|
1032
|
-
|
1070
|
+
if (!this.#procedurePool) {
|
1071
|
+
throw new Error('User Database not enabled.');
|
1072
|
+
}
|
1073
|
+
const client = await this.#procedurePool.connect();
|
1033
1074
|
try {
|
1034
1075
|
const readOnly = config.readOnly ?? false;
|
1035
1076
|
const isolationLevel = config.isolationLevel ?? transaction_1.IsolationLevel.Serializable;
|
@@ -1094,7 +1135,7 @@ class DBOSExecutor {
|
|
1094
1135
|
intervalSeconds: stepConfig.intervalSeconds,
|
1095
1136
|
maxAttempts: stepConfig.maxAttempts,
|
1096
1137
|
backoffRate: stepConfig.backoffRate,
|
1097
|
-
}
|
1138
|
+
});
|
1098
1139
|
// Check if this execution previously happened, returning its original result if it did.
|
1099
1140
|
const checkr = await this.systemDatabase.getOperationResultAndThrowIfCancelled(wfid, funcID);
|
1100
1141
|
if (checkr) {
|
@@ -1107,7 +1148,7 @@ class DBOSExecutor {
|
|
1107
1148
|
this.tracer.endSpan(span);
|
1108
1149
|
return check;
|
1109
1150
|
}
|
1110
|
-
if (this
|
1151
|
+
if (this.#debugMode) {
|
1111
1152
|
throw new error_1.DBOSDebuggerError(`Failed to find the recorded output for the step: workflow UUID: ${wfid}, step number: ${funcID}`);
|
1112
1153
|
}
|
1113
1154
|
const maxAttempts = stepConfig.maxAttempts ?? 3;
|
@@ -1126,7 +1167,7 @@ class DBOSExecutor {
|
|
1126
1167
|
try {
|
1127
1168
|
await this.systemDatabase.checkIfCanceled(wfid);
|
1128
1169
|
let cresult;
|
1129
|
-
await (0, context_1.runInStepContext)(lctx, funcID,
|
1170
|
+
await (0, context_1.runInStepContext)(lctx, funcID, maxAttempts, attemptNum, async () => {
|
1130
1171
|
const sf = stepFn;
|
1131
1172
|
cresult = await sf.call(clsInst, ...args);
|
1132
1173
|
});
|
@@ -1150,9 +1191,11 @@ class DBOSExecutor {
|
|
1150
1191
|
else {
|
1151
1192
|
try {
|
1152
1193
|
let cresult;
|
1153
|
-
await (
|
1154
|
-
|
1155
|
-
|
1194
|
+
await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
|
1195
|
+
await (0, context_1.runInStepContext)(lctx, funcID, maxAttempts, undefined, async () => {
|
1196
|
+
const sf = stepFn;
|
1197
|
+
cresult = await sf.call(clsInst, ...args);
|
1198
|
+
});
|
1156
1199
|
});
|
1157
1200
|
result = cresult;
|
1158
1201
|
}
|
@@ -1207,7 +1250,7 @@ class DBOSExecutor {
|
|
1207
1250
|
*/
|
1208
1251
|
forkWorkflow(workflowID, startStep, options = {}) {
|
1209
1252
|
const newWorkflowID = options.newWorkflowID ?? (0, context_1.getNextWFID)(undefined);
|
1210
|
-
return (0, workflow_management_1.forkWorkflow)(this.systemDatabase, this
|
1253
|
+
return (0, workflow_management_1.forkWorkflow)(this.systemDatabase, this.#userDatabase, workflowID, startStep, { ...options, newWorkflowID });
|
1211
1254
|
}
|
1212
1255
|
/**
|
1213
1256
|
* Retrieve a handle for a workflow UUID.
|
@@ -1251,14 +1294,17 @@ class DBOSExecutor {
|
|
1251
1294
|
return (0, workflow_management_1.listQueuedWorkflows)(this.systemDatabase, input);
|
1252
1295
|
}
|
1253
1296
|
async listWorkflowSteps(workflowID) {
|
1254
|
-
return (0, workflow_management_1.listWorkflowSteps)(this.systemDatabase, this
|
1297
|
+
return (0, workflow_management_1.listWorkflowSteps)(this.systemDatabase, this.#userDatabase, workflowID);
|
1255
1298
|
}
|
1256
1299
|
async queryUserDB(sql, params) {
|
1300
|
+
if (!this.#userDatabase) {
|
1301
|
+
throw new Error('User database not enabled.');
|
1302
|
+
}
|
1257
1303
|
if (params !== undefined) {
|
1258
|
-
return await this
|
1304
|
+
return await this.#userDatabase.query(sql, ...params);
|
1259
1305
|
}
|
1260
1306
|
else {
|
1261
|
-
return await this
|
1307
|
+
return await this.#userDatabase.query(sql);
|
1262
1308
|
}
|
1263
1309
|
}
|
1264
1310
|
/* INTERNAL HELPERS */
|
@@ -1267,7 +1313,7 @@ class DBOSExecutor {
|
|
1267
1313
|
* It runs to completion all pending workflows that were executing when the previous executor failed.
|
1268
1314
|
*/
|
1269
1315
|
async recoverPendingWorkflows(executorIDs = ['local']) {
|
1270
|
-
if (this
|
1316
|
+
if (this.#debugMode) {
|
1271
1317
|
throw new error_1.DBOSDebuggerError('Cannot recover pending workflows in debug mode.');
|
1272
1318
|
}
|
1273
1319
|
const handlerArray = [];
|
@@ -1305,7 +1351,7 @@ class DBOSExecutor {
|
|
1305
1351
|
return handlerArray;
|
1306
1352
|
}
|
1307
1353
|
async initEventReceivers() {
|
1308
|
-
this
|
1354
|
+
this.#wfqEnded = wfqueue_1.wfQueueRunner.dispatchLoop(this);
|
1309
1355
|
for (const lcl of (0, decorators_1.getLifecycleListeners)()) {
|
1310
1356
|
await lcl.initialize?.();
|
1311
1357
|
}
|
@@ -1325,7 +1371,7 @@ class DBOSExecutor {
|
|
1325
1371
|
if (stopQueueThread) {
|
1326
1372
|
try {
|
1327
1373
|
wfqueue_1.wfQueueRunner.stop();
|
1328
|
-
await this
|
1374
|
+
await this.#wfqEnded;
|
1329
1375
|
}
|
1330
1376
|
catch (err) {
|
1331
1377
|
const e = err;
|
@@ -1362,7 +1408,7 @@ class DBOSExecutor {
|
|
1362
1408
|
// Should be temporary workflows. Parse the name of the workflow.
|
1363
1409
|
const wfName = wfStatus.workflowName;
|
1364
1410
|
const nameArr = wfName.split('-');
|
1365
|
-
if (!nameArr[0].startsWith(DBOSExecutor
|
1411
|
+
if (!nameArr[0].startsWith(DBOSExecutor.#tempWorkflowName)) {
|
1366
1412
|
throw new error_1.DBOSError(`Cannot find workflow function for a non-temporary workflow, ID ${workflowID}, class '${wfStatus.workflowClassName}', function '${wfName}'; did you change your code?`);
|
1367
1413
|
}
|
1368
1414
|
if (nameArr[1] === exports.TempWorkflowType.transaction) {
|