@dbos-inc/dbos-sdk 4.20.5-preview → 4.20.8-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 +11 -4
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +20 -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 +1 -1
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos.d.ts +12 -0
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +19 -0
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/system_database.d.ts +9 -3
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +137 -220
- 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
|
@@ -385,11 +385,15 @@ class SystemDatabase {
|
|
|
385
385
|
notificationsMap = new NotificationMap();
|
|
386
386
|
workflowEventsMap = new NotificationMap();
|
|
387
387
|
streamsMap = new NotificationMap();
|
|
388
|
-
cancelWakeupMap = new NotificationMap();
|
|
389
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;
|
|
390
395
|
runningWorkflowMap = new Map(); // Map from workflowID to workflow promise, queue name and partition key
|
|
391
|
-
|
|
392
|
-
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) {
|
|
393
397
|
this.systemDatabaseUrl = systemDatabaseUrl;
|
|
394
398
|
this.logger = logger;
|
|
395
399
|
this.serializer = serializer;
|
|
@@ -408,6 +412,11 @@ class SystemDatabase {
|
|
|
408
412
|
};
|
|
409
413
|
this.pool = new pg_1.Pool(systemPoolConfig);
|
|
410
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);
|
|
411
420
|
this.pool.on('error', (err) => {
|
|
412
421
|
this.logger.warn(`Unexpected error in pool: ${err}`);
|
|
413
422
|
});
|
|
@@ -718,23 +727,11 @@ class SystemDatabase {
|
|
|
718
727
|
completed_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint
|
|
719
728
|
WHERE workflow_uuid = ANY($2)
|
|
720
729
|
AND status NOT IN ($3, $4)`, [workflow_1.StatusString.CANCELLED, workflowIDs, workflow_1.StatusString.SUCCESS, workflow_1.StatusString.ERROR]);
|
|
721
|
-
for (const workflowID of workflowIDs) {
|
|
722
|
-
this.#setWFCancelMap(workflowID);
|
|
723
|
-
}
|
|
724
730
|
}
|
|
725
731
|
async checkIfCanceled(workflowID) {
|
|
726
|
-
|
|
727
|
-
try {
|
|
728
|
-
await this.#checkIfCanceled(client, workflowID);
|
|
729
|
-
}
|
|
730
|
-
finally {
|
|
731
|
-
client.release();
|
|
732
|
-
}
|
|
732
|
+
await this.#checkIfCanceled(this.pool, workflowID);
|
|
733
733
|
}
|
|
734
734
|
async resumeWorkflows(workflowIDs, queueName) {
|
|
735
|
-
for (const workflowID of workflowIDs) {
|
|
736
|
-
this.#clearWFCancelMap(workflowID);
|
|
737
|
-
}
|
|
738
735
|
await this.pool.query(`UPDATE "${this.schemaName}".workflow_status
|
|
739
736
|
SET status = $1, queue_name = $2, recovery_attempts = 0,
|
|
740
737
|
workflow_deadline_epoch_ms = NULL, deduplication_id = NULL,
|
|
@@ -789,7 +786,6 @@ class SystemDatabase {
|
|
|
789
786
|
await this.pool.query(`DELETE FROM "${this.schemaName}".workflow_status WHERE workflow_uuid = ANY($1)`, [allIds]);
|
|
790
787
|
for (const wfid of allIds) {
|
|
791
788
|
this.runningWorkflowMap.delete(wfid);
|
|
792
|
-
this.workflowCancellationMap.delete(wfid);
|
|
793
789
|
}
|
|
794
790
|
}
|
|
795
791
|
async forkWorkflow(workflowID, startStep, options = {}) {
|
|
@@ -1152,7 +1148,6 @@ class SystemDatabase {
|
|
|
1152
1148
|
.finally(() => {
|
|
1153
1149
|
// Remove itself from pending workflow map.
|
|
1154
1150
|
this.runningWorkflowMap.delete(workflowID);
|
|
1155
|
-
this.workflowCancellationMap.delete(workflowID);
|
|
1156
1151
|
});
|
|
1157
1152
|
this.runningWorkflowMap.set(workflowID, {
|
|
1158
1153
|
promise: awaitWorkflowPromise,
|
|
@@ -1185,151 +1180,110 @@ class SystemDatabase {
|
|
|
1185
1180
|
//throw new Error('Message notification map is not empty - shutdown is not clean.');
|
|
1186
1181
|
}
|
|
1187
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
|
+
}
|
|
1188
1200
|
async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID, pollingIntervalMs) {
|
|
1189
1201
|
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
|
1190
1202
|
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
|
1191
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
|
+
}
|
|
1192
1209
|
while (true) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
resolveNotification = resolve;
|
|
1196
|
-
});
|
|
1197
|
-
const irh = this.cancelWakeupMap.registerCallback(workflowID, (_res) => {
|
|
1198
|
-
resolveNotification();
|
|
1199
|
-
});
|
|
1200
|
-
const crh = callerID
|
|
1201
|
-
? this.cancelWakeupMap.registerCallback(callerID, (_res) => {
|
|
1202
|
-
resolveNotification();
|
|
1203
|
-
})
|
|
1204
|
-
: undefined;
|
|
1210
|
+
if (callerID)
|
|
1211
|
+
await this.#checkIfCanceledLimited(callerID);
|
|
1205
1212
|
try {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
else {
|
|
1226
|
-
// Status is not actionable
|
|
1227
|
-
}
|
|
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]));
|
|
1215
|
+
if (rows.length > 0) {
|
|
1216
|
+
const status = rows[0].status;
|
|
1217
|
+
if (status === workflow_1.StatusString.SUCCESS) {
|
|
1218
|
+
return { output: rows[0].output, serialization: rows[0].serialization };
|
|
1219
|
+
}
|
|
1220
|
+
else if (status === workflow_1.StatusString.ERROR) {
|
|
1221
|
+
return { error: rows[0].error, serialization: rows[0].serialization };
|
|
1222
|
+
}
|
|
1223
|
+
else if (status === workflow_1.StatusString.CANCELLED) {
|
|
1224
|
+
return { cancelled: true };
|
|
1225
|
+
}
|
|
1226
|
+
else if (status === workflow_1.StatusString.MAX_RECOVERY_ATTEMPTS_EXCEEDED) {
|
|
1227
|
+
return { maxRecoveryAttemptsExceeded: true };
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
// Status is not actionable
|
|
1228
1231
|
}
|
|
1229
|
-
}
|
|
1230
|
-
catch (e) {
|
|
1231
|
-
const err = e;
|
|
1232
|
-
this.logger.error(`Exception from system database: ${err}`, err);
|
|
1233
|
-
throw err;
|
|
1234
|
-
}
|
|
1235
|
-
const ct = Date.now();
|
|
1236
|
-
if (finishTime && ct > finishTime)
|
|
1237
|
-
return undefined; // Time's up
|
|
1238
|
-
let timeoutPromise = Promise.resolve();
|
|
1239
|
-
let timeoutCancel = () => { };
|
|
1240
|
-
if (timerFuncID !== undefined && callerID !== undefined && timeoutms !== undefined) {
|
|
1241
|
-
const { promise, cancel, endTime } = await this.#durableSleep(callerID, timerFuncID, timeoutms, pollIntervalMs);
|
|
1242
|
-
finishTime = endTime;
|
|
1243
|
-
timeoutPromise = promise;
|
|
1244
|
-
timeoutCancel = cancel;
|
|
1245
|
-
}
|
|
1246
|
-
else {
|
|
1247
|
-
let poll = finishTime ? finishTime - ct : pollIntervalMs;
|
|
1248
|
-
poll = Math.min(pollIntervalMs, poll);
|
|
1249
|
-
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
|
1250
|
-
timeoutPromise = promise;
|
|
1251
|
-
timeoutCancel = cancel;
|
|
1252
|
-
}
|
|
1253
|
-
try {
|
|
1254
|
-
await Promise.race([statusPromise, timeoutPromise]);
|
|
1255
|
-
}
|
|
1256
|
-
finally {
|
|
1257
|
-
timeoutCancel();
|
|
1258
1232
|
}
|
|
1259
1233
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1234
|
+
catch (e) {
|
|
1235
|
+
const err = e;
|
|
1236
|
+
this.logger.error(`Exception from system database: ${err}`, err);
|
|
1237
|
+
throw err;
|
|
1264
1238
|
}
|
|
1239
|
+
const ct = Date.now();
|
|
1240
|
+
if (finishTime && ct > finishTime)
|
|
1241
|
+
return undefined; // Time's up
|
|
1242
|
+
let poll = finishTime ? finishTime - Date.now() : pollIntervalMs;
|
|
1243
|
+
poll = Math.min(pollIntervalMs, poll);
|
|
1244
|
+
await (0, utils_1.sleepms)(poll);
|
|
1265
1245
|
}
|
|
1266
1246
|
}
|
|
1267
1247
|
async awaitFirstWorkflowId(workflowIds, callerID, pollingIntervalMs) {
|
|
1268
1248
|
const placeholders = workflowIds.map((_, i) => `$${i + 1}`).join(', ');
|
|
1269
1249
|
const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalResultMs;
|
|
1270
1250
|
while (true) {
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
});
|
|
1275
|
-
// Register cancel callbacks for all target workflows and the caller.
|
|
1276
|
-
const cbHandles = workflowIds.map((wfid) => this.cancelWakeupMap.registerCallback(wfid, () => resolveNotification()));
|
|
1277
|
-
const callerCbHandle = callerID
|
|
1278
|
-
? this.cancelWakeupMap.registerCallback(callerID, () => resolveNotification())
|
|
1279
|
-
: undefined;
|
|
1280
|
-
try {
|
|
1281
|
-
if (callerID)
|
|
1282
|
-
await this.checkIfCanceled(callerID);
|
|
1283
|
-
const { rows } = await this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status
|
|
1251
|
+
if (callerID)
|
|
1252
|
+
await this.#checkIfCanceledLimited(callerID);
|
|
1253
|
+
const { rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status
|
|
1284
1254
|
WHERE workflow_uuid IN (${placeholders})
|
|
1285
1255
|
AND status NOT IN ('${workflow_1.StatusString.PENDING}', '${workflow_1.StatusString.ENQUEUED}', '${workflow_1.StatusString.DELAYED}')
|
|
1286
|
-
LIMIT 1`, workflowIds);
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}
|
|
1290
|
-
const { promise: sleepPromise, cancel: sleepCancel } = (0, utils_1.cancellableSleep)(pollIntervalMs);
|
|
1291
|
-
try {
|
|
1292
|
-
await Promise.race([wakeupPromise, sleepPromise]);
|
|
1293
|
-
}
|
|
1294
|
-
finally {
|
|
1295
|
-
sleepCancel();
|
|
1296
|
-
}
|
|
1256
|
+
LIMIT 1`, workflowIds));
|
|
1257
|
+
if (rows.length > 0) {
|
|
1258
|
+
return rows[0].workflow_uuid;
|
|
1297
1259
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1260
|
+
await (0, utils_1.sleepms)(pollIntervalMs);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async awaitWorkflowIds(workflowIds, callerID, pollingIntervalMs) {
|
|
1264
|
+
const remainingWorkflowIds = new Set(workflowIds);
|
|
1265
|
+
const pollIntervalMs = pollingIntervalMs ?? this.dbPollingIntervalResultMs;
|
|
1266
|
+
while (remainingWorkflowIds.size > 0) {
|
|
1267
|
+
const currentWorkflowIds = [...remainingWorkflowIds];
|
|
1268
|
+
if (callerID)
|
|
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]));
|
|
1273
|
+
for (const row of rows) {
|
|
1274
|
+
remainingWorkflowIds.delete(row.workflow_uuid);
|
|
1275
|
+
}
|
|
1276
|
+
if (remainingWorkflowIds.size === 0) {
|
|
1277
|
+
return;
|
|
1304
1278
|
}
|
|
1279
|
+
await (0, utils_1.sleepms)(pollIntervalMs);
|
|
1305
1280
|
}
|
|
1306
1281
|
}
|
|
1307
1282
|
// ==================== Sleep ====================
|
|
1308
1283
|
async durableSleepms(workflowID, functionID, durationMS) {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
resolveNotification = () => {
|
|
1313
|
-
cancelled = true;
|
|
1314
|
-
resolve();
|
|
1315
|
-
};
|
|
1316
|
-
});
|
|
1317
|
-
const cbr = this.cancelWakeupMap.registerCallback(workflowID, resolveNotification);
|
|
1318
|
-
try {
|
|
1319
|
-
const { cancel: cancelInitial, endTime } = await this.#durableSleep(workflowID, functionID, durationMS);
|
|
1320
|
-
cancelInitial();
|
|
1321
|
-
while (!cancelled && Date.now() < endTime) {
|
|
1322
|
-
const { promise, cancel } = (0, utils_1.cancellableSleep)(Math.min(endTime - Date.now(), utils_1.sleepConfig.maxTimeoutMS));
|
|
1323
|
-
try {
|
|
1324
|
-
await Promise.race([cancelPromise, promise]);
|
|
1325
|
-
}
|
|
1326
|
-
finally {
|
|
1327
|
-
cancel();
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
finally {
|
|
1332
|
-
this.cancelWakeupMap.deregisterCallback(cbr);
|
|
1284
|
+
const endTime = await this.#durableSleep(workflowID, functionID, durationMS);
|
|
1285
|
+
while (Date.now() < endTime) {
|
|
1286
|
+
await (0, utils_1.sleepms)(Math.min(endTime - Date.now(), utils_1.sleepConfig.maxTimeoutMS));
|
|
1333
1287
|
}
|
|
1334
1288
|
await this.checkIfCanceled(workflowID);
|
|
1335
1289
|
}
|
|
@@ -1394,6 +1348,11 @@ class SystemDatabase {
|
|
|
1394
1348
|
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
|
1395
1349
|
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
|
1396
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
|
+
}
|
|
1397
1356
|
while (true) {
|
|
1398
1357
|
// register the key with the global notifications listener.
|
|
1399
1358
|
let resolveNotification;
|
|
@@ -1402,43 +1361,27 @@ class SystemDatabase {
|
|
|
1402
1361
|
});
|
|
1403
1362
|
const payload = `${workflowID}::${topic}`;
|
|
1404
1363
|
const cbr = this.notificationsMap.registerCallback(payload, resolveNotification);
|
|
1405
|
-
const crh = this.cancelWakeupMap.registerCallback(workflowID, (_res) => {
|
|
1406
|
-
resolveNotification();
|
|
1407
|
-
});
|
|
1408
1364
|
try {
|
|
1409
|
-
await this
|
|
1365
|
+
await this.#checkIfCanceledLimited(workflowID);
|
|
1410
1366
|
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
|
1411
|
-
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;
|
|
1412
1368
|
if (initRecvRows.length !== 0)
|
|
1413
1369
|
break;
|
|
1414
1370
|
const ct = Date.now();
|
|
1415
1371
|
if (finishTime && ct > finishTime)
|
|
1416
1372
|
break; // Time's up
|
|
1417
|
-
let
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
const { promise, cancel, endTime } = await this.#durableSleep(workflowID, timeoutFunctionID, timeoutms, pollIntervalMs);
|
|
1421
|
-
timeoutPromise = promise;
|
|
1422
|
-
timeoutCancel = cancel;
|
|
1423
|
-
finishTime = endTime;
|
|
1424
|
-
}
|
|
1425
|
-
else {
|
|
1426
|
-
let poll = finishTime ? finishTime - ct : pollIntervalMs;
|
|
1427
|
-
poll = Math.min(pollIntervalMs, poll);
|
|
1428
|
-
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
|
1429
|
-
timeoutPromise = promise;
|
|
1430
|
-
timeoutCancel = cancel;
|
|
1431
|
-
}
|
|
1373
|
+
let poll = finishTime ? finishTime - Date.now() : pollIntervalMs;
|
|
1374
|
+
poll = Math.min(pollIntervalMs, poll);
|
|
1375
|
+
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
|
1432
1376
|
try {
|
|
1433
|
-
await Promise.race([messagePromise,
|
|
1377
|
+
await Promise.race([messagePromise, promise]);
|
|
1434
1378
|
}
|
|
1435
1379
|
finally {
|
|
1436
|
-
|
|
1380
|
+
cancel();
|
|
1437
1381
|
}
|
|
1438
1382
|
}
|
|
1439
1383
|
finally {
|
|
1440
1384
|
this.notificationsMap.deregisterCallback(cbr);
|
|
1441
|
-
this.cancelWakeupMap.deregisterCallback(crh);
|
|
1442
1385
|
}
|
|
1443
1386
|
}
|
|
1444
1387
|
await this.checkIfCanceled(workflowID);
|
|
@@ -1531,6 +1474,12 @@ class SystemDatabase {
|
|
|
1531
1474
|
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
|
1532
1475
|
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
|
1533
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
|
+
}
|
|
1534
1483
|
// Register the key with the global notifications listener first... we do not want to look in the DB first
|
|
1535
1484
|
// or that would cause a timing hole.
|
|
1536
1485
|
while (true) {
|
|
@@ -1539,18 +1488,13 @@ class SystemDatabase {
|
|
|
1539
1488
|
resolveNotification = resolve;
|
|
1540
1489
|
});
|
|
1541
1490
|
const cbr = this.workflowEventsMap.registerCallback(payloadKey, resolveNotification);
|
|
1542
|
-
const crh = callerWorkflow?.workflowID
|
|
1543
|
-
? this.cancelWakeupMap.registerCallback(callerWorkflow.workflowID, (_res) => {
|
|
1544
|
-
resolveNotification();
|
|
1545
|
-
})
|
|
1546
|
-
: undefined;
|
|
1547
1491
|
try {
|
|
1548
1492
|
if (callerWorkflow?.workflowID)
|
|
1549
|
-
await this
|
|
1493
|
+
await this.#checkIfCanceledLimited(callerWorkflow?.workflowID);
|
|
1550
1494
|
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
|
1551
|
-
const initRecvRows = (await this.pool.query(`SELECT key, value, serialization
|
|
1495
|
+
const initRecvRows = (await this.#pollWithLimiter(() => this.pool.query(`SELECT key, value, serialization
|
|
1552
1496
|
FROM "${this.schemaName}".workflow_events
|
|
1553
|
-
WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
|
|
1497
|
+
WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key]))).rows;
|
|
1554
1498
|
if (initRecvRows.length > 0) {
|
|
1555
1499
|
value = initRecvRows[0].value;
|
|
1556
1500
|
valueSer = initRecvRows[0].serialization;
|
|
@@ -1559,33 +1503,18 @@ class SystemDatabase {
|
|
|
1559
1503
|
const ct = Date.now();
|
|
1560
1504
|
if (finishTime && ct > finishTime)
|
|
1561
1505
|
break; // Time's up
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
if (callerWorkflow && timeoutms) {
|
|
1566
|
-
const { promise, cancel, endTime } = await this.#durableSleep(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutms, pollIntervalMs);
|
|
1567
|
-
timeoutPromise = promise;
|
|
1568
|
-
timeoutCancel = cancel;
|
|
1569
|
-
finishTime = endTime;
|
|
1570
|
-
}
|
|
1571
|
-
else {
|
|
1572
|
-
let poll = finishTime ? finishTime - ct : pollIntervalMs;
|
|
1573
|
-
poll = Math.min(pollIntervalMs, poll);
|
|
1574
|
-
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
|
1575
|
-
timeoutPromise = promise;
|
|
1576
|
-
timeoutCancel = cancel;
|
|
1577
|
-
}
|
|
1506
|
+
let poll = finishTime ? finishTime - Date.now() : pollIntervalMs;
|
|
1507
|
+
poll = Math.min(pollIntervalMs, poll);
|
|
1508
|
+
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
|
1578
1509
|
try {
|
|
1579
|
-
await Promise.race([valuePromise,
|
|
1510
|
+
await Promise.race([valuePromise, promise]);
|
|
1580
1511
|
}
|
|
1581
1512
|
finally {
|
|
1582
|
-
|
|
1513
|
+
cancel();
|
|
1583
1514
|
}
|
|
1584
1515
|
}
|
|
1585
1516
|
finally {
|
|
1586
1517
|
this.workflowEventsMap.deregisterCallback(cbr);
|
|
1587
|
-
if (crh)
|
|
1588
|
-
this.cancelWakeupMap.deregisterCallback(crh);
|
|
1589
1518
|
}
|
|
1590
1519
|
}
|
|
1591
1520
|
// Record the output if it is inside a workflow.
|
|
@@ -2861,31 +2790,18 @@ class SystemDatabase {
|
|
|
2861
2790
|
});
|
|
2862
2791
|
return output;
|
|
2863
2792
|
}
|
|
2864
|
-
#setWFCancelMap(workflowID) {
|
|
2865
|
-
if (this.runningWorkflowMap.has(workflowID)) {
|
|
2866
|
-
this.workflowCancellationMap.set(workflowID, true);
|
|
2867
|
-
}
|
|
2868
|
-
this.cancelWakeupMap.callCallbacks(workflowID);
|
|
2869
|
-
}
|
|
2870
|
-
#clearWFCancelMap(workflowID) {
|
|
2871
|
-
if (this.workflowCancellationMap.has(workflowID)) {
|
|
2872
|
-
this.workflowCancellationMap.delete(workflowID);
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
2793
|
async #checkIfCanceled(client, workflowID) {
|
|
2876
|
-
if (this.workflowCancellationMap.get(workflowID) === true) {
|
|
2877
|
-
throw new error_1.DBOSWorkflowCancelledError(workflowID);
|
|
2878
|
-
}
|
|
2879
2794
|
const statusValue = await this.getWorkflowStatusValue(client, workflowID);
|
|
2880
2795
|
if (statusValue === workflow_1.StatusString.CANCELLED) {
|
|
2881
2796
|
throw new error_1.DBOSWorkflowCancelledError(workflowID);
|
|
2882
2797
|
}
|
|
2883
2798
|
}
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2799
|
+
// Durably records (or, on recovery, reads back) the wakeup deadline for a sleep or
|
|
2800
|
+
// timeout so it survives recovery. Returns the absolute end time in epoch ms; the
|
|
2801
|
+
// caller is responsible for actually waiting until then. Throws if the workflow has
|
|
2802
|
+
// been cancelled.
|
|
2803
|
+
async #durableSleep(workflowID, functionID, durationMS) {
|
|
2804
|
+
const endTimeMs = Date.now() + durationMS;
|
|
2889
2805
|
const client = await this.pool.connect();
|
|
2890
2806
|
try {
|
|
2891
2807
|
const res = await this.#getOperationResultAndThrowIfCancelled(client, workflowID, functionID);
|
|
@@ -2893,18 +2809,13 @@ class SystemDatabase {
|
|
|
2893
2809
|
if (res.functionName !== exports.DBOS_FUNCNAME_SLEEP) {
|
|
2894
2810
|
throw new error_1.DBOSUnexpectedStepError(workflowID, functionID, exports.DBOS_FUNCNAME_SLEEP, res.functionName);
|
|
2895
2811
|
}
|
|
2896
|
-
|
|
2897
|
-
}
|
|
2898
|
-
else {
|
|
2899
|
-
await this.recordOperationResultInternal(client, workflowID, functionID, exports.DBOS_FUNCNAME_SLEEP, false, Date.now(), Date.now(), {
|
|
2900
|
-
output: serialization_1.DBOSPortableJSON.stringify(endTimeMs),
|
|
2901
|
-
serialization: serialization_1.DBOSPortableJSON.name(),
|
|
2902
|
-
});
|
|
2812
|
+
return JSON.parse(res.output);
|
|
2903
2813
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
};
|
|
2814
|
+
await this.recordOperationResultInternal(client, workflowID, functionID, exports.DBOS_FUNCNAME_SLEEP, false, Date.now(), Date.now(), {
|
|
2815
|
+
output: serialization_1.DBOSPortableJSON.stringify(endTimeMs),
|
|
2816
|
+
serialization: serialization_1.DBOSPortableJSON.name(),
|
|
2817
|
+
});
|
|
2818
|
+
return endTimeMs;
|
|
2908
2819
|
}
|
|
2909
2820
|
finally {
|
|
2910
2821
|
client.release();
|
|
@@ -3051,6 +2962,12 @@ __decorate([
|
|
|
3051
2962
|
__metadata("design:paramtypes", [Array, String, Number]),
|
|
3052
2963
|
__metadata("design:returntype", Promise)
|
|
3053
2964
|
], SystemDatabase.prototype, "awaitFirstWorkflowId", null);
|
|
2965
|
+
__decorate([
|
|
2966
|
+
dbRetry(),
|
|
2967
|
+
__metadata("design:type", Function),
|
|
2968
|
+
__metadata("design:paramtypes", [Array, String, Number]),
|
|
2969
|
+
__metadata("design:returntype", Promise)
|
|
2970
|
+
], SystemDatabase.prototype, "awaitWorkflowIds", null);
|
|
3054
2971
|
__decorate([
|
|
3055
2972
|
dbRetry(),
|
|
3056
2973
|
__metadata("design:type", Function),
|