@dbos-inc/dbos-sdk 4.20.7-preview → 4.20.9-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/client.d.ts +9 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +11 -7
- 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 +12 -0
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +5 -1
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/system_database.d.ts +8 -1
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +62 -28
- package/dist/src/system_database.js.map +1 -1
- package/dist/src/utils.d.ts +20 -0
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +54 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -386,8 +386,14 @@ class SystemDatabase {
|
|
|
386
386
|
workflowEventsMap = new NotificationMap();
|
|
387
387
|
streamsMap = new NotificationMap();
|
|
388
388
|
customPool = false;
|
|
389
|
+
/**
|
|
390
|
+
* Caps how many DB-backed polling reads (from wait operations) may run
|
|
391
|
+
* concurrently against the pool, so a polling storm cannot check out every
|
|
392
|
+
* client and starve control-plane operations. See {@link #pollWithLimiter}.
|
|
393
|
+
*/
|
|
394
|
+
pollLimiter;
|
|
389
395
|
runningWorkflowMap = new Map(); // Map from workflowID to workflow promise, queue name and partition key
|
|
390
|
-
constructor(systemDatabaseUrl, logger, serializer, sysDbPoolSize = exports.DEFAULT_POOL_SIZE, systemDatabasePool, schemaName = 'dbos', useListenNotify = true) {
|
|
396
|
+
constructor(systemDatabaseUrl, logger, serializer, sysDbPoolSize = exports.DEFAULT_POOL_SIZE, systemDatabasePool, schemaName = 'dbos', useListenNotify = true, pollingConcurrency) {
|
|
391
397
|
this.systemDatabaseUrl = systemDatabaseUrl;
|
|
392
398
|
this.logger = logger;
|
|
393
399
|
this.serializer = serializer;
|
|
@@ -406,6 +412,11 @@ class SystemDatabase {
|
|
|
406
412
|
};
|
|
407
413
|
this.pool = new pg_1.Pool(systemPoolConfig);
|
|
408
414
|
}
|
|
415
|
+
// Default the polling limit to half the pool (minimum 1), reserving the rest
|
|
416
|
+
// of the pool for control-plane operations.
|
|
417
|
+
const effectivePoolSize = this.pool.options.max ?? sysDbPoolSize;
|
|
418
|
+
const pollingLimit = pollingConcurrency ?? Math.max(1, Math.floor(effectivePoolSize / 2));
|
|
419
|
+
this.pollLimiter = new utils_1.Semaphore(pollingLimit);
|
|
409
420
|
this.pool.on('error', (err) => {
|
|
410
421
|
this.logger.warn(`Unexpected error in pool: ${err}`);
|
|
411
422
|
});
|
|
@@ -1169,16 +1180,38 @@ class SystemDatabase {
|
|
|
1169
1180
|
//throw new Error('Message notification map is not empty - shutdown is not clean.');
|
|
1170
1181
|
}
|
|
1171
1182
|
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Run a DB-backed polling read under the polling concurrency limiter, so that
|
|
1185
|
+
* high-fan-out wait loops cannot check out every pool client and starve
|
|
1186
|
+
* control-plane operations. Only polling reads should go through here;
|
|
1187
|
+
* control-plane work hits the pool directly and bypasses the limiter.
|
|
1188
|
+
*/
|
|
1189
|
+
#pollWithLimiter(query) {
|
|
1190
|
+
return this.pollLimiter.runExclusive(query);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Cancellation check for use inside polling wait loops: the status read runs
|
|
1194
|
+
* under the polling limiter so it counts against the same concurrency budget
|
|
1195
|
+
* as the rest of the loop's reads.
|
|
1196
|
+
*/
|
|
1197
|
+
async #checkIfCanceledLimited(workflowID) {
|
|
1198
|
+
await this.#pollWithLimiter(() => this.#checkIfCanceled(this.pool, workflowID));
|
|
1199
|
+
}
|
|
1172
1200
|
async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID, pollingIntervalMs) {
|
|
1173
1201
|
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
|
1174
1202
|
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
|
1175
1203
|
const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalResultMs;
|
|
1204
|
+
// Record the durable timeout deadline once before polling. #durableSleep persists it on the
|
|
1205
|
+
// first call and reads back the same value on recovery, so it never changes across iterations.
|
|
1206
|
+
if (timerFuncID !== undefined && callerID !== undefined && timeoutms !== undefined) {
|
|
1207
|
+
finishTime = await this.#durableSleep(callerID, timerFuncID, timeoutms);
|
|
1208
|
+
}
|
|
1176
1209
|
while (true) {
|
|
1177
1210
|
if (callerID)
|
|
1178
|
-
await this
|
|
1211
|
+
await this.#checkIfCanceledLimited(callerID);
|
|
1179
1212
|
try {
|
|
1180
|
-
const { rows } = await this.pool.query(`SELECT status, output, error, serialization FROM "${this.schemaName}".workflow_status
|
|
1181
|
-
|
|
1213
|
+
const { rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT status, output, error, serialization FROM "${this.schemaName}".workflow_status
|
|
1214
|
+
WHERE workflow_uuid=$1`, [workflowID]));
|
|
1182
1215
|
if (rows.length > 0) {
|
|
1183
1216
|
const status = rows[0].status;
|
|
1184
1217
|
if (status === workflow_1.StatusString.SUCCESS) {
|
|
@@ -1206,9 +1239,6 @@ class SystemDatabase {
|
|
|
1206
1239
|
const ct = Date.now();
|
|
1207
1240
|
if (finishTime && ct > finishTime)
|
|
1208
1241
|
return undefined; // Time's up
|
|
1209
|
-
if (timerFuncID !== undefined && callerID !== undefined && timeoutms !== undefined) {
|
|
1210
|
-
finishTime = await this.#durableSleep(callerID, timerFuncID, timeoutms);
|
|
1211
|
-
}
|
|
1212
1242
|
let poll = finishTime ? finishTime - Date.now() : pollIntervalMs;
|
|
1213
1243
|
poll = Math.min(pollIntervalMs, poll);
|
|
1214
1244
|
await (0, utils_1.sleepms)(poll);
|
|
@@ -1219,11 +1249,11 @@ class SystemDatabase {
|
|
|
1219
1249
|
const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalResultMs;
|
|
1220
1250
|
while (true) {
|
|
1221
1251
|
if (callerID)
|
|
1222
|
-
await this
|
|
1223
|
-
const { rows } = await this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1252
|
+
await this.#checkIfCanceledLimited(callerID);
|
|
1253
|
+
const { rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status
|
|
1254
|
+
WHERE workflow_uuid IN (${placeholders})
|
|
1255
|
+
AND status NOT IN ('${workflow_1.StatusString.PENDING}', '${workflow_1.StatusString.ENQUEUED}', '${workflow_1.StatusString.DELAYED}')
|
|
1256
|
+
LIMIT 1`, workflowIds));
|
|
1227
1257
|
if (rows.length > 0) {
|
|
1228
1258
|
return rows[0].workflow_uuid;
|
|
1229
1259
|
}
|
|
@@ -1236,10 +1266,10 @@ class SystemDatabase {
|
|
|
1236
1266
|
while (remainingWorkflowIds.size > 0) {
|
|
1237
1267
|
const currentWorkflowIds = [...remainingWorkflowIds];
|
|
1238
1268
|
if (callerID)
|
|
1239
|
-
await this
|
|
1240
|
-
const { rows } = await this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status
|
|
1241
|
-
|
|
1242
|
-
|
|
1269
|
+
await this.#checkIfCanceledLimited(callerID);
|
|
1270
|
+
const { rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status
|
|
1271
|
+
WHERE workflow_uuid = ANY($1::text[])
|
|
1272
|
+
AND status NOT IN ('${workflow_1.StatusString.PENDING}', '${workflow_1.StatusString.ENQUEUED}', '${workflow_1.StatusString.DELAYED}')`, [currentWorkflowIds]));
|
|
1243
1273
|
for (const row of rows) {
|
|
1244
1274
|
remainingWorkflowIds.delete(row.workflow_uuid);
|
|
1245
1275
|
}
|
|
@@ -1318,6 +1348,11 @@ class SystemDatabase {
|
|
|
1318
1348
|
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
|
1319
1349
|
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
|
1320
1350
|
const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalEventMs;
|
|
1351
|
+
// Record the durable timeout deadline once before polling. #durableSleep persists it on the
|
|
1352
|
+
// first call and reads back the same value on recovery, so it never changes across iterations.
|
|
1353
|
+
if (timeoutms) {
|
|
1354
|
+
finishTime = await this.#durableSleep(workflowID, timeoutFunctionID, timeoutms);
|
|
1355
|
+
}
|
|
1321
1356
|
while (true) {
|
|
1322
1357
|
// register the key with the global notifications listener.
|
|
1323
1358
|
let resolveNotification;
|
|
@@ -1327,17 +1362,14 @@ class SystemDatabase {
|
|
|
1327
1362
|
const payload = `${workflowID}::${topic}`;
|
|
1328
1363
|
const cbr = this.notificationsMap.registerCallback(payload, resolveNotification);
|
|
1329
1364
|
try {
|
|
1330
|
-
await this
|
|
1365
|
+
await this.#checkIfCanceledLimited(workflowID);
|
|
1331
1366
|
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
|
1332
|
-
const initRecvRows = (await this.pool.query(`SELECT topic FROM "${this.schemaName}".notifications WHERE destination_uuid=$1 AND topic=$2 AND consumed = false;`, [workflowID, topic])).rows;
|
|
1367
|
+
const initRecvRows = (await this.#pollWithLimiter(() => this.pool.query(`SELECT topic FROM "${this.schemaName}".notifications WHERE destination_uuid=$1 AND topic=$2 AND consumed = false;`, [workflowID, topic]))).rows;
|
|
1333
1368
|
if (initRecvRows.length !== 0)
|
|
1334
1369
|
break;
|
|
1335
1370
|
const ct = Date.now();
|
|
1336
1371
|
if (finishTime && ct > finishTime)
|
|
1337
1372
|
break; // Time's up
|
|
1338
|
-
if (timeoutms) {
|
|
1339
|
-
finishTime = await this.#durableSleep(workflowID, timeoutFunctionID, timeoutms);
|
|
1340
|
-
}
|
|
1341
1373
|
let poll = finishTime ? finishTime - Date.now() : pollIntervalMs;
|
|
1342
1374
|
poll = Math.min(pollIntervalMs, poll);
|
|
1343
1375
|
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
|
@@ -1442,6 +1474,12 @@ class SystemDatabase {
|
|
|
1442
1474
|
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
|
1443
1475
|
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
|
1444
1476
|
const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalEventMs;
|
|
1477
|
+
// If we have a callerWorkflow, we want a durable sleep, otherwise, not. Record the durable
|
|
1478
|
+
// timeout deadline once before polling. #durableSleep persists it on the first call and reads
|
|
1479
|
+
// back the same value on recovery, so it never changes across iterations.
|
|
1480
|
+
if (callerWorkflow && timeoutms) {
|
|
1481
|
+
finishTime = await this.#durableSleep(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutms);
|
|
1482
|
+
}
|
|
1445
1483
|
// Register the key with the global notifications listener first... we do not want to look in the DB first
|
|
1446
1484
|
// or that would cause a timing hole.
|
|
1447
1485
|
while (true) {
|
|
@@ -1452,11 +1490,11 @@ class SystemDatabase {
|
|
|
1452
1490
|
const cbr = this.workflowEventsMap.registerCallback(payloadKey, resolveNotification);
|
|
1453
1491
|
try {
|
|
1454
1492
|
if (callerWorkflow?.workflowID)
|
|
1455
|
-
await this
|
|
1493
|
+
await this.#checkIfCanceledLimited(callerWorkflow?.workflowID);
|
|
1456
1494
|
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
|
1457
|
-
const initRecvRows = (await this.pool.query(`SELECT key, value, serialization
|
|
1495
|
+
const initRecvRows = (await this.#pollWithLimiter(() => this.pool.query(`SELECT key, value, serialization
|
|
1458
1496
|
FROM "${this.schemaName}".workflow_events
|
|
1459
|
-
WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
|
|
1497
|
+
WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key]))).rows;
|
|
1460
1498
|
if (initRecvRows.length > 0) {
|
|
1461
1499
|
value = initRecvRows[0].value;
|
|
1462
1500
|
valueSer = initRecvRows[0].serialization;
|
|
@@ -1465,10 +1503,6 @@ class SystemDatabase {
|
|
|
1465
1503
|
const ct = Date.now();
|
|
1466
1504
|
if (finishTime && ct > finishTime)
|
|
1467
1505
|
break; // Time's up
|
|
1468
|
-
// If we have a callerWorkflow, we want a durable sleep, otherwise, not
|
|
1469
|
-
if (callerWorkflow && timeoutms) {
|
|
1470
|
-
finishTime = await this.#durableSleep(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutms);
|
|
1471
|
-
}
|
|
1472
1506
|
let poll = finishTime ? finishTime - Date.now() : pollIntervalMs;
|
|
1473
1507
|
poll = Math.min(pollIntervalMs, poll);
|
|
1474
1508
|
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|