@dbos-inc/dbos-sdk 2.8.20-preview → 2.8.45-preview.g9518e3e14d

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.
@@ -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,9 +78,33 @@ 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 = {};
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.
54
108
  constructor(pgPoolConfig, systemDatabaseName, logger, sysDbPoolSize) {
55
109
  this.pgPoolConfig = pgPoolConfig;
56
110
  this.systemDatabaseName = systemDatabaseName;
@@ -100,7 +154,9 @@ class PostgresSystemDatabase {
100
154
  finally {
101
155
  await pgSystemClient.end();
102
156
  }
103
- await this.listenForNotifications();
157
+ if (this.shouldUseDBNotifications) {
158
+ await this.listenForNotifications();
159
+ }
104
160
  }
105
161
  async destroy() {
106
162
  await this.knexDB.destroy();
@@ -123,7 +179,7 @@ class PostgresSystemDatabase {
123
179
  await pgSystemClient.query(`DROP DATABASE IF EXISTS ${dbosConfig.system_database};`);
124
180
  await pgSystemClient.end();
125
181
  }
126
- async initWorkflowStatus(initStatus, args) {
182
+ async initWorkflowStatus(initStatus, serializedInputs) {
127
183
  const result = await this.pool.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status (
128
184
  workflow_uuid,
129
185
  status,
@@ -157,8 +213,8 @@ class PostgresSystemDatabase {
157
213
  initStatus.queueName,
158
214
  initStatus.authenticatedUser,
159
215
  initStatus.assumedRole,
160
- utils_1.DBOSJSON.stringify(initStatus.authenticatedRoles),
161
- utils_1.DBOSJSON.stringify(initStatus.request),
216
+ JSON.stringify(initStatus.authenticatedRoles),
217
+ JSON.stringify(initStatus.request),
162
218
  null,
163
219
  initStatus.executorId,
164
220
  initStatus.applicationVersion,
@@ -200,12 +256,11 @@ class PostgresSystemDatabase {
200
256
  }
201
257
  this.logger.debug(`Workflow ${initStatus.workflowUUID} attempt number: ${attempts}.`);
202
258
  const status = resRow.status;
203
- const serializedInputs = utils_1.DBOSJSON.stringify(args);
204
259
  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]);
205
260
  if (serializedInputs !== rows[0].inputs) {
206
261
  this.logger.warn(`Workflow inputs for ${initStatus.workflowUUID} changed since the first call! Use the original inputs.`);
207
262
  }
208
- return { args: utils_1.DBOSJSON.parse(rows[0].inputs), status };
263
+ return { serializedInputs: rows[0].inputs, status };
209
264
  }
210
265
  async recordWorkflowStatusChange(workflowID, status, update, client) {
211
266
  let rec = '';
@@ -218,7 +273,7 @@ class PostgresSystemDatabase {
218
273
  const wRes = await (client ?? this.pool).query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status
219
274
  SET ${rec} status=$2, output=$3, error=$4, updated_at=$5 WHERE workflow_uuid=$1`, [workflowID, status, update.output, update.error, Date.now()]);
220
275
  if (wRes.rowCount !== 1) {
221
- throw new error_1.DBOSWorkflowConflictUUIDError(`Attempt to record transition of nonexistent workflow ${workflowID}`);
276
+ throw new error_1.DBOSWorkflowConflictError(`Attempt to record transition of nonexistent workflow ${workflowID}`);
222
277
  }
223
278
  }
224
279
  async recordWorkflowOutput(workflowID, status) {
@@ -239,9 +294,10 @@ class PostgresSystemDatabase {
239
294
  if (rows.length === 0) {
240
295
  return null;
241
296
  }
242
- return utils_1.DBOSJSON.parse(rows[0].inputs);
297
+ return rows[0].inputs;
243
298
  }
244
299
  async getOperationResult(workflowID, functionID, client) {
300
+ await this.checkIfCanceled(workflowID);
245
301
  const { rows } = await (client ?? this.pool).query(`SELECT output, error, child_workflow_id, function_name
246
302
  FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs
247
303
  WHERE workflow_uuid=$1 AND function_id=$2`, [workflowID, functionID]);
@@ -282,7 +338,7 @@ class PostgresSystemDatabase {
282
338
  const err = error;
283
339
  if (err.code === '40001' || err.code === '23505') {
284
340
  // Serialization and primary key conflict (Postgres).
285
- throw new error_1.DBOSWorkflowConflictUUIDError(workflowID);
341
+ throw new error_1.DBOSWorkflowConflictError(workflowID);
286
342
  }
287
343
  else {
288
344
  throw err;
@@ -307,6 +363,30 @@ class PostgresSystemDatabase {
307
363
  return serialOutput;
308
364
  }
309
365
  async durableSleepms(workflowID, functionID, durationMS) {
366
+ let resolveNotification;
367
+ const cancelPromise = new Promise((resolve) => {
368
+ resolveNotification = resolve;
369
+ });
370
+ const cbr = this.cancelWakeupMap.registerCallback(workflowID, resolveNotification);
371
+ try {
372
+ let timeoutPromise = Promise.resolve();
373
+ const { promise, cancel: timeoutCancel } = await this.durableSleepmsInternal(workflowID, functionID, durationMS);
374
+ timeoutPromise = promise;
375
+ try {
376
+ await Promise.race([cancelPromise, timeoutPromise]);
377
+ }
378
+ finally {
379
+ timeoutCancel();
380
+ }
381
+ }
382
+ finally {
383
+ this.cancelWakeupMap.deregisterCallback(cbr);
384
+ }
385
+ await this.checkIfCanceled(workflowID);
386
+ }
387
+ async durableSleepmsInternal(workflowID, functionID, durationMS, maxSleepPerIteration) {
388
+ if (maxSleepPerIteration === undefined)
389
+ maxSleepPerIteration = durationMS;
310
390
  const curTime = Date.now();
311
391
  let endTimeMs = curTime + durationMS;
312
392
  const res = await this.getOperationResult(workflowID, functionID);
@@ -319,7 +399,10 @@ class PostgresSystemDatabase {
319
399
  else {
320
400
  await this.recordOperationResult(workflowID, functionID, { serialOutput: JSON.stringify(endTimeMs), functionName: exports.DBOS_FUNCNAME_SLEEP }, false);
321
401
  }
322
- return (0, utils_1.cancellableSleep)(Math.max(endTimeMs - curTime, 0));
402
+ return {
403
+ ...(0, utils_1.cancellableSleep)(Math.max(Math.min(maxSleepPerIteration, endTimeMs - curTime), 0)),
404
+ endTime: endTimeMs,
405
+ };
323
406
  }
324
407
  nullTopic = '__null__topic__';
325
408
  async send(workflowID, functionID, destinationID, message, topic) {
@@ -358,37 +441,56 @@ class PostgresSystemDatabase {
358
441
  }
359
442
  return res.res.res;
360
443
  }
361
- // Check if the key is already in the DB, then wait for the notification if it isn't.
362
- 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;
363
- if (initRecvRows.length === 0) {
364
- // Then, register the key with the global notifications listener.
444
+ const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
445
+ let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
446
+ while (true) {
447
+ // register the key with the global notifications listener.
365
448
  let resolveNotification;
366
449
  const messagePromise = new Promise((resolve) => {
367
450
  resolveNotification = resolve;
368
451
  });
369
452
  const payload = `${workflowID}::${topic}`;
370
- this.notificationsMap[payload] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
371
- let timeoutPromise = Promise.resolve();
372
- let timeoutCancel = () => { };
373
- try {
374
- const { promise, cancel } = await this.durableSleepms(workflowID, timeoutFunctionID, timeoutSeconds * 1000);
375
- timeoutPromise = promise;
376
- timeoutCancel = cancel;
377
- }
378
- catch (e) {
379
- this.logger.error(e);
380
- delete this.notificationsMap[payload];
381
- timeoutCancel();
382
- throw new Error('durable sleepms failed');
383
- }
453
+ const cbr = this.notificationsMap.registerCallback(payload, resolveNotification);
454
+ const crh = this.cancelWakeupMap.registerCallback(workflowID, (_res) => {
455
+ resolveNotification();
456
+ });
384
457
  try {
385
- await Promise.race([messagePromise, timeoutPromise]);
458
+ await this.checkIfCanceled(workflowID);
459
+ // Check if the key is already in the DB, then wait for the notification if it isn't.
460
+ 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;
461
+ if (initRecvRows.length !== 0)
462
+ break;
463
+ const ct = Date.now();
464
+ if (finishTime && ct > finishTime)
465
+ break; // Time's up
466
+ let timeoutPromise = Promise.resolve();
467
+ let timeoutCancel = () => { };
468
+ if (timeoutms) {
469
+ const { promise, cancel, endTime } = await this.durableSleepmsInternal(workflowID, timeoutFunctionID, timeoutms, this.dbPollingIntervalMs);
470
+ timeoutPromise = promise;
471
+ timeoutCancel = cancel;
472
+ finishTime = endTime;
473
+ }
474
+ else {
475
+ let poll = finishTime ? finishTime - ct : this.dbPollingIntervalMs;
476
+ poll = Math.min(this.dbPollingIntervalMs, poll);
477
+ const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
478
+ timeoutPromise = promise;
479
+ timeoutCancel = cancel;
480
+ }
481
+ try {
482
+ await Promise.race([messagePromise, timeoutPromise]);
483
+ }
484
+ finally {
485
+ timeoutCancel();
486
+ }
386
487
  }
387
488
  finally {
388
- timeoutCancel();
389
- delete this.notificationsMap[payload];
489
+ this.notificationsMap.deregisterCallback(cbr);
490
+ this.cancelWakeupMap.deregisterCallback(crh);
390
491
  }
391
492
  }
493
+ await this.checkIfCanceled(workflowID);
392
494
  // Transactionally consume and return the message if it's in the DB, otherwise return null.
393
495
  let message = null;
394
496
  const client = await this.pool.connect();
@@ -462,40 +564,49 @@ class PostgresSystemDatabase {
462
564
  // Get the return the value. if it's in the DB, otherwise return null.
463
565
  let value = null;
464
566
  const payloadKey = `${workflowID}::${key}`;
567
+ const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
568
+ let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
465
569
  // Register the key with the global notifications listener first... we do not want to look in the DB first
466
570
  // or that would cause a timing hole.
467
- let resolveNotification;
468
- const valuePromise = new Promise((resolve) => {
469
- resolveNotification = resolve;
470
- });
471
- this.workflowEventsMap[payloadKey] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
472
- try {
473
- // Check if the key is already in the DB, then wait for the notification if it isn't.
474
- const initRecvRows = (await this.pool.query(`
475
- SELECT key, value
476
- FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
477
- WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
478
- if (initRecvRows.length > 0) {
479
- value = initRecvRows[0].value;
480
- }
481
- else {
571
+ while (true) {
572
+ let resolveNotification;
573
+ const valuePromise = new Promise((resolve) => {
574
+ resolveNotification = resolve;
575
+ });
576
+ const cbr = this.workflowEventsMap.registerCallback(payloadKey, resolveNotification);
577
+ const crh = callerWorkflow?.workflowID
578
+ ? this.cancelWakeupMap.registerCallback(callerWorkflow.workflowID, (_res) => {
579
+ resolveNotification();
580
+ })
581
+ : undefined;
582
+ try {
583
+ if (callerWorkflow?.workflowID)
584
+ await this.checkIfCanceled(callerWorkflow?.workflowID);
585
+ // Check if the key is already in the DB, then wait for the notification if it isn't.
586
+ const initRecvRows = (await this.pool.query(`
587
+ SELECT key, value
588
+ FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
589
+ WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
590
+ if (initRecvRows.length > 0) {
591
+ value = initRecvRows[0].value;
592
+ break;
593
+ }
594
+ const ct = Date.now();
595
+ if (finishTime && ct > finishTime)
596
+ break; // Time's up
482
597
  // If we have a callerWorkflow, we want a durable sleep, otherwise, not
483
598
  let timeoutPromise = Promise.resolve();
484
599
  let timeoutCancel = () => { };
485
- if (callerWorkflow) {
486
- try {
487
- const { promise, cancel } = await this.durableSleepms(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutSeconds * 1000);
488
- timeoutPromise = promise;
489
- timeoutCancel = cancel;
490
- }
491
- catch (e) {
492
- this.logger.error(e);
493
- delete this.workflowEventsMap[payloadKey];
494
- throw new Error('durable sleepms failed');
495
- }
600
+ if (callerWorkflow && timeoutms) {
601
+ const { promise, cancel, endTime } = await this.durableSleepmsInternal(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutms, this.dbPollingIntervalMs);
602
+ timeoutPromise = promise;
603
+ timeoutCancel = cancel;
604
+ finishTime = endTime;
496
605
  }
497
606
  else {
498
- const { promise, cancel } = (0, utils_1.cancellableSleep)(timeoutSeconds * 1000);
607
+ let poll = finishTime ? finishTime - ct : this.dbPollingIntervalMs;
608
+ poll = Math.min(this.dbPollingIntervalMs, poll);
609
+ const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
499
610
  timeoutPromise = promise;
500
611
  timeoutCancel = cancel;
501
612
  }
@@ -505,17 +616,12 @@ class PostgresSystemDatabase {
505
616
  finally {
506
617
  timeoutCancel();
507
618
  }
508
- const finalRecvRows = (await this.pool.query(`
509
- SELECT value
510
- FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
511
- WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
512
- if (finalRecvRows.length > 0) {
513
- value = finalRecvRows[0].value;
514
- }
515
619
  }
516
- }
517
- finally {
518
- delete this.workflowEventsMap[payloadKey];
620
+ finally {
621
+ this.workflowEventsMap.deregisterCallback(cbr);
622
+ if (crh)
623
+ this.cancelWakeupMap.deregisterCallback(crh);
624
+ }
519
625
  }
520
626
  // Record the output if it is inside a workflow.
521
627
  if (callerWorkflow) {
@@ -529,10 +635,23 @@ class PostgresSystemDatabase {
529
635
  async setWorkflowStatus(workflowID, status, resetRecoveryAttempts) {
530
636
  await this.recordWorkflowStatusChange(workflowID, status, { resetRecoveryAttempts });
531
637
  }
638
+ setWFCancelMap(workflowID) {
639
+ if (this.runningWorkflowMap.has(workflowID)) {
640
+ this.workflowCancellationMap.set(workflowID, true);
641
+ }
642
+ this.cancelWakeupMap.callCallbacks(workflowID);
643
+ }
644
+ clearWFCancelMap(workflowID) {
645
+ if (this.workflowCancellationMap.has(workflowID)) {
646
+ this.workflowCancellationMap.delete(workflowID);
647
+ }
648
+ }
532
649
  async cancelWorkflow(workflowID) {
533
650
  const client = await this.pool.connect();
534
651
  try {
535
652
  await client.query('BEGIN');
653
+ await client.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_cancel(workflow_id)
654
+ VALUES ($1)`, [workflowID]);
536
655
  // Remove workflow from queues table
537
656
  await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
538
657
  WHERE workflow_uuid = $1`, [workflowID]);
@@ -541,12 +660,20 @@ class PostgresSystemDatabase {
541
660
  await client.query('COMMIT');
542
661
  }
543
662
  catch (error) {
663
+ console.log(error);
544
664
  await client.query('ROLLBACK');
545
665
  throw error;
546
666
  }
547
667
  finally {
548
668
  client.release();
549
669
  }
670
+ this.setWFCancelMap(workflowID);
671
+ }
672
+ async checkIfCanceled(workflowID) {
673
+ if (this.workflowCancellationMap.get(workflowID) === true) {
674
+ throw new error_1.DBOSWorkflowCancelledError(workflowID);
675
+ }
676
+ return Promise.resolve();
550
677
  }
551
678
  async resumeWorkflow(workflowID) {
552
679
  const client = await this.pool.connect();
@@ -564,6 +691,8 @@ class PostgresSystemDatabase {
564
691
  // Remove the workflow from the queues table so resume can safely be called on an ENQUEUED workflow
565
692
  await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
566
693
  WHERE workflow_uuid = $1`, [workflowID]);
694
+ await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_cancel
695
+ WHERE workflow_id = $1`, [workflowID]);
567
696
  // Update status to pending and reset recovery attempts
568
697
  await this.recordWorkflowStatusChange(workflowID, workflow_1.StatusString.PENDING, { resetRecoveryAttempts: true }, client);
569
698
  await client.query('COMMIT');
@@ -575,6 +704,38 @@ class PostgresSystemDatabase {
575
704
  finally {
576
705
  client.release();
577
706
  }
707
+ this.clearWFCancelMap(workflowID);
708
+ }
709
+ registerRunningWorkflow(workflowID, workflowPromise) {
710
+ // Need to await for the workflow and capture errors.
711
+ const awaitWorkflowPromise = workflowPromise
712
+ .catch((error) => {
713
+ this.logger.debug('Captured error in awaitWorkflowPromise: ' + error);
714
+ })
715
+ .finally(() => {
716
+ // Remove itself from pending workflow map.
717
+ this.runningWorkflowMap.delete(workflowID);
718
+ this.workflowCancellationMap.delete(workflowID);
719
+ });
720
+ this.runningWorkflowMap.set(workflowID, awaitWorkflowPromise);
721
+ }
722
+ async awaitRunningWorkflows() {
723
+ if (this.runningWorkflowMap.size > 0) {
724
+ this.logger.info('Waiting for pending workflows to finish.');
725
+ await Promise.allSettled(this.runningWorkflowMap.values());
726
+ }
727
+ if (this.workflowEventsMap.map.size > 0) {
728
+ this.logger.warn('Workflow events map is not empty - shutdown is not clean.');
729
+ //throw new Error('Workflow events map is not empty - shutdown is not clean.');
730
+ }
731
+ if (this.notificationsMap.map.size > 0) {
732
+ this.logger.warn('Message notification map is not empty - shutdown is not clean.');
733
+ //throw new Error('Message notification map is not empty - shutdown is not clean.');
734
+ }
735
+ if (this.workflowStatusMap.map.size > 0) {
736
+ this.logger.warn('Workflow status map is not empty - shutdown is not clean.');
737
+ //throw new Error('Workflow status map is not empty - shutdown is not clean.');
738
+ }
578
739
  }
579
740
  async getWorkflowStatus(workflowID, callerID, callerFN) {
580
741
  const internalStatus = await this.getWorkflowStatusInternal(workflowID, callerID, callerFN);
@@ -611,8 +772,8 @@ class PostgresSystemDatabase {
611
772
  queueName: rows[0].queue_name || undefined,
612
773
  authenticatedUser: rows[0].authenticated_user,
613
774
  assumedRole: rows[0].assumed_role,
614
- authenticatedRoles: utils_1.DBOSJSON.parse(rows[0].authenticated_roles),
615
- request: utils_1.DBOSJSON.parse(rows[0].request),
775
+ authenticatedRoles: JSON.parse(rows[0].authenticated_roles),
776
+ request: JSON.parse(rows[0].request),
616
777
  executorId: rows[0].executor_id,
617
778
  createdAt: Number(rows[0].created_at),
618
779
  updatedAt: Number(rows[0].updated_at),
@@ -626,34 +787,79 @@ class PostgresSystemDatabase {
626
787
  }, exports.DBOS_FUNCNAME_GETSTATUS, callerID, callerFN);
627
788
  return sv ? JSON.parse(sv) : null;
628
789
  }
629
- async awaitWorkflowResult(workflowID, timeoutms) {
630
- const pollingIntervalMs = 1000;
631
- const et = timeoutms !== undefined ? new Date().getTime() + timeoutms : undefined;
790
+ async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID) {
791
+ const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
792
+ let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
632
793
  while (true) {
633
- const { rows } = await this.pool.query(`SELECT status, output, error FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE workflow_uuid=$1`, [workflowID]);
634
- if (rows.length > 0) {
635
- const status = rows[0].status;
636
- if (status === workflow_1.StatusString.SUCCESS) {
637
- return { res: rows[0].output };
794
+ let resolveNotification;
795
+ const statusPromise = new Promise((resolve) => {
796
+ resolveNotification = resolve;
797
+ });
798
+ const irh = this.workflowStatusMap.registerCallback(workflowID, (_res) => {
799
+ resolveNotification();
800
+ });
801
+ const crh = callerID
802
+ ? this.cancelWakeupMap.registerCallback(callerID, (_res) => {
803
+ resolveNotification();
804
+ })
805
+ : undefined;
806
+ try {
807
+ if (callerID)
808
+ await this.checkIfCanceled(callerID);
809
+ try {
810
+ const { rows } = await this.pool.query(`SELECT status, output, error FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE workflow_uuid=$1`, [workflowID]);
811
+ if (rows.length > 0) {
812
+ const status = rows[0].status;
813
+ if (status === workflow_1.StatusString.SUCCESS) {
814
+ return { res: rows[0].output };
815
+ }
816
+ else if (status === workflow_1.StatusString.ERROR) {
817
+ return { err: rows[0].error };
818
+ }
819
+ else if (status === workflow_1.StatusString.CANCELLED) {
820
+ return { cancelled: true };
821
+ }
822
+ else {
823
+ // Status is not actionable
824
+ }
825
+ }
638
826
  }
639
- else if (status === workflow_1.StatusString.ERROR) {
640
- return { err: rows[0].error };
827
+ catch (e) {
828
+ const err = e;
829
+ this.logger.error(`Exception from system database: ${err}`);
830
+ throw err;
641
831
  }
642
- }
643
- if (et !== undefined) {
644
- const ct = new Date().getTime();
645
- if (et > ct) {
646
- await (0, utils_1.sleepms)(Math.min(pollingIntervalMs, et - ct));
832
+ const ct = Date.now();
833
+ if (finishTime && ct > finishTime)
834
+ return undefined; // Time's up
835
+ let timeoutPromise = Promise.resolve();
836
+ let timeoutCancel = () => { };
837
+ if (timerFuncID !== undefined && callerID !== undefined && timeoutms !== undefined) {
838
+ const { promise, cancel, endTime } = await this.durableSleepmsInternal(callerID, timerFuncID, timeoutms, this.dbPollingIntervalMs);
839
+ finishTime = endTime;
840
+ timeoutPromise = promise;
841
+ timeoutCancel = cancel;
647
842
  }
648
843
  else {
649
- break;
844
+ let poll = finishTime ? finishTime - ct : this.dbPollingIntervalMs;
845
+ poll = Math.min(this.dbPollingIntervalMs, poll);
846
+ const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
847
+ timeoutPromise = promise;
848
+ timeoutCancel = cancel;
849
+ }
850
+ try {
851
+ await Promise.race([statusPromise, timeoutPromise]);
852
+ }
853
+ finally {
854
+ timeoutCancel();
650
855
  }
651
856
  }
652
- else {
653
- await (0, utils_1.sleepms)(pollingIntervalMs);
857
+ finally {
858
+ this.workflowStatusMap.deregisterCallback(irh);
859
+ if (crh)
860
+ this.cancelWakeupMap.deregisterCallback(crh);
654
861
  }
655
862
  }
656
- return undefined;
657
863
  }
658
864
  /* BACKGROUND PROCESSES */
659
865
  /**
@@ -664,15 +870,42 @@ class PostgresSystemDatabase {
664
870
  this.notificationsClient = await this.pool.connect();
665
871
  await this.notificationsClient.query('LISTEN dbos_notifications_channel;');
666
872
  await this.notificationsClient.query('LISTEN dbos_workflow_events_channel;');
873
+ await this.notificationsClient.query('LISTEN dbos_workflow_status_channel;');
874
+ await this.notificationsClient.query('LISTEN dbos_workflow_cancel_channel;');
667
875
  const handler = (msg) => {
876
+ if (!this.shouldUseDBNotifications)
877
+ return; // Testing parameter
668
878
  if (msg.channel === 'dbos_notifications_channel') {
669
- if (msg.payload && msg.payload in this.notificationsMap) {
670
- this.notificationsMap[msg.payload]();
879
+ if (msg.payload) {
880
+ this.notificationsMap.callCallbacks(msg.payload);
671
881
  }
672
882
  }
673
- else {
674
- if (msg.payload && msg.payload in this.workflowEventsMap) {
675
- this.workflowEventsMap[msg.payload]();
883
+ else if (msg.channel === 'dbos_workflow_events_channel') {
884
+ if (msg.payload) {
885
+ this.workflowEventsMap.callCallbacks(msg.payload);
886
+ }
887
+ }
888
+ else if (msg.channel === 'dbos_workflow_status_channel') {
889
+ if (msg.payload) {
890
+ const notif = JSON.parse(msg.payload);
891
+ this.workflowStatusMap.callCallbacks(notif.wfid, notif);
892
+ if (notif.status === workflow_1.StatusString.CANCELLED) {
893
+ this.setWFCancelMap(notif.wfid);
894
+ }
895
+ else {
896
+ this.clearWFCancelMap(notif.wfid);
897
+ }
898
+ }
899
+ }
900
+ else if (msg.channel === 'dbos_workflow_cancel_channel') {
901
+ if (msg.payload) {
902
+ const notif = JSON.parse(msg.payload);
903
+ if (notif.cancelled === 't') {
904
+ this.setWFCancelMap(notif.wfid);
905
+ }
906
+ else {
907
+ this.clearWFCancelMap(notif.wfid);
908
+ }
676
909
  }
677
910
  }
678
911
  };
@@ -869,7 +1102,7 @@ class PostgresSystemDatabase {
869
1102
  }
870
1103
  async dequeueWorkflow(workflowId, queue) {
871
1104
  if (queue.rateLimit) {
872
- const time = new Date().getTime();
1105
+ const time = Date.now();
873
1106
  await this.pool.query(`
874
1107
  UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
875
1108
  SET completed_at_epoch_ms = $2
@@ -884,7 +1117,7 @@ class PostgresSystemDatabase {
884
1117
  }
885
1118
  }
886
1119
  async findAndMarkStartableWorkflows(queue, executorID, appVersion) {
887
- const startTimeMs = new Date().getTime();
1120
+ const startTimeMs = Date.now();
888
1121
  const limiterPeriodMS = queue.rateLimit ? queue.rateLimit.periodSec * 1000 : 0;
889
1122
  const claimedIDs = [];
890
1123
  await this.knexDB.transaction(async (trx) => {