@dbos-inc/dbos-sdk 2.8.17-preview → 2.8.41-preview.gb44af319d0

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.
Files changed (41) hide show
  1. package/dbos-config.schema.json +0 -4
  2. package/dist/dbos-config.schema.json +0 -4
  3. package/dist/src/client.d.ts.map +1 -1
  4. package/dist/src/client.js +9 -13
  5. package/dist/src/client.js.map +1 -1
  6. package/dist/src/dbos-executor.d.ts +1 -3
  7. package/dist/src/dbos-executor.d.ts.map +1 -1
  8. package/dist/src/dbos-executor.js +32 -91
  9. package/dist/src/dbos-executor.js.map +1 -1
  10. package/dist/src/dbos-runtime/config.d.ts +24 -2
  11. package/dist/src/dbos-runtime/config.d.ts.map +1 -1
  12. package/dist/src/dbos-runtime/config.js +151 -56
  13. package/dist/src/dbos-runtime/config.js.map +1 -1
  14. package/dist/src/dbos-runtime/migrate.js +7 -4
  15. package/dist/src/dbos-runtime/migrate.js.map +1 -1
  16. package/dist/src/dbos-runtime/workflow_management.d.ts.map +1 -1
  17. package/dist/src/dbos-runtime/workflow_management.js +2 -1
  18. package/dist/src/dbos-runtime/workflow_management.js.map +1 -1
  19. package/dist/src/dbos.d.ts +8 -1
  20. package/dist/src/dbos.d.ts.map +1 -1
  21. package/dist/src/dbos.js +28 -8
  22. package/dist/src/dbos.js.map +1 -1
  23. package/dist/src/error.d.ts +11 -6
  24. package/dist/src/error.d.ts.map +1 -1
  25. package/dist/src/error.js +27 -16
  26. package/dist/src/error.js.map +1 -1
  27. package/dist/src/scheduler/scheduler.js +1 -1
  28. package/dist/src/scheduler/scheduler.js.map +1 -1
  29. package/dist/src/system_database.d.ts +48 -16
  30. package/dist/src/system_database.d.ts.map +1 -1
  31. package/dist/src/system_database.js +323 -105
  32. package/dist/src/system_database.js.map +1 -1
  33. package/dist/src/user_database.d.ts.map +1 -1
  34. package/dist/src/user_database.js +8 -2
  35. package/dist/src/user_database.js.map +1 -1
  36. package/dist/src/workflow.d.ts.map +1 -1
  37. package/dist/src/workflow.js +7 -38
  38. package/dist/src/workflow.js.map +1 -1
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/migrations/20250415134400_triggers_wfstatus.js +43 -0
  41. package/package.json +1 -1
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
4
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
4
  };
@@ -40,6 +39,37 @@ async function migrateSystemDatabase(systemPoolConfig, logger) {
40
39
  }
41
40
  }
42
41
  exports.migrateSystemDatabase = migrateSystemDatabase;
42
+ class NotificationMap {
43
+ map = new Map();
44
+ curCK = 0;
45
+ registerCallback(key, cb) {
46
+ if (!this.map.has(key)) {
47
+ this.map.set(key, new Map());
48
+ }
49
+ const ck = this.curCK++;
50
+ this.map.get(key).set(ck, cb);
51
+ return { key, ck };
52
+ }
53
+ deregisterCallback(k) {
54
+ if (!this.map.has(k.key))
55
+ return;
56
+ const sm = this.map.get(k.key);
57
+ if (!sm.has(k.ck))
58
+ return;
59
+ sm.delete(k.ck);
60
+ if (sm.size === 0) {
61
+ this.map.delete(k.key);
62
+ }
63
+ }
64
+ callCallbacks(key, event) {
65
+ if (!this.map.has(key))
66
+ return;
67
+ const sm = this.map.get(key);
68
+ for (const cb of sm.values()) {
69
+ cb(event);
70
+ }
71
+ }
72
+ }
43
73
  class PostgresSystemDatabase {
44
74
  pgPoolConfig;
45
75
  systemDatabaseName;
@@ -48,20 +78,46 @@ class PostgresSystemDatabase {
48
78
  pool;
49
79
  systemPoolConfig;
50
80
  knexDB;
81
+ /*
82
+ * Generally, notifications are asynchronous. One should:
83
+ * Subscribe to updates
84
+ * Read the database item in question
85
+ * In response to updates, re-read the database item
86
+ * Unsubscribe at the end
87
+ * The notification mechanism is reliable in the sense that it will eventually deliver updates
88
+ * or the DB connection will get dropped. The right thing to do if you lose connectivity to
89
+ * the system DB is to exit the process and go through recovery... system DB writes, notifications,
90
+ * etc may not have completed correctly, and recovery is the way to rebuild in-memory state.
91
+ *
92
+ * NOTE:
93
+ * PG Notifications are not fully reliable.
94
+ * Dropped connections are recoverable - you just need to restart and scan everything.
95
+ * (The whole VM being the logical choice, so workflows can recover from any write failures.)
96
+ * The real problem is, if the pipes out of the server are full... then notifications can be
97
+ * dropped, and only the PG server log may note it. For those reasons, we do occasional polling
98
+ */
51
99
  notificationsClient = null;
52
- notificationsMap = {};
53
- workflowEventsMap = {};
54
- static connectionTimeoutMillis = 10000; // 10 second timeout
100
+ dbPollingIntervalMs = 10000;
101
+ shouldUseDBNotifications = true;
102
+ notificationsMap = new NotificationMap();
103
+ workflowEventsMap = new NotificationMap();
104
+ cancelWakeupMap = new NotificationMap();
105
+ workflowStatusMap = new NotificationMap();
106
+ runningWorkflowMap = new Map(); // Map from workflowID to workflow promise
107
+ workflowCancellationMap = new Map(); // Map from workflowID to its cancellation status.
55
108
  constructor(pgPoolConfig, systemDatabaseName, logger, sysDbPoolSize) {
56
109
  this.pgPoolConfig = pgPoolConfig;
57
110
  this.systemDatabaseName = systemDatabaseName;
58
111
  this.logger = logger;
59
112
  this.sysDbPoolSize = sysDbPoolSize;
60
- this.systemPoolConfig = { ...pgPoolConfig };
61
- this.systemPoolConfig.database = systemDatabaseName;
62
- this.systemPoolConfig.connectionTimeoutMillis = PostgresSystemDatabase.connectionTimeoutMillis;
63
- // This sets the application_name column in pg_stat_activity
64
- this.systemPoolConfig.application_name = `dbos_transact_${utils_1.globalParams.executorID}_${utils_1.globalParams.appVersion}`;
113
+ // Craft a db string from the app db string, replacing the database name:
114
+ const systemDbConnectionString = new URL(pgPoolConfig.connectionString);
115
+ systemDbConnectionString.pathname = `/${systemDatabaseName}`;
116
+ this.systemPoolConfig = {
117
+ connectionString: systemDbConnectionString.toString(),
118
+ // This sets the application_name column in pg_stat_activity
119
+ application_name: `dbos_transact_${utils_1.globalParams.executorID}_${utils_1.globalParams.appVersion}`,
120
+ };
65
121
  this.pool = new pg_1.Pool(this.systemPoolConfig);
66
122
  const knexConfig = {
67
123
  client: 'pg',
@@ -97,7 +153,9 @@ class PostgresSystemDatabase {
97
153
  finally {
98
154
  await pgSystemClient.end();
99
155
  }
100
- await this.listenForNotifications();
156
+ if (this.shouldUseDBNotifications) {
157
+ await this.listenForNotifications();
158
+ }
101
159
  }
102
160
  async destroy() {
103
161
  await this.knexDB.destroy();
@@ -120,7 +178,7 @@ class PostgresSystemDatabase {
120
178
  await pgSystemClient.query(`DROP DATABASE IF EXISTS ${dbosConfig.system_database};`);
121
179
  await pgSystemClient.end();
122
180
  }
123
- async initWorkflowStatus(initStatus, args) {
181
+ async initWorkflowStatus(initStatus, serializedInputs) {
124
182
  const result = await this.pool.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status (
125
183
  workflow_uuid,
126
184
  status,
@@ -154,8 +212,8 @@ class PostgresSystemDatabase {
154
212
  initStatus.queueName,
155
213
  initStatus.authenticatedUser,
156
214
  initStatus.assumedRole,
157
- utils_1.DBOSJSON.stringify(initStatus.authenticatedRoles),
158
- utils_1.DBOSJSON.stringify(initStatus.request),
215
+ JSON.stringify(initStatus.authenticatedRoles),
216
+ JSON.stringify(initStatus.request),
159
217
  null,
160
218
  initStatus.executorId,
161
219
  initStatus.applicationVersion,
@@ -197,12 +255,11 @@ class PostgresSystemDatabase {
197
255
  }
198
256
  this.logger.debug(`Workflow ${initStatus.workflowUUID} attempt number: ${attempts}.`);
199
257
  const status = resRow.status;
200
- const serializedInputs = utils_1.DBOSJSON.stringify(args);
201
258
  const { rows } = await this.pool.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_inputs (workflow_uuid, inputs) VALUES($1, $2) ON CONFLICT (workflow_uuid) DO UPDATE SET workflow_uuid = excluded.workflow_uuid RETURNING inputs`, [initStatus.workflowUUID, serializedInputs]);
202
259
  if (serializedInputs !== rows[0].inputs) {
203
260
  this.logger.warn(`Workflow inputs for ${initStatus.workflowUUID} changed since the first call! Use the original inputs.`);
204
261
  }
205
- return { args: utils_1.DBOSJSON.parse(rows[0].inputs), status };
262
+ return { serializedInputs: rows[0].inputs, status };
206
263
  }
207
264
  async recordWorkflowStatusChange(workflowID, status, update, client) {
208
265
  let rec = '';
@@ -215,7 +272,7 @@ class PostgresSystemDatabase {
215
272
  const wRes = await (client ?? this.pool).query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status
216
273
  SET ${rec} status=$2, output=$3, error=$4, updated_at=$5 WHERE workflow_uuid=$1`, [workflowID, status, update.output, update.error, Date.now()]);
217
274
  if (wRes.rowCount !== 1) {
218
- throw new error_1.DBOSWorkflowConflictUUIDError(`Attempt to record transition of nonexistent workflow ${workflowID}`);
275
+ throw new error_1.DBOSWorkflowConflictError(`Attempt to record transition of nonexistent workflow ${workflowID}`);
219
276
  }
220
277
  }
221
278
  async recordWorkflowOutput(workflowID, status) {
@@ -236,9 +293,10 @@ class PostgresSystemDatabase {
236
293
  if (rows.length === 0) {
237
294
  return null;
238
295
  }
239
- return utils_1.DBOSJSON.parse(rows[0].inputs);
296
+ return rows[0].inputs;
240
297
  }
241
298
  async getOperationResult(workflowID, functionID, client) {
299
+ await this.checkIfCanceled(workflowID);
242
300
  const { rows } = await (client ?? this.pool).query(`SELECT output, error, child_workflow_id, function_name
243
301
  FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs
244
302
  WHERE workflow_uuid=$1 AND function_id=$2`, [workflowID, functionID]);
@@ -279,7 +337,7 @@ class PostgresSystemDatabase {
279
337
  const err = error;
280
338
  if (err.code === '40001' || err.code === '23505') {
281
339
  // Serialization and primary key conflict (Postgres).
282
- throw new error_1.DBOSWorkflowConflictUUIDError(workflowID);
340
+ throw new error_1.DBOSWorkflowConflictError(workflowID);
283
341
  }
284
342
  else {
285
343
  throw err;
@@ -304,6 +362,30 @@ class PostgresSystemDatabase {
304
362
  return serialOutput;
305
363
  }
306
364
  async durableSleepms(workflowID, functionID, durationMS) {
365
+ let resolveNotification;
366
+ const cancelPromise = new Promise((resolve) => {
367
+ resolveNotification = resolve;
368
+ });
369
+ const cbr = this.cancelWakeupMap.registerCallback(workflowID, resolveNotification);
370
+ try {
371
+ let timeoutPromise = Promise.resolve();
372
+ const { promise, cancel: timeoutCancel } = await this.durableSleepmsInternal(workflowID, functionID, durationMS);
373
+ timeoutPromise = promise;
374
+ try {
375
+ await Promise.race([cancelPromise, timeoutPromise]);
376
+ }
377
+ finally {
378
+ timeoutCancel();
379
+ }
380
+ }
381
+ finally {
382
+ this.cancelWakeupMap.deregisterCallback(cbr);
383
+ }
384
+ await this.checkIfCanceled(workflowID);
385
+ }
386
+ async durableSleepmsInternal(workflowID, functionID, durationMS, maxSleepPerIteration) {
387
+ if (maxSleepPerIteration === undefined)
388
+ maxSleepPerIteration = durationMS;
307
389
  const curTime = Date.now();
308
390
  let endTimeMs = curTime + durationMS;
309
391
  const res = await this.getOperationResult(workflowID, functionID);
@@ -316,7 +398,10 @@ class PostgresSystemDatabase {
316
398
  else {
317
399
  await this.recordOperationResult(workflowID, functionID, { serialOutput: JSON.stringify(endTimeMs), functionName: exports.DBOS_FUNCNAME_SLEEP }, false);
318
400
  }
319
- return (0, utils_1.cancellableSleep)(Math.max(endTimeMs - curTime, 0));
401
+ return {
402
+ ...(0, utils_1.cancellableSleep)(Math.max(Math.min(maxSleepPerIteration, endTimeMs - curTime), 0)),
403
+ endTime: endTimeMs,
404
+ };
320
405
  }
321
406
  nullTopic = '__null__topic__';
322
407
  async send(workflowID, functionID, destinationID, message, topic) {
@@ -355,37 +440,56 @@ class PostgresSystemDatabase {
355
440
  }
356
441
  return res.res.res;
357
442
  }
358
- // Check if the key is already in the DB, then wait for the notification if it isn't.
359
- const initRecvRows = (await this.pool.query(`SELECT topic FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.notifications WHERE destination_uuid=$1 AND topic=$2;`, [workflowID, topic])).rows;
360
- if (initRecvRows.length === 0) {
361
- // Then, register the key with the global notifications listener.
443
+ const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
444
+ let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
445
+ while (true) {
446
+ // register the key with the global notifications listener.
362
447
  let resolveNotification;
363
448
  const messagePromise = new Promise((resolve) => {
364
449
  resolveNotification = resolve;
365
450
  });
366
451
  const payload = `${workflowID}::${topic}`;
367
- this.notificationsMap[payload] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
368
- let timeoutPromise = Promise.resolve();
369
- let timeoutCancel = () => { };
370
- try {
371
- const { promise, cancel } = await this.durableSleepms(workflowID, timeoutFunctionID, timeoutSeconds * 1000);
372
- timeoutPromise = promise;
373
- timeoutCancel = cancel;
374
- }
375
- catch (e) {
376
- this.logger.error(e);
377
- delete this.notificationsMap[payload];
378
- timeoutCancel();
379
- throw new Error('durable sleepms failed');
380
- }
452
+ const cbr = this.notificationsMap.registerCallback(payload, resolveNotification);
453
+ const crh = this.cancelWakeupMap.registerCallback(workflowID, (_res) => {
454
+ resolveNotification();
455
+ });
381
456
  try {
382
- await Promise.race([messagePromise, timeoutPromise]);
457
+ await this.checkIfCanceled(workflowID);
458
+ // Check if the key is already in the DB, then wait for the notification if it isn't.
459
+ const initRecvRows = (await this.pool.query(`SELECT topic FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.notifications WHERE destination_uuid=$1 AND topic=$2;`, [workflowID, topic])).rows;
460
+ if (initRecvRows.length !== 0)
461
+ break;
462
+ const ct = Date.now();
463
+ if (finishTime && ct > finishTime)
464
+ break; // Time's up
465
+ let timeoutPromise = Promise.resolve();
466
+ let timeoutCancel = () => { };
467
+ if (timeoutms) {
468
+ const { promise, cancel, endTime } = await this.durableSleepmsInternal(workflowID, timeoutFunctionID, timeoutms, this.dbPollingIntervalMs);
469
+ timeoutPromise = promise;
470
+ timeoutCancel = cancel;
471
+ finishTime = endTime;
472
+ }
473
+ else {
474
+ let poll = finishTime ? finishTime - ct : this.dbPollingIntervalMs;
475
+ poll = Math.min(this.dbPollingIntervalMs, poll);
476
+ const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
477
+ timeoutPromise = promise;
478
+ timeoutCancel = cancel;
479
+ }
480
+ try {
481
+ await Promise.race([messagePromise, timeoutPromise]);
482
+ }
483
+ finally {
484
+ timeoutCancel();
485
+ }
383
486
  }
384
487
  finally {
385
- timeoutCancel();
386
- delete this.notificationsMap[payload];
488
+ this.notificationsMap.deregisterCallback(cbr);
489
+ this.cancelWakeupMap.deregisterCallback(crh);
387
490
  }
388
491
  }
492
+ await this.checkIfCanceled(workflowID);
389
493
  // Transactionally consume and return the message if it's in the DB, otherwise return null.
390
494
  let message = null;
391
495
  const client = await this.pool.connect();
@@ -459,40 +563,49 @@ class PostgresSystemDatabase {
459
563
  // Get the return the value. if it's in the DB, otherwise return null.
460
564
  let value = null;
461
565
  const payloadKey = `${workflowID}::${key}`;
566
+ const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
567
+ let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
462
568
  // Register the key with the global notifications listener first... we do not want to look in the DB first
463
569
  // or that would cause a timing hole.
464
- let resolveNotification;
465
- const valuePromise = new Promise((resolve) => {
466
- resolveNotification = resolve;
467
- });
468
- this.workflowEventsMap[payloadKey] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
469
- try {
470
- // Check if the key is already in the DB, then wait for the notification if it isn't.
471
- const initRecvRows = (await this.pool.query(`
472
- SELECT key, value
473
- FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
474
- WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
475
- if (initRecvRows.length > 0) {
476
- value = initRecvRows[0].value;
477
- }
478
- else {
570
+ while (true) {
571
+ let resolveNotification;
572
+ const valuePromise = new Promise((resolve) => {
573
+ resolveNotification = resolve;
574
+ });
575
+ const cbr = this.workflowEventsMap.registerCallback(payloadKey, resolveNotification);
576
+ const crh = callerWorkflow?.workflowID
577
+ ? this.cancelWakeupMap.registerCallback(callerWorkflow.workflowID, (_res) => {
578
+ resolveNotification();
579
+ })
580
+ : undefined;
581
+ try {
582
+ if (callerWorkflow?.workflowID)
583
+ await this.checkIfCanceled(callerWorkflow?.workflowID);
584
+ // Check if the key is already in the DB, then wait for the notification if it isn't.
585
+ const initRecvRows = (await this.pool.query(`
586
+ SELECT key, value
587
+ FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
588
+ WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
589
+ if (initRecvRows.length > 0) {
590
+ value = initRecvRows[0].value;
591
+ break;
592
+ }
593
+ const ct = Date.now();
594
+ if (finishTime && ct > finishTime)
595
+ break; // Time's up
479
596
  // If we have a callerWorkflow, we want a durable sleep, otherwise, not
480
597
  let timeoutPromise = Promise.resolve();
481
598
  let timeoutCancel = () => { };
482
- if (callerWorkflow) {
483
- try {
484
- const { promise, cancel } = await this.durableSleepms(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutSeconds * 1000);
485
- timeoutPromise = promise;
486
- timeoutCancel = cancel;
487
- }
488
- catch (e) {
489
- this.logger.error(e);
490
- delete this.workflowEventsMap[payloadKey];
491
- throw new Error('durable sleepms failed');
492
- }
599
+ if (callerWorkflow && timeoutms) {
600
+ const { promise, cancel, endTime } = await this.durableSleepmsInternal(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutms, this.dbPollingIntervalMs);
601
+ timeoutPromise = promise;
602
+ timeoutCancel = cancel;
603
+ finishTime = endTime;
493
604
  }
494
605
  else {
495
- const { promise, cancel } = (0, utils_1.cancellableSleep)(timeoutSeconds * 1000);
606
+ let poll = finishTime ? finishTime - ct : this.dbPollingIntervalMs;
607
+ poll = Math.min(this.dbPollingIntervalMs, poll);
608
+ const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
496
609
  timeoutPromise = promise;
497
610
  timeoutCancel = cancel;
498
611
  }
@@ -502,17 +615,12 @@ class PostgresSystemDatabase {
502
615
  finally {
503
616
  timeoutCancel();
504
617
  }
505
- const finalRecvRows = (await this.pool.query(`
506
- SELECT value
507
- FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
508
- WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
509
- if (finalRecvRows.length > 0) {
510
- value = finalRecvRows[0].value;
511
- }
512
618
  }
513
- }
514
- finally {
515
- delete this.workflowEventsMap[payloadKey];
619
+ finally {
620
+ this.workflowEventsMap.deregisterCallback(cbr);
621
+ if (crh)
622
+ this.cancelWakeupMap.deregisterCallback(crh);
623
+ }
516
624
  }
517
625
  // Record the output if it is inside a workflow.
518
626
  if (callerWorkflow) {
@@ -526,6 +634,17 @@ class PostgresSystemDatabase {
526
634
  async setWorkflowStatus(workflowID, status, resetRecoveryAttempts) {
527
635
  await this.recordWorkflowStatusChange(workflowID, status, { resetRecoveryAttempts });
528
636
  }
637
+ setWFCancelMap(workflowID) {
638
+ if (this.runningWorkflowMap.has(workflowID)) {
639
+ this.workflowCancellationMap.set(workflowID, true);
640
+ }
641
+ this.cancelWakeupMap.callCallbacks(workflowID);
642
+ }
643
+ clearWFCancelMap(workflowID) {
644
+ if (this.workflowCancellationMap.has(workflowID)) {
645
+ this.workflowCancellationMap.delete(workflowID);
646
+ }
647
+ }
529
648
  async cancelWorkflow(workflowID) {
530
649
  const client = await this.pool.connect();
531
650
  try {
@@ -544,6 +663,13 @@ class PostgresSystemDatabase {
544
663
  finally {
545
664
  client.release();
546
665
  }
666
+ this.setWFCancelMap(workflowID);
667
+ }
668
+ async checkIfCanceled(workflowID) {
669
+ if (this.workflowCancellationMap.get(workflowID) === true) {
670
+ throw new error_1.DBOSWorkflowCancelledError(workflowID);
671
+ }
672
+ return Promise.resolve();
547
673
  }
548
674
  async resumeWorkflow(workflowID) {
549
675
  const client = await this.pool.connect();
@@ -572,6 +698,38 @@ class PostgresSystemDatabase {
572
698
  finally {
573
699
  client.release();
574
700
  }
701
+ this.clearWFCancelMap(workflowID);
702
+ }
703
+ registerRunningWorkflow(workflowID, workflowPromise) {
704
+ // Need to await for the workflow and capture errors.
705
+ const awaitWorkflowPromise = workflowPromise
706
+ .catch((error) => {
707
+ this.logger.debug('Captured error in awaitWorkflowPromise: ' + error);
708
+ })
709
+ .finally(() => {
710
+ // Remove itself from pending workflow map.
711
+ this.runningWorkflowMap.delete(workflowID);
712
+ this.workflowCancellationMap.delete(workflowID);
713
+ });
714
+ this.runningWorkflowMap.set(workflowID, awaitWorkflowPromise);
715
+ }
716
+ async awaitRunningWorkflows() {
717
+ if (this.runningWorkflowMap.size > 0) {
718
+ this.logger.info('Waiting for pending workflows to finish.');
719
+ await Promise.allSettled(this.runningWorkflowMap.values());
720
+ }
721
+ if (this.workflowEventsMap.map.size > 0) {
722
+ this.logger.warn('Workflow events map is not empty - shutdown is not clean.');
723
+ //throw new Error('Workflow events map is not empty - shutdown is not clean.');
724
+ }
725
+ if (this.notificationsMap.map.size > 0) {
726
+ this.logger.warn('Message notification map is not empty - shutdown is not clean.');
727
+ //throw new Error('Message notification map is not empty - shutdown is not clean.');
728
+ }
729
+ if (this.workflowStatusMap.map.size > 0) {
730
+ this.logger.warn('Workflow status map is not empty - shutdown is not clean.');
731
+ //throw new Error('Workflow status map is not empty - shutdown is not clean.');
732
+ }
575
733
  }
576
734
  async getWorkflowStatus(workflowID, callerID, callerFN) {
577
735
  const internalStatus = await this.getWorkflowStatusInternal(workflowID, callerID, callerFN);
@@ -608,8 +766,8 @@ class PostgresSystemDatabase {
608
766
  queueName: rows[0].queue_name || undefined,
609
767
  authenticatedUser: rows[0].authenticated_user,
610
768
  assumedRole: rows[0].assumed_role,
611
- authenticatedRoles: utils_1.DBOSJSON.parse(rows[0].authenticated_roles),
612
- request: utils_1.DBOSJSON.parse(rows[0].request),
769
+ authenticatedRoles: JSON.parse(rows[0].authenticated_roles),
770
+ request: JSON.parse(rows[0].request),
613
771
  executorId: rows[0].executor_id,
614
772
  createdAt: Number(rows[0].created_at),
615
773
  updatedAt: Number(rows[0].updated_at),
@@ -623,34 +781,79 @@ class PostgresSystemDatabase {
623
781
  }, exports.DBOS_FUNCNAME_GETSTATUS, callerID, callerFN);
624
782
  return sv ? JSON.parse(sv) : null;
625
783
  }
626
- async awaitWorkflowResult(workflowID, timeoutms) {
627
- const pollingIntervalMs = 1000;
628
- const et = timeoutms !== undefined ? new Date().getTime() + timeoutms : undefined;
784
+ async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID) {
785
+ const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
786
+ let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
629
787
  while (true) {
630
- const { rows } = await this.pool.query(`SELECT status, output, error FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE workflow_uuid=$1`, [workflowID]);
631
- if (rows.length > 0) {
632
- const status = rows[0].status;
633
- if (status === workflow_1.StatusString.SUCCESS) {
634
- return { res: rows[0].output };
788
+ let resolveNotification;
789
+ const statusPromise = new Promise((resolve) => {
790
+ resolveNotification = resolve;
791
+ });
792
+ const irh = this.workflowStatusMap.registerCallback(workflowID, (_res) => {
793
+ resolveNotification();
794
+ });
795
+ const crh = callerID
796
+ ? this.cancelWakeupMap.registerCallback(callerID, (_res) => {
797
+ resolveNotification();
798
+ })
799
+ : undefined;
800
+ try {
801
+ if (callerID)
802
+ await this.checkIfCanceled(callerID);
803
+ try {
804
+ const { rows } = await this.pool.query(`SELECT status, output, error FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE workflow_uuid=$1`, [workflowID]);
805
+ if (rows.length > 0) {
806
+ const status = rows[0].status;
807
+ if (status === workflow_1.StatusString.SUCCESS) {
808
+ return { res: rows[0].output };
809
+ }
810
+ else if (status === workflow_1.StatusString.ERROR) {
811
+ return { err: rows[0].error };
812
+ }
813
+ else if (status === workflow_1.StatusString.CANCELLED) {
814
+ return { cancelled: true };
815
+ }
816
+ else {
817
+ // Status is not actionable
818
+ }
819
+ }
635
820
  }
636
- else if (status === workflow_1.StatusString.ERROR) {
637
- return { err: rows[0].error };
821
+ catch (e) {
822
+ const err = e;
823
+ this.logger.error(`Exception from system database: ${err}`);
824
+ throw err;
638
825
  }
639
- }
640
- if (et !== undefined) {
641
- const ct = new Date().getTime();
642
- if (et > ct) {
643
- await (0, utils_1.sleepms)(Math.min(pollingIntervalMs, et - ct));
826
+ const ct = Date.now();
827
+ if (finishTime && ct > finishTime)
828
+ return undefined; // Time's up
829
+ let timeoutPromise = Promise.resolve();
830
+ let timeoutCancel = () => { };
831
+ if (timerFuncID !== undefined && callerID !== undefined && timeoutms !== undefined) {
832
+ const { promise, cancel, endTime } = await this.durableSleepmsInternal(callerID, timerFuncID, timeoutms, this.dbPollingIntervalMs);
833
+ finishTime = endTime;
834
+ timeoutPromise = promise;
835
+ timeoutCancel = cancel;
644
836
  }
645
837
  else {
646
- break;
838
+ let poll = finishTime ? finishTime - ct : this.dbPollingIntervalMs;
839
+ poll = Math.min(this.dbPollingIntervalMs, poll);
840
+ const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
841
+ timeoutPromise = promise;
842
+ timeoutCancel = cancel;
843
+ }
844
+ try {
845
+ await Promise.race([statusPromise, timeoutPromise]);
846
+ }
847
+ finally {
848
+ timeoutCancel();
647
849
  }
648
850
  }
649
- else {
650
- await (0, utils_1.sleepms)(pollingIntervalMs);
851
+ finally {
852
+ this.workflowStatusMap.deregisterCallback(irh);
853
+ if (crh)
854
+ this.cancelWakeupMap.deregisterCallback(crh);
651
855
  }
652
856
  }
653
- return undefined;
654
857
  }
655
858
  /* BACKGROUND PROCESSES */
656
859
  /**
@@ -661,15 +864,30 @@ class PostgresSystemDatabase {
661
864
  this.notificationsClient = await this.pool.connect();
662
865
  await this.notificationsClient.query('LISTEN dbos_notifications_channel;');
663
866
  await this.notificationsClient.query('LISTEN dbos_workflow_events_channel;');
867
+ await this.notificationsClient.query('LISTEN dbos_workflow_status_channel;');
664
868
  const handler = (msg) => {
869
+ if (!this.shouldUseDBNotifications)
870
+ return; // Testing parameter
665
871
  if (msg.channel === 'dbos_notifications_channel') {
666
- if (msg.payload && msg.payload in this.notificationsMap) {
667
- this.notificationsMap[msg.payload]();
872
+ if (msg.payload) {
873
+ this.notificationsMap.callCallbacks(msg.payload);
668
874
  }
669
875
  }
670
- else {
671
- if (msg.payload && msg.payload in this.workflowEventsMap) {
672
- this.workflowEventsMap[msg.payload]();
876
+ else if (msg.channel === 'dbos_workflow_events_channel') {
877
+ if (msg.payload) {
878
+ this.workflowEventsMap.callCallbacks(msg.payload);
879
+ }
880
+ }
881
+ else if (msg.channel === 'dbos_workflow_status_channel') {
882
+ if (msg.payload) {
883
+ const notif = JSON.parse(msg.payload);
884
+ this.workflowStatusMap.callCallbacks(notif.wfid, notif);
885
+ if (notif.status === workflow_1.StatusString.CANCELLED) {
886
+ this.setWFCancelMap(notif.wfid);
887
+ }
888
+ else {
889
+ this.clearWFCancelMap(notif.wfid);
890
+ }
673
891
  }
674
892
  }
675
893
  };
@@ -866,7 +1084,7 @@ class PostgresSystemDatabase {
866
1084
  }
867
1085
  async dequeueWorkflow(workflowId, queue) {
868
1086
  if (queue.rateLimit) {
869
- const time = new Date().getTime();
1087
+ const time = Date.now();
870
1088
  await this.pool.query(`
871
1089
  UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
872
1090
  SET completed_at_epoch_ms = $2
@@ -881,7 +1099,7 @@ class PostgresSystemDatabase {
881
1099
  }
882
1100
  }
883
1101
  async findAndMarkStartableWorkflows(queue, executorID, appVersion) {
884
- const startTimeMs = new Date().getTime();
1102
+ const startTimeMs = Date.now();
885
1103
  const limiterPeriodMS = queue.rateLimit ? queue.rateLimit.periodSec * 1000 : 0;
886
1104
  const claimedIDs = [];
887
1105
  await this.knexDB.transaction(async (trx) => {