@dbos-inc/dbos-sdk 4.26.8-preview → 4.26.10
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/client.d.ts +50 -7
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +70 -43
- package/dist/src/client.js.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +1 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/dbos-executor.d.ts +15 -0
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +1 -1
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos.d.ts +10 -1
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +12 -3
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/system_database.d.ts +11 -4
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +191 -106
- package/dist/src/system_database.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.SystemDatabase = exports.ensureSystemDatabase = exports.grantDbosSchemaPermissions = exports.getDbosSchemaPermissionsSql = exports.DEFAULT_RENAME_BATCH_SIZE = exports.DEFAULT_NOTIFICATION_COALESCE_MS = exports.DBOS_STREAMS_CHANNEL = exports.DBOS_WORKFLOW_EVENTS_CHANNEL = exports.DBOS_NOTIFICATIONS_CHANNEL = exports.DBOS_STREAM_CLOSED_SENTINEL = exports.DEFAULT_POOL_SIZE = exports.DBOS_FUNCNAME_CLOSESTREAM = exports.DBOS_FUNCNAME_WRITESTREAM = exports.DBOS_FUNCNAME_GETSTATUS = exports.DBOS_FUNCNAME_SLEEP = exports.DBOS_FUNCNAME_GETEVENT = exports.DBOS_FUNCNAME_SETEVENT = exports.DBOS_FUNCNAME_RECV = exports.DBOS_FUNCNAME_SEND = void 0;
|
|
12
|
+
exports.SystemDatabase = exports.verifySystemDatabase = exports.ensureSystemDatabase = exports.grantDbosSchemaPermissions = exports.getDbosSchemaPermissionsSql = exports.DEFAULT_RENAME_BATCH_SIZE = exports.DEFAULT_NOTIFICATION_COALESCE_MS = exports.DBOS_STREAMS_CHANNEL = exports.DBOS_WORKFLOW_EVENTS_CHANNEL = exports.DBOS_NOTIFICATIONS_CHANNEL = exports.DBOS_STREAM_CLOSED_SENTINEL = exports.DEFAULT_POOL_SIZE = exports.DBOS_FUNCNAME_CLOSESTREAM = exports.DBOS_FUNCNAME_WRITESTREAM = exports.DBOS_FUNCNAME_GETSTATUS = exports.DBOS_FUNCNAME_SLEEP = exports.DBOS_FUNCNAME_GETEVENT = exports.DBOS_FUNCNAME_SETEVENT = exports.DBOS_FUNCNAME_RECV = exports.DBOS_FUNCNAME_SEND = void 0;
|
|
13
13
|
const dbos_executor_1 = require("./dbos-executor");
|
|
14
14
|
const pg_1 = require("pg");
|
|
15
15
|
const error_1 = require("./error");
|
|
@@ -100,49 +100,94 @@ async function grantDbosSchemaPermissions(databaseUrl, roleName, logger, schemaN
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
exports.grantDbosSchemaPermissions = grantDbosSchemaPermissions;
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Check out a connection carrying our own error handler, so a socket death while we hold it is
|
|
105
|
+
* logged instead of thrown at an emitter with no listener. `release()` takes the handler back off,
|
|
106
|
+
* so nothing we attach outlives the checkout and the pool itself is never touched.
|
|
107
|
+
*/
|
|
108
|
+
async function borrowClient(pool, onError) {
|
|
109
|
+
const client = await pool.connect();
|
|
110
|
+
// Nothing awaits between the checkout and this attach, nor between the removal and release()
|
|
111
|
+
// below, so there is no turn of the event loop in which we hold the connection unguarded.
|
|
112
|
+
client.on('error', onError);
|
|
113
|
+
const release = client.release.bind(client);
|
|
114
|
+
client.release = (err) => {
|
|
115
|
+
client.removeListener('error', onError);
|
|
116
|
+
release(err);
|
|
117
|
+
};
|
|
118
|
+
return client;
|
|
119
|
+
}
|
|
120
|
+
/** Connect to the system database without creating it. The caller releases the client. */
|
|
121
|
+
async function connectToSystemDatabase(sysDbUrl, logger, customPool) {
|
|
105
122
|
if (customPool) {
|
|
106
|
-
|
|
107
|
-
// a client to run migrations.
|
|
108
|
-
client = await customPool.connect();
|
|
123
|
+
return await borrowClient(customPool, (err) => logger.warn(`Unexpected error in system database client: ${err}`));
|
|
109
124
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
const sysClient = new pg_1.Client((0, utils_2.getClientConfig)(sysDbUrl));
|
|
126
|
+
// An 'error' event with no listener would take down the process.
|
|
127
|
+
sysClient.on('error', (err) => logger.warn(`Unexpected error in system database client: ${err}`));
|
|
128
|
+
try {
|
|
129
|
+
await sysClient.connect();
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
await sysClient.end().catch(() => { });
|
|
133
|
+
throw new error_1.DBOSInitializationError(`Unable to connect to system database at ${(0, database_utils_1.maskDatabaseUrl)(sysDbUrl)}: ${e.message}`, e instanceof Error ? e : undefined);
|
|
134
|
+
}
|
|
135
|
+
return sysClient;
|
|
136
|
+
}
|
|
137
|
+
async function releaseSystemDatabaseClient(client, customPool) {
|
|
138
|
+
try {
|
|
139
|
+
if (customPool) {
|
|
140
|
+
client.release();
|
|
118
141
|
}
|
|
119
|
-
|
|
120
|
-
await
|
|
121
|
-
throw new error_1.DBOSInitializationError(`Unable to connect to system database at ${(0, database_utils_1.maskDatabaseUrl)(sysDbUrl)}: ${e.message}`, e instanceof Error ? e : undefined);
|
|
142
|
+
else {
|
|
143
|
+
await client.end();
|
|
122
144
|
}
|
|
123
|
-
client = sysClient;
|
|
124
145
|
}
|
|
146
|
+
catch (e) { }
|
|
147
|
+
}
|
|
148
|
+
async function isCockroachDB(client) {
|
|
149
|
+
const versionRes = await client.query('SELECT version() AS version');
|
|
150
|
+
return /cockroachdb/i.test(versionRes.rows[0]?.version ?? '');
|
|
151
|
+
}
|
|
152
|
+
async function ensureSystemDatabase(sysDbUrl, logger, customPool, schemaName = 'dbos', useListenNotify = true) {
|
|
153
|
+
if (!customPool) {
|
|
154
|
+
// A custom pool means the database already exists; otherwise, create it if it does not.
|
|
155
|
+
await (0, database_utils_1.ensurePGDatabase)(sysDbUrl, logger);
|
|
156
|
+
}
|
|
157
|
+
const client = await connectToSystemDatabase(sysDbUrl, logger, customPool);
|
|
125
158
|
try {
|
|
126
|
-
const
|
|
127
|
-
const isCockroach = /cockroachdb/i.test(versionRes.rows[0]?.version ?? '');
|
|
159
|
+
const isCockroach = await isCockroachDB(client);
|
|
128
160
|
await (0, migration_runner_1.runSysMigrationsPg)(client, (0, migrations_1.allMigrations)(schemaName, { useListenNotify, isCockroach }), schemaName, {
|
|
129
161
|
onWarn: (e) => logger.info(e),
|
|
130
162
|
isCockroach,
|
|
131
163
|
});
|
|
132
164
|
}
|
|
133
165
|
finally {
|
|
134
|
-
|
|
135
|
-
if (customPool) {
|
|
136
|
-
client.release();
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
await client.end();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch (e) { }
|
|
166
|
+
await releaseSystemDatabaseClient(client, customPool);
|
|
143
167
|
}
|
|
144
168
|
}
|
|
145
169
|
exports.ensureSystemDatabase = ensureSystemDatabase;
|
|
170
|
+
/** Check the system database is migrated to the version this build requires, creating and changing nothing. */
|
|
171
|
+
async function verifySystemDatabase(sysDbUrl, logger, customPool, schemaName = 'dbos', useListenNotify = true) {
|
|
172
|
+
const client = await connectToSystemDatabase(sysDbUrl, logger, customPool);
|
|
173
|
+
try {
|
|
174
|
+
const isCockroach = await isCockroachDB(client);
|
|
175
|
+
const requiredVersion = (0, migrations_1.allMigrations)(schemaName, { useListenNotify, isCockroach }).length;
|
|
176
|
+
const currentVersion = await (0, migration_runner_1.getCurrentSysDBVersion)(client, schemaName);
|
|
177
|
+
// A database ahead of this build belongs to a newer peer, which the migration runner also tolerates.
|
|
178
|
+
if (currentVersion < requiredVersion) {
|
|
179
|
+
throw new error_1.DBOSInitializationError(`System database ${(0, database_utils_1.maskDatabaseUrl)(sysDbUrl)} is at schema version ${currentVersion}, but this version ` +
|
|
180
|
+
`of DBOS requires ${requiredVersion}. This process is configured with runMigrations disabled, so it ` +
|
|
181
|
+
`will not migrate it: either migrate the system database out of band (\`npx dbos schema\`) or launch ` +
|
|
182
|
+
`with runMigrations enabled.`);
|
|
183
|
+
}
|
|
184
|
+
logger.debug(`System database schema version ${currentVersion} satisfies the required version ${requiredVersion}`);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await releaseSystemDatabaseClient(client, customPool);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.verifySystemDatabase = verifySystemDatabase;
|
|
146
191
|
class NotificationMap {
|
|
147
192
|
map = new Map();
|
|
148
193
|
curCK = 0;
|
|
@@ -441,6 +486,8 @@ class SystemDatabase {
|
|
|
441
486
|
runningWorkflowMap = new Map(); // Map from workflowID to workflow promise, queue name and partition key
|
|
442
487
|
// Per-partition-key created_at cursors: keep per-key queue order monotonic across batches
|
|
443
488
|
#batchCreatedAtCursors = new Map();
|
|
489
|
+
// Set by destroy(), so polling waits end instead of running on against a pool that outlives this handle.
|
|
490
|
+
#destroyed = false;
|
|
444
491
|
constructor(systemDatabaseUrl, logger, serializer, sysDbPoolSize = exports.DEFAULT_POOL_SIZE, systemDatabasePool, schemaName = 'dbos', useListenNotify = true, pollingConcurrency, notificationCoalesceMs = exports.DEFAULT_NOTIFICATION_COALESCE_MS,
|
|
445
492
|
// The application this handle acts for; undefined writes unclaimed rows.
|
|
446
493
|
appName) {
|
|
@@ -469,14 +516,20 @@ class SystemDatabase {
|
|
|
469
516
|
const effectivePoolSize = this.pool.options.max ?? sysDbPoolSize;
|
|
470
517
|
const pollingLimit = pollingConcurrency ?? Math.max(1, Math.floor(effectivePoolSize / 2));
|
|
471
518
|
this.pollLimiter = new utils_1.Semaphore(pollingLimit);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this.logger.warn(`Unexpected error in idle client: ${err}`);
|
|
519
|
+
// Only ever attach listeners to a pool we own; a caller's pool is theirs to instrument. Idle
|
|
520
|
+
// connections are all this covers, since #connect guards the ones we are holding.
|
|
521
|
+
if (!this.customPool) {
|
|
522
|
+
this.pool.on('error', (err) => {
|
|
523
|
+
this.logger.warn(`Unexpected error in pool: ${err}`);
|
|
478
524
|
});
|
|
479
|
-
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
#onClientError = (err) => {
|
|
528
|
+
this.logger.warn(`Unexpected error on a system database connection: ${err}`);
|
|
529
|
+
};
|
|
530
|
+
/** Check out a pool connection guarded for as long as we hold it. See {@link borrowClient}. */
|
|
531
|
+
#connect() {
|
|
532
|
+
return borrowClient(this.pool, this.#onClientError);
|
|
480
533
|
}
|
|
481
534
|
getSerializer() {
|
|
482
535
|
return this.serializer;
|
|
@@ -535,8 +588,10 @@ class SystemDatabase {
|
|
|
535
588
|
`Either ${takeANewName}, or, if '${current}' was renamed to '${owner}', ` +
|
|
536
589
|
`re-own its rows first with dbos rename-application`);
|
|
537
590
|
}
|
|
538
|
-
|
|
539
|
-
|
|
591
|
+
/** Migrates the system database, or, when `runMigrations` is false, verifies it is already migrated. */
|
|
592
|
+
async init(runMigrations = true) {
|
|
593
|
+
const migrateOrVerify = runMigrations ? ensureSystemDatabase : verifySystemDatabase;
|
|
594
|
+
await migrateOrVerify(this.systemDatabaseUrl, this.logger, this.customPool ? this.pool : undefined, this.schemaName, this.shouldUseDBNotifications);
|
|
540
595
|
if (this.shouldUseDBNotifications) {
|
|
541
596
|
await this.#listenForNotifications();
|
|
542
597
|
// Push coalesced stream and event notifications off the write path.
|
|
@@ -547,6 +602,7 @@ class SystemDatabase {
|
|
|
547
602
|
async destroy() {
|
|
548
603
|
// Set synchronously, before any await, so no reconnect is scheduled or published after this point.
|
|
549
604
|
this.#notificationsStopped = true;
|
|
605
|
+
this.#destroyed = true;
|
|
550
606
|
if (this.reconnectTimeout) {
|
|
551
607
|
clearTimeout(this.reconnectTimeout);
|
|
552
608
|
this.reconnectTimeout = null;
|
|
@@ -561,45 +617,28 @@ class SystemDatabase {
|
|
|
561
617
|
if (this.notificationsClient) {
|
|
562
618
|
this.#retireNotificationsClient(this.notificationsClient);
|
|
563
619
|
}
|
|
564
|
-
|
|
620
|
+
// We attached nothing to the pool object itself, so there is nothing to unpick; only close one we own.
|
|
621
|
+
if (!this.customPool) {
|
|
622
|
+
await this.pool.end();
|
|
623
|
+
}
|
|
565
624
|
}
|
|
566
625
|
// ==================== Workflow Status ====================
|
|
567
|
-
|
|
568
|
-
|
|
626
|
+
/** Runs on `client` if given, joining its transaction; otherwise in its own retried transaction. */
|
|
627
|
+
async initWorkflowStatus(initStatus, ownerXid, client) {
|
|
628
|
+
if (client !== undefined) {
|
|
629
|
+
return await this.#initWorkflowStatusInternal(client, initStatus, ownerXid);
|
|
630
|
+
}
|
|
631
|
+
return await this.initWorkflowStatusStandalone(initStatus, ownerXid);
|
|
632
|
+
}
|
|
633
|
+
async initWorkflowStatusStandalone(initStatus, ownerXid) {
|
|
634
|
+
const client = await this.#connect();
|
|
569
635
|
let shouldCommit = false;
|
|
570
636
|
try {
|
|
571
637
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
572
|
-
const
|
|
573
|
-
if (resRow.name !== initStatus.workflowName) {
|
|
574
|
-
const msg = `Workflow already exists with a different function name: ${resRow.name}, but the provided function name is: ${initStatus.workflowName}`;
|
|
575
|
-
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
|
576
|
-
}
|
|
577
|
-
else if (resRow.class_name !== initStatus.workflowClassName) {
|
|
578
|
-
const msg = `Workflow already exists with a different class name: ${resRow.class_name}, but the provided class name is: ${initStatus.workflowClassName}`;
|
|
579
|
-
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
|
580
|
-
}
|
|
581
|
-
else if ((resRow.config_name || '') !== (initStatus.workflowConfigName || '')) {
|
|
582
|
-
const msg = `Workflow already exists with a different class configuration: ${resRow.config_name}, but the provided class configuration is: ${initStatus.workflowConfigName}`;
|
|
583
|
-
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
|
584
|
-
}
|
|
585
|
-
else if ((resRow.queue_name ?? undefined) !== (initStatus.queueName ?? undefined)) {
|
|
586
|
-
// This is a warning because a different queue name is not necessarily an error.
|
|
587
|
-
this.logger.warn(`Workflow (${initStatus.workflowUUID}) already exists in queue: ${resRow.queue_name}, but the provided queue name is: ${initStatus.queueName}. The queue is not updated. ${new Error().stack}`);
|
|
588
|
-
}
|
|
589
|
-
const status = resRow.status;
|
|
590
|
-
const deadlineEpochMS = resRow.workflow_deadline_epoch_ms ?? undefined;
|
|
638
|
+
const result = await this.#initWorkflowStatusInternal(client, initStatus, ownerXid);
|
|
591
639
|
// If there is an existing DB record and we aren't here to recover it, leave it be.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
// Upsert above already set executor assignment
|
|
596
|
-
shouldCommit = true;
|
|
597
|
-
return {
|
|
598
|
-
status,
|
|
599
|
-
deadlineEpochMS,
|
|
600
|
-
shouldExecuteOnThisExecutor: true,
|
|
601
|
-
serialization: resRow.serialization,
|
|
602
|
-
};
|
|
640
|
+
shouldCommit = result.shouldExecuteOnThisExecutor;
|
|
641
|
+
return result;
|
|
603
642
|
}
|
|
604
643
|
finally {
|
|
605
644
|
try {
|
|
@@ -616,6 +655,34 @@ class SystemDatabase {
|
|
|
616
655
|
}
|
|
617
656
|
}
|
|
618
657
|
}
|
|
658
|
+
async #initWorkflowStatusInternal(client, initStatus, ownerXid) {
|
|
659
|
+
const resRow = await this.insertWorkflowStatus(client, initStatus, ownerXid);
|
|
660
|
+
if (resRow.name !== initStatus.workflowName) {
|
|
661
|
+
const msg = `Workflow already exists with a different function name: ${resRow.name}, but the provided function name is: ${initStatus.workflowName}`;
|
|
662
|
+
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
|
663
|
+
}
|
|
664
|
+
else if (resRow.class_name !== initStatus.workflowClassName) {
|
|
665
|
+
const msg = `Workflow already exists with a different class name: ${resRow.class_name}, but the provided class name is: ${initStatus.workflowClassName}`;
|
|
666
|
+
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
|
667
|
+
}
|
|
668
|
+
else if ((resRow.config_name || '') !== (initStatus.workflowConfigName || '')) {
|
|
669
|
+
const msg = `Workflow already exists with a different class configuration: ${resRow.config_name}, but the provided class configuration is: ${initStatus.workflowConfigName}`;
|
|
670
|
+
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
|
671
|
+
}
|
|
672
|
+
else if ((resRow.queue_name ?? undefined) !== (initStatus.queueName ?? undefined)) {
|
|
673
|
+
// This is a warning because a different queue name is not necessarily an error.
|
|
674
|
+
this.logger.warn(`Workflow (${initStatus.workflowUUID}) already exists in queue: ${resRow.queue_name}, but the provided queue name is: ${initStatus.queueName}. The queue is not updated. ${new Error().stack}`);
|
|
675
|
+
}
|
|
676
|
+
const status = resRow.status;
|
|
677
|
+
const deadlineEpochMS = resRow.workflow_deadline_epoch_ms ?? undefined;
|
|
678
|
+
// The upsert above already set executor assignment for a row we own.
|
|
679
|
+
return {
|
|
680
|
+
status,
|
|
681
|
+
deadlineEpochMS,
|
|
682
|
+
shouldExecuteOnThisExecutor: ownerXid === resRow.owner_xid,
|
|
683
|
+
serialization: resRow.serialization,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
619
686
|
/** Move claimed workflows that exhausted their attempts off the queue, leaving rows others have moved on alone. */
|
|
620
687
|
async deadLetterWorkflows(workflowIDs, minRecoveryAttempts) {
|
|
621
688
|
if (workflowIDs.length === 0)
|
|
@@ -737,7 +804,7 @@ class SystemDatabase {
|
|
|
737
804
|
'schedule_name',
|
|
738
805
|
'application_name',
|
|
739
806
|
];
|
|
740
|
-
const client = await this
|
|
807
|
+
const client = await this.#connect();
|
|
741
808
|
try {
|
|
742
809
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
743
810
|
// Chunk to stay well under the bind-parameter limit.
|
|
@@ -775,7 +842,7 @@ class SystemDatabase {
|
|
|
775
842
|
return inserted;
|
|
776
843
|
}
|
|
777
844
|
async recordWorkflowOutput(workflowID, status) {
|
|
778
|
-
const client = await this
|
|
845
|
+
const client = await this.#connect();
|
|
779
846
|
try {
|
|
780
847
|
return await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.SUCCESS, { output: status.output });
|
|
781
848
|
}
|
|
@@ -784,7 +851,7 @@ class SystemDatabase {
|
|
|
784
851
|
}
|
|
785
852
|
}
|
|
786
853
|
async recordWorkflowError(workflowID, status) {
|
|
787
|
-
const client = await this
|
|
854
|
+
const client = await this.#connect();
|
|
788
855
|
try {
|
|
789
856
|
return await this.#recordWorkflowOutcome(client, workflowID, workflow_1.StatusString.ERROR, { error: status.error });
|
|
790
857
|
}
|
|
@@ -846,7 +913,7 @@ class SystemDatabase {
|
|
|
846
913
|
return status ? JSON.stringify(status) : null;
|
|
847
914
|
};
|
|
848
915
|
if (callerID && callerFN) {
|
|
849
|
-
const client = await this
|
|
916
|
+
const client = await this.#connect();
|
|
850
917
|
try {
|
|
851
918
|
// Check if the operation has been done before for OAOO (only do this inside a workflow).
|
|
852
919
|
const json = await this.#runAndRecordResult(client, exports.DBOS_FUNCNAME_GETSTATUS, callerID, callerFN, funcGetStatus);
|
|
@@ -882,7 +949,7 @@ class SystemDatabase {
|
|
|
882
949
|
}
|
|
883
950
|
// Only used in tests
|
|
884
951
|
async setWorkflowStatus(workflowID, status, resetRecoveryAttempts, internalOptions) {
|
|
885
|
-
const client = await this
|
|
952
|
+
const client = await this.#connect();
|
|
886
953
|
try {
|
|
887
954
|
await this.updateWorkflowStatus(client, workflowID, status, {
|
|
888
955
|
update: {
|
|
@@ -899,7 +966,7 @@ class SystemDatabase {
|
|
|
899
966
|
}
|
|
900
967
|
// ==================== Step Results ====================
|
|
901
968
|
async getOperationResultAndThrowIfCancelled(workflowID, functionID) {
|
|
902
|
-
const client = await this
|
|
969
|
+
const client = await this.#connect();
|
|
903
970
|
try {
|
|
904
971
|
return await this.#getOperationResultAndThrowIfCancelled(client, workflowID, functionID);
|
|
905
972
|
}
|
|
@@ -922,7 +989,7 @@ class SystemDatabase {
|
|
|
922
989
|
return rows;
|
|
923
990
|
}
|
|
924
991
|
async recordOperationResult(workflowID, functionID, functionName, checkConflict, startTimeEpochMs, endTimeEpochMs, options = {}) {
|
|
925
|
-
const client = await this
|
|
992
|
+
const client = await this.#connect();
|
|
926
993
|
try {
|
|
927
994
|
await this.recordOperationResultInternal(client, workflowID, functionID, functionName, checkConflict, startTimeEpochMs, endTimeEpochMs, options);
|
|
928
995
|
}
|
|
@@ -932,7 +999,7 @@ class SystemDatabase {
|
|
|
932
999
|
}
|
|
933
1000
|
}
|
|
934
1001
|
async runTransactionalStep(workflowID, functionID, functionName, callback) {
|
|
935
|
-
const client = await this
|
|
1002
|
+
const client = await this.#connect();
|
|
936
1003
|
try {
|
|
937
1004
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
938
1005
|
const existing = await this.#getOperationResultAndThrowIfCancelled(client, workflowID, functionID);
|
|
@@ -1056,7 +1123,7 @@ class SystemDatabase {
|
|
|
1056
1123
|
return await this.debounceDelayedWorkflowStandalone(params);
|
|
1057
1124
|
}
|
|
1058
1125
|
async debounceDelayedWorkflowStandalone(params) {
|
|
1059
|
-
const client = await this
|
|
1126
|
+
const client = await this.#connect();
|
|
1060
1127
|
try {
|
|
1061
1128
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
1062
1129
|
const result = await this.#debounceDelayedWorkflowInternal(client, params);
|
|
@@ -1237,7 +1304,7 @@ class SystemDatabase {
|
|
|
1237
1304
|
if (originalWorkflowIDs.length !== forkedWorkflowIDs.length || originalWorkflowIDs.length !== startSteps.length) {
|
|
1238
1305
|
throw new Error('originalWorkflowIDs, forkedWorkflowIDs, and startSteps must have the same length');
|
|
1239
1306
|
}
|
|
1240
|
-
const client = await this
|
|
1307
|
+
const client = await this.#connect();
|
|
1241
1308
|
try {
|
|
1242
1309
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
1243
1310
|
// Fetch the status of all original workflows inside the transaction.
|
|
@@ -1383,7 +1450,7 @@ class SystemDatabase {
|
|
|
1383
1450
|
workflowIDs.push(...(await this.getWorkflowChildren(workflowID)));
|
|
1384
1451
|
}
|
|
1385
1452
|
const exportedWorkflows = [];
|
|
1386
|
-
const client = await this
|
|
1453
|
+
const client = await this.#connect();
|
|
1387
1454
|
try {
|
|
1388
1455
|
for (const wfID of workflowIDs) {
|
|
1389
1456
|
// Export workflow_status
|
|
@@ -1441,7 +1508,7 @@ class SystemDatabase {
|
|
|
1441
1508
|
return exportedWorkflows;
|
|
1442
1509
|
}
|
|
1443
1510
|
async importWorkflow(workflows) {
|
|
1444
|
-
const client = await this
|
|
1511
|
+
const client = await this.#connect();
|
|
1445
1512
|
try {
|
|
1446
1513
|
await client.query('BEGIN');
|
|
1447
1514
|
for (const workflow of workflows) {
|
|
@@ -1597,6 +1664,10 @@ class SystemDatabase {
|
|
|
1597
1664
|
* control-plane work hits the pool directly and bypasses the limiter.
|
|
1598
1665
|
*/
|
|
1599
1666
|
#pollWithLimiter(query) {
|
|
1667
|
+
// Closing our own pool used to end these waits; a caller's pool stays open, so end them here.
|
|
1668
|
+
if (this.#destroyed) {
|
|
1669
|
+
return Promise.reject(new error_1.DBOSError('The system database has been shut down'));
|
|
1670
|
+
}
|
|
1600
1671
|
return this.pollLimiter.runExclusive(query);
|
|
1601
1672
|
}
|
|
1602
1673
|
/**
|
|
@@ -1711,7 +1782,7 @@ class SystemDatabase {
|
|
|
1711
1782
|
async send(workflowID, functionID, destinationID, message, topic, serialization, idempotencyKey) {
|
|
1712
1783
|
topic = topic ?? this.nullTopic;
|
|
1713
1784
|
const messageUUID = idempotencyKey ? `${idempotencyKey}::${destinationID}` : (0, crypto_1.randomUUID)();
|
|
1714
|
-
const client = await this
|
|
1785
|
+
const client = await this.#connect();
|
|
1715
1786
|
try {
|
|
1716
1787
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
1717
1788
|
await this.#runAndRecordResult(client, exports.DBOS_FUNCNAME_SEND, workflowID, functionID, async () => {
|
|
@@ -1737,12 +1808,22 @@ class SystemDatabase {
|
|
|
1737
1808
|
client.release();
|
|
1738
1809
|
}
|
|
1739
1810
|
}
|
|
1740
|
-
|
|
1811
|
+
/** Runs on `client` if given, joining its transaction; otherwise on the pool with retries. */
|
|
1812
|
+
async sendDirect(destinationID, message, topic, serialization, idempotencyKey, client) {
|
|
1813
|
+
if (client !== undefined) {
|
|
1814
|
+
return await this.#sendDirectInternal(client, destinationID, message, topic, serialization, idempotencyKey);
|
|
1815
|
+
}
|
|
1816
|
+
return await this.sendDirectStandalone(destinationID, message, topic, serialization, idempotencyKey);
|
|
1817
|
+
}
|
|
1818
|
+
async sendDirectStandalone(destinationID, message, topic, serialization, idempotencyKey) {
|
|
1819
|
+
return await this.#sendDirectInternal(this.pool, destinationID, message, topic, serialization, idempotencyKey);
|
|
1820
|
+
}
|
|
1821
|
+
async #sendDirectInternal(db, destinationID, message, topic, serialization, idempotencyKey) {
|
|
1741
1822
|
topic = topic ?? this.nullTopic;
|
|
1742
1823
|
// Same per-destination scoping as send() above.
|
|
1743
1824
|
const messageUUID = idempotencyKey ? `${idempotencyKey}::${destinationID}` : (0, crypto_1.randomUUID)();
|
|
1744
1825
|
try {
|
|
1745
|
-
await
|
|
1826
|
+
await db.query(`INSERT INTO "${this.schemaName}".notifications (destination_uuid, topic, message, serialization, message_uuid)
|
|
1746
1827
|
VALUES ($1, $2, $3, $4, $5)
|
|
1747
1828
|
ON CONFLICT (message_uuid) DO NOTHING;`, [destinationID, topic, message, serialization, messageUUID]);
|
|
1748
1829
|
}
|
|
@@ -1808,7 +1889,7 @@ class SystemDatabase {
|
|
|
1808
1889
|
// Transactionally consume and return the message if it's in the DB, otherwise return null.
|
|
1809
1890
|
let message = null;
|
|
1810
1891
|
let serialization = null;
|
|
1811
|
-
const client = await this
|
|
1892
|
+
const client = await this.#connect();
|
|
1812
1893
|
try {
|
|
1813
1894
|
await client.query(`BEGIN ISOLATION LEVEL READ COMMITTED`);
|
|
1814
1895
|
const finalRecvRows = (await client.query(`UPDATE "${this.schemaName}".notifications
|
|
@@ -1848,7 +1929,7 @@ class SystemDatabase {
|
|
|
1848
1929
|
}
|
|
1849
1930
|
// ==================== Events ====================
|
|
1850
1931
|
async setEvent(workflowID, functionID, key, message, serialization) {
|
|
1851
|
-
const client = await this
|
|
1932
|
+
const client = await this.#connect();
|
|
1852
1933
|
try {
|
|
1853
1934
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
1854
1935
|
// Only a real write (not a replay) should wake readers.
|
|
@@ -1996,7 +2077,7 @@ class SystemDatabase {
|
|
|
1996
2077
|
}
|
|
1997
2078
|
// ==================== Streams ====================
|
|
1998
2079
|
async writeStreamFromStep(workflowID, functionID, key, serializedValue, serialization) {
|
|
1999
|
-
const client = await this
|
|
2080
|
+
const client = await this.#connect();
|
|
2000
2081
|
try {
|
|
2001
2082
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
2002
2083
|
// Find the maximum offset for this workflow_uuid and key combination
|
|
@@ -2022,7 +2103,7 @@ class SystemDatabase {
|
|
|
2022
2103
|
}
|
|
2023
2104
|
}
|
|
2024
2105
|
async writeStreamFromWorkflow(workflowID, functionID, key, serializedValue, serialization, functionName) {
|
|
2025
|
-
const client = await this
|
|
2106
|
+
const client = await this.#connect();
|
|
2026
2107
|
try {
|
|
2027
2108
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
2028
2109
|
// Only a real insert (not a replay) should wake readers.
|
|
@@ -2142,7 +2223,7 @@ class SystemDatabase {
|
|
|
2142
2223
|
}
|
|
2143
2224
|
try {
|
|
2144
2225
|
// One statement: one round trip, one async-notify queue-lock acquisition; unnest emits one notification per payload.
|
|
2145
|
-
const client = await this
|
|
2226
|
+
const client = await this.#connect();
|
|
2146
2227
|
try {
|
|
2147
2228
|
await client.query(`SELECT pg_notify($1, p) FROM unnest($2::text[]) AS p`, [channel, Array.from(batch)]);
|
|
2148
2229
|
}
|
|
@@ -2158,7 +2239,7 @@ class SystemDatabase {
|
|
|
2158
2239
|
}
|
|
2159
2240
|
// ==================== Observability: Workflow Communications ====================
|
|
2160
2241
|
async getAllEvents(workflowID) {
|
|
2161
|
-
const client = await this
|
|
2242
|
+
const client = await this.#connect();
|
|
2162
2243
|
try {
|
|
2163
2244
|
const result = await client.query(`SELECT key, value, serialization FROM "${this.schemaName}".workflow_events
|
|
2164
2245
|
WHERE workflow_uuid = $1`, [workflowID]);
|
|
@@ -2173,7 +2254,7 @@ class SystemDatabase {
|
|
|
2173
2254
|
}
|
|
2174
2255
|
}
|
|
2175
2256
|
async getAllNotifications(workflowID) {
|
|
2176
|
-
const client = await this
|
|
2257
|
+
const client = await this.#connect();
|
|
2177
2258
|
try {
|
|
2178
2259
|
const result = await client.query(`SELECT topic, message, serialization, created_at_epoch_ms, consumed
|
|
2179
2260
|
FROM "${this.schemaName}".notifications
|
|
@@ -2191,7 +2272,7 @@ class SystemDatabase {
|
|
|
2191
2272
|
}
|
|
2192
2273
|
}
|
|
2193
2274
|
async getAllStreamEntries(workflowID) {
|
|
2194
|
-
const client = await this
|
|
2275
|
+
const client = await this.#connect();
|
|
2195
2276
|
try {
|
|
2196
2277
|
const result = await client.query(`SELECT key, value, serialization FROM "${this.schemaName}".streams
|
|
2197
2278
|
WHERE workflow_uuid = $1
|
|
@@ -2264,7 +2345,7 @@ class SystemDatabase {
|
|
|
2264
2345
|
partitionFilter = `AND queue_partition_key = $PARTITION`;
|
|
2265
2346
|
partitionParams.push(queuePartitionKey);
|
|
2266
2347
|
}
|
|
2267
|
-
const client = await this
|
|
2348
|
+
const client = await this.#connect();
|
|
2268
2349
|
try {
|
|
2269
2350
|
// Default to READ COMMITTED except with global concurrency limits or rate limits
|
|
2270
2351
|
if (queue.concurrency !== undefined || queue.rateLimit !== undefined) {
|
|
@@ -2419,7 +2500,7 @@ class SystemDatabase {
|
|
|
2419
2500
|
throw new error_1.DBOSError(`Batched partitioned dequeue requires a queue with concurrency 1 and no rate limit: ${queue.name}`);
|
|
2420
2501
|
}
|
|
2421
2502
|
// workerConcurrency needs no handling here: dispatch routes 0 (the pause-dequeue idiom) to the fallback path, and validation caps any other value at concurrency=1, which the PENDING gate already enforces globally.
|
|
2422
|
-
const client = await this
|
|
2503
|
+
const client = await this.#connect();
|
|
2423
2504
|
try {
|
|
2424
2505
|
await client.query('BEGIN');
|
|
2425
2506
|
const latestVersion = await this.#latestApplicationVersionName(client);
|
|
@@ -3219,7 +3300,7 @@ class SystemDatabase {
|
|
|
3219
3300
|
await this.pool.query(`UPDATE "${this.schemaName}".workflow_schedules SET last_fired_at = $1 WHERE schedule_name = $2`, [lastFiredAt, name]);
|
|
3220
3301
|
}
|
|
3221
3302
|
async applySchedules(schedules) {
|
|
3222
|
-
const client = await this
|
|
3303
|
+
const client = await this.#connect();
|
|
3223
3304
|
try {
|
|
3224
3305
|
await client.query('BEGIN');
|
|
3225
3306
|
for (const sched of schedules) {
|
|
@@ -3271,7 +3352,7 @@ class SystemDatabase {
|
|
|
3271
3352
|
*/
|
|
3272
3353
|
async createApplicationVersion(versionName, applicationName) {
|
|
3273
3354
|
const owner = applicationName ?? this.appName;
|
|
3274
|
-
const client = await this
|
|
3355
|
+
const client = await this.#connect();
|
|
3275
3356
|
try {
|
|
3276
3357
|
await client.query('BEGIN');
|
|
3277
3358
|
// Claim a pre-upgrade row in place, so the version is not recreated or retimed.
|
|
@@ -3302,7 +3383,7 @@ class SystemDatabase {
|
|
|
3302
3383
|
*/
|
|
3303
3384
|
async updateApplicationVersionTimestamp(versionName, newTimestamp, applicationName) {
|
|
3304
3385
|
const owner = applicationName ?? this.appName;
|
|
3305
|
-
const client = await this
|
|
3386
|
+
const client = await this.#connect();
|
|
3306
3387
|
try {
|
|
3307
3388
|
await client.query('BEGIN');
|
|
3308
3389
|
const resolved = await this.#resolveRowOwner(client, 'application_versions', 'version_name', versionName, owner, 'Application version');
|
|
@@ -3408,7 +3489,7 @@ class SystemDatabase {
|
|
|
3408
3489
|
application_name = COALESCE("${this.schemaName}".queues.application_name, EXCLUDED.application_name)`
|
|
3409
3490
|
: `ON CONFLICT (name) DO NOTHING`;
|
|
3410
3491
|
const owner = record.applicationName ?? this.appName;
|
|
3411
|
-
const client = await this
|
|
3492
|
+
const client = await this.#connect();
|
|
3412
3493
|
try {
|
|
3413
3494
|
await client.query('BEGIN');
|
|
3414
3495
|
const existed = await client.query(`SELECT name FROM "${this.schemaName}".queues WHERE name = $1`, [record.name]);
|
|
@@ -3527,7 +3608,7 @@ class SystemDatabase {
|
|
|
3527
3608
|
throw new error_1.DBOSError(`batchSize must be a positive integer, got ${batchSize}`);
|
|
3528
3609
|
}
|
|
3529
3610
|
// Never a merge: queue, schedule, and version names are globally unique whatever their owner, so this cannot collide.
|
|
3530
|
-
const client = await this
|
|
3611
|
+
const client = await this.#connect();
|
|
3531
3612
|
let queues, schedules, versions, inFlight;
|
|
3532
3613
|
try {
|
|
3533
3614
|
await client.query('BEGIN');
|
|
@@ -3815,7 +3896,7 @@ class SystemDatabase {
|
|
|
3815
3896
|
const startTimeMs = Date.now();
|
|
3816
3897
|
// Round once so the deadline stays integral: completed_at_epoch_ms is BIGINT and rejects fractional values.
|
|
3817
3898
|
const endTimeMs = startTimeMs + Math.ceil(durationMS);
|
|
3818
|
-
const client = await this
|
|
3899
|
+
const client = await this.#connect();
|
|
3819
3900
|
try {
|
|
3820
3901
|
const res = await this.#getOperationResultAndThrowIfCancelled(client, workflowID, functionID);
|
|
3821
3902
|
if (res) {
|
|
@@ -3847,7 +3928,7 @@ class SystemDatabase {
|
|
|
3847
3928
|
this.notificationsClient = null;
|
|
3848
3929
|
}
|
|
3849
3930
|
client.removeAllListeners();
|
|
3850
|
-
//
|
|
3931
|
+
// Cover the release() call itself, which tears the connection down and can surface a socket error.
|
|
3851
3932
|
client.on('error', () => { });
|
|
3852
3933
|
try {
|
|
3853
3934
|
client.release(true);
|
|
@@ -3855,6 +3936,10 @@ class SystemDatabase {
|
|
|
3855
3936
|
catch (e) {
|
|
3856
3937
|
this.logger.warn(`Error releasing notifications client: ${String(e)}`);
|
|
3857
3938
|
}
|
|
3939
|
+
// release() re-attached pg's idle listener, which would forward this dead client's error on to the
|
|
3940
|
+
// pool. A caller's pool may have no 'error' listener at all, so this client's death has to stay ours.
|
|
3941
|
+
client.removeAllListeners('error');
|
|
3942
|
+
client.on('error', (e) => this.logger.warn(`Error on retired notifications client: ${e}`));
|
|
3858
3943
|
}
|
|
3859
3944
|
// Shutdown can begin during any await in the setup below; releasing the client instead of carrying on
|
|
3860
3945
|
// keeps pool.end() from waiting on a connection that will never be published.
|
|
@@ -3878,7 +3963,7 @@ class SystemDatabase {
|
|
|
3878
3963
|
};
|
|
3879
3964
|
let acquired = null;
|
|
3880
3965
|
try {
|
|
3881
|
-
const client = await this
|
|
3966
|
+
const client = await this.#connect();
|
|
3882
3967
|
acquired = client;
|
|
3883
3968
|
if (this.#abandonIfStopped(client))
|
|
3884
3969
|
return;
|
|
@@ -3960,7 +4045,7 @@ __decorate([
|
|
|
3960
4045
|
__metadata("design:type", Function),
|
|
3961
4046
|
__metadata("design:paramtypes", [Object, Object]),
|
|
3962
4047
|
__metadata("design:returntype", Promise)
|
|
3963
|
-
], SystemDatabase.prototype, "
|
|
4048
|
+
], SystemDatabase.prototype, "initWorkflowStatusStandalone", null);
|
|
3964
4049
|
__decorate([
|
|
3965
4050
|
dbRetry(),
|
|
3966
4051
|
__metadata("design:type", Function),
|
|
@@ -4062,7 +4147,7 @@ __decorate([
|
|
|
4062
4147
|
__metadata("design:type", Function),
|
|
4063
4148
|
__metadata("design:paramtypes", [String, Object, Object, Object, String]),
|
|
4064
4149
|
__metadata("design:returntype", Promise)
|
|
4065
|
-
], SystemDatabase.prototype, "
|
|
4150
|
+
], SystemDatabase.prototype, "sendDirectStandalone", null);
|
|
4066
4151
|
__decorate([
|
|
4067
4152
|
dbRetry(),
|
|
4068
4153
|
__metadata("design:type", Function),
|