@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.
@@ -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
- workflowCancellationMap = new Map(); // Map from workflowID to its cancellation status.
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
- const client = await this.pool.connect();
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
- let resolveNotification;
1194
- const statusPromise = new Promise((resolve) => {
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
- if (callerID)
1207
- await this.checkIfCanceled(callerID);
1208
- try {
1209
- const { rows } = await this.pool.query(`SELECT status, output, error, serialization FROM "${this.schemaName}".workflow_status
1210
- WHERE workflow_uuid=$1`, [workflowID]);
1211
- if (rows.length > 0) {
1212
- const status = rows[0].status;
1213
- if (status === workflow_1.StatusString.SUCCESS) {
1214
- return { output: rows[0].output, serialization: rows[0].serialization };
1215
- }
1216
- else if (status === workflow_1.StatusString.ERROR) {
1217
- return { error: rows[0].error, serialization: rows[0].serialization };
1218
- }
1219
- else if (status === workflow_1.StatusString.CANCELLED) {
1220
- return { cancelled: true };
1221
- }
1222
- else if (status === workflow_1.StatusString.MAX_RECOVERY_ATTEMPTS_EXCEEDED) {
1223
- return { maxRecoveryAttemptsExceeded: true };
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
- finally {
1261
- this.cancelWakeupMap.deregisterCallback(irh);
1262
- if (crh)
1263
- this.cancelWakeupMap.deregisterCallback(crh);
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
- let resolveNotification;
1272
- const wakeupPromise = new Promise((resolve) => {
1273
- resolveNotification = resolve;
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
- if (rows.length > 0) {
1288
- return rows[0].workflow_uuid;
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
- finally {
1299
- for (const h of cbHandles) {
1300
- this.cancelWakeupMap.deregisterCallback(h);
1301
- }
1302
- if (callerCbHandle)
1303
- this.cancelWakeupMap.deregisterCallback(callerCbHandle);
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
- let cancelled = false;
1310
- let resolveNotification;
1311
- const cancelPromise = new Promise((resolve) => {
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.checkIfCanceled(workflowID);
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 timeoutPromise = Promise.resolve();
1418
- let timeoutCancel = () => { };
1419
- if (timeoutms) {
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, timeoutPromise]);
1377
+ await Promise.race([messagePromise, promise]);
1434
1378
  }
1435
1379
  finally {
1436
- timeoutCancel();
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.checkIfCanceled(callerWorkflow?.workflowID);
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
- // If we have a callerWorkflow, we want a durable sleep, otherwise, not
1563
- let timeoutPromise = Promise.resolve();
1564
- let timeoutCancel = () => { };
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, timeoutPromise]);
1510
+ await Promise.race([valuePromise, promise]);
1580
1511
  }
1581
1512
  finally {
1582
- timeoutCancel();
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
- async #durableSleep(workflowID, functionID, durationMS, maxSleepPerIteration) {
2885
- if (maxSleepPerIteration === undefined)
2886
- maxSleepPerIteration = durationMS;
2887
- const curTime = Date.now();
2888
- let endTimeMs = curTime + durationMS;
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
- endTimeMs = JSON.parse(res.output);
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
- return {
2905
- ...(0, utils_1.cancellableSleep)(Math.max(Math.min(maxSleepPerIteration, endTimeMs - curTime), 0)),
2906
- endTime: endTimeMs,
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),