@dbos-inc/dbos-sdk 2.8.19-preview → 2.8.46-preview.g30b0e27ed1

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 (34) hide show
  1. package/dist/schemas/system_db_schema.d.ts +4 -0
  2. package/dist/schemas/system_db_schema.d.ts.map +1 -1
  3. package/dist/src/client.js +2 -2
  4. package/dist/src/client.js.map +1 -1
  5. package/dist/src/dbos-executor.d.ts +1 -3
  6. package/dist/src/dbos-executor.d.ts.map +1 -1
  7. package/dist/src/dbos-executor.js +26 -63
  8. package/dist/src/dbos-executor.js.map +1 -1
  9. package/dist/src/dbos-runtime/config.js +4 -4
  10. package/dist/src/dbos-runtime/config.js.map +1 -1
  11. package/dist/src/dbos-runtime/workflow_management.d.ts.map +1 -1
  12. package/dist/src/dbos-runtime/workflow_management.js +2 -1
  13. package/dist/src/dbos-runtime/workflow_management.js.map +1 -1
  14. package/dist/src/dbos.d.ts +8 -1
  15. package/dist/src/dbos.d.ts.map +1 -1
  16. package/dist/src/dbos.js +28 -8
  17. package/dist/src/dbos.js.map +1 -1
  18. package/dist/src/error.d.ts +11 -6
  19. package/dist/src/error.d.ts.map +1 -1
  20. package/dist/src/error.js +27 -16
  21. package/dist/src/error.js.map +1 -1
  22. package/dist/src/scheduler/scheduler.js +1 -1
  23. package/dist/src/scheduler/scheduler.js.map +1 -1
  24. package/dist/src/system_database.d.ts +42 -15
  25. package/dist/src/system_database.d.ts.map +1 -1
  26. package/dist/src/system_database.js +316 -99
  27. package/dist/src/system_database.js.map +1 -1
  28. package/dist/src/workflow.d.ts.map +1 -1
  29. package/dist/src/workflow.js +7 -38
  30. package/dist/src/workflow.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/migrations/20250421000000_workflowcancel.js +15 -0
  33. package/migrations/20250421100000_triggers_wfcancel.js +35 -0
  34. 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,9 +78,32 @@ 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 = 1000;
101
+ shouldUseDBNotifications = true;
102
+ notificationsMap = new NotificationMap();
103
+ workflowEventsMap = new NotificationMap();
104
+ cancelWakeupMap = new NotificationMap();
105
+ runningWorkflowMap = new Map(); // Map from workflowID to workflow promise
106
+ workflowCancellationMap = new Map(); // Map from workflowID to its cancellation status.
54
107
  constructor(pgPoolConfig, systemDatabaseName, logger, sysDbPoolSize) {
55
108
  this.pgPoolConfig = pgPoolConfig;
56
109
  this.systemDatabaseName = systemDatabaseName;
@@ -61,6 +114,7 @@ class PostgresSystemDatabase {
61
114
  systemDbConnectionString.pathname = `/${systemDatabaseName}`;
62
115
  this.systemPoolConfig = {
63
116
  connectionString: systemDbConnectionString.toString(),
117
+ connectionTimeoutMillis: pgPoolConfig.connectionTimeoutMillis,
64
118
  // This sets the application_name column in pg_stat_activity
65
119
  application_name: `dbos_transact_${utils_1.globalParams.executorID}_${utils_1.globalParams.appVersion}`,
66
120
  };
@@ -99,7 +153,9 @@ class PostgresSystemDatabase {
99
153
  finally {
100
154
  await pgSystemClient.end();
101
155
  }
102
- await this.listenForNotifications();
156
+ if (this.shouldUseDBNotifications) {
157
+ await this.listenForNotifications();
158
+ }
103
159
  }
104
160
  async destroy() {
105
161
  await this.knexDB.destroy();
@@ -122,7 +178,7 @@ class PostgresSystemDatabase {
122
178
  await pgSystemClient.query(`DROP DATABASE IF EXISTS ${dbosConfig.system_database};`);
123
179
  await pgSystemClient.end();
124
180
  }
125
- async initWorkflowStatus(initStatus, args) {
181
+ async initWorkflowStatus(initStatus, serializedInputs) {
126
182
  const result = await this.pool.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status (
127
183
  workflow_uuid,
128
184
  status,
@@ -156,8 +212,8 @@ class PostgresSystemDatabase {
156
212
  initStatus.queueName,
157
213
  initStatus.authenticatedUser,
158
214
  initStatus.assumedRole,
159
- utils_1.DBOSJSON.stringify(initStatus.authenticatedRoles),
160
- utils_1.DBOSJSON.stringify(initStatus.request),
215
+ JSON.stringify(initStatus.authenticatedRoles),
216
+ JSON.stringify(initStatus.request),
161
217
  null,
162
218
  initStatus.executorId,
163
219
  initStatus.applicationVersion,
@@ -199,12 +255,11 @@ class PostgresSystemDatabase {
199
255
  }
200
256
  this.logger.debug(`Workflow ${initStatus.workflowUUID} attempt number: ${attempts}.`);
201
257
  const status = resRow.status;
202
- const serializedInputs = utils_1.DBOSJSON.stringify(args);
203
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]);
204
259
  if (serializedInputs !== rows[0].inputs) {
205
260
  this.logger.warn(`Workflow inputs for ${initStatus.workflowUUID} changed since the first call! Use the original inputs.`);
206
261
  }
207
- return { args: utils_1.DBOSJSON.parse(rows[0].inputs), status };
262
+ return { serializedInputs: rows[0].inputs, status };
208
263
  }
209
264
  async recordWorkflowStatusChange(workflowID, status, update, client) {
210
265
  let rec = '';
@@ -217,7 +272,7 @@ class PostgresSystemDatabase {
217
272
  const wRes = await (client ?? this.pool).query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status
218
273
  SET ${rec} status=$2, output=$3, error=$4, updated_at=$5 WHERE workflow_uuid=$1`, [workflowID, status, update.output, update.error, Date.now()]);
219
274
  if (wRes.rowCount !== 1) {
220
- 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}`);
221
276
  }
222
277
  }
223
278
  async recordWorkflowOutput(workflowID, status) {
@@ -238,9 +293,10 @@ class PostgresSystemDatabase {
238
293
  if (rows.length === 0) {
239
294
  return null;
240
295
  }
241
- return utils_1.DBOSJSON.parse(rows[0].inputs);
296
+ return rows[0].inputs;
242
297
  }
243
298
  async getOperationResult(workflowID, functionID, client) {
299
+ await this.checkIfCanceled(workflowID);
244
300
  const { rows } = await (client ?? this.pool).query(`SELECT output, error, child_workflow_id, function_name
245
301
  FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs
246
302
  WHERE workflow_uuid=$1 AND function_id=$2`, [workflowID, functionID]);
@@ -281,7 +337,7 @@ class PostgresSystemDatabase {
281
337
  const err = error;
282
338
  if (err.code === '40001' || err.code === '23505') {
283
339
  // Serialization and primary key conflict (Postgres).
284
- throw new error_1.DBOSWorkflowConflictUUIDError(workflowID);
340
+ throw new error_1.DBOSWorkflowConflictError(workflowID);
285
341
  }
286
342
  else {
287
343
  throw err;
@@ -306,6 +362,30 @@ class PostgresSystemDatabase {
306
362
  return serialOutput;
307
363
  }
308
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;
309
389
  const curTime = Date.now();
310
390
  let endTimeMs = curTime + durationMS;
311
391
  const res = await this.getOperationResult(workflowID, functionID);
@@ -318,7 +398,10 @@ class PostgresSystemDatabase {
318
398
  else {
319
399
  await this.recordOperationResult(workflowID, functionID, { serialOutput: JSON.stringify(endTimeMs), functionName: exports.DBOS_FUNCNAME_SLEEP }, false);
320
400
  }
321
- 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
+ };
322
405
  }
323
406
  nullTopic = '__null__topic__';
324
407
  async send(workflowID, functionID, destinationID, message, topic) {
@@ -357,37 +440,56 @@ class PostgresSystemDatabase {
357
440
  }
358
441
  return res.res.res;
359
442
  }
360
- // Check if the key is already in the DB, then wait for the notification if it isn't.
361
- 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;
362
- if (initRecvRows.length === 0) {
363
- // 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.
364
447
  let resolveNotification;
365
448
  const messagePromise = new Promise((resolve) => {
366
449
  resolveNotification = resolve;
367
450
  });
368
451
  const payload = `${workflowID}::${topic}`;
369
- this.notificationsMap[payload] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
370
- let timeoutPromise = Promise.resolve();
371
- let timeoutCancel = () => { };
372
- try {
373
- const { promise, cancel } = await this.durableSleepms(workflowID, timeoutFunctionID, timeoutSeconds * 1000);
374
- timeoutPromise = promise;
375
- timeoutCancel = cancel;
376
- }
377
- catch (e) {
378
- this.logger.error(e);
379
- delete this.notificationsMap[payload];
380
- timeoutCancel();
381
- throw new Error('durable sleepms failed');
382
- }
452
+ const cbr = this.notificationsMap.registerCallback(payload, resolveNotification);
453
+ const crh = this.cancelWakeupMap.registerCallback(workflowID, (_res) => {
454
+ resolveNotification();
455
+ });
383
456
  try {
384
- 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
+ }
385
486
  }
386
487
  finally {
387
- timeoutCancel();
388
- delete this.notificationsMap[payload];
488
+ this.notificationsMap.deregisterCallback(cbr);
489
+ this.cancelWakeupMap.deregisterCallback(crh);
389
490
  }
390
491
  }
492
+ await this.checkIfCanceled(workflowID);
391
493
  // Transactionally consume and return the message if it's in the DB, otherwise return null.
392
494
  let message = null;
393
495
  const client = await this.pool.connect();
@@ -461,40 +563,49 @@ class PostgresSystemDatabase {
461
563
  // Get the return the value. if it's in the DB, otherwise return null.
462
564
  let value = null;
463
565
  const payloadKey = `${workflowID}::${key}`;
566
+ const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
567
+ let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
464
568
  // Register the key with the global notifications listener first... we do not want to look in the DB first
465
569
  // or that would cause a timing hole.
466
- let resolveNotification;
467
- const valuePromise = new Promise((resolve) => {
468
- resolveNotification = resolve;
469
- });
470
- this.workflowEventsMap[payloadKey] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
471
- try {
472
- // Check if the key is already in the DB, then wait for the notification if it isn't.
473
- const initRecvRows = (await this.pool.query(`
474
- SELECT key, value
475
- FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
476
- WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
477
- if (initRecvRows.length > 0) {
478
- value = initRecvRows[0].value;
479
- }
480
- 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
481
596
  // If we have a callerWorkflow, we want a durable sleep, otherwise, not
482
597
  let timeoutPromise = Promise.resolve();
483
598
  let timeoutCancel = () => { };
484
- if (callerWorkflow) {
485
- try {
486
- const { promise, cancel } = await this.durableSleepms(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutSeconds * 1000);
487
- timeoutPromise = promise;
488
- timeoutCancel = cancel;
489
- }
490
- catch (e) {
491
- this.logger.error(e);
492
- delete this.workflowEventsMap[payloadKey];
493
- throw new Error('durable sleepms failed');
494
- }
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;
495
604
  }
496
605
  else {
497
- 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);
498
609
  timeoutPromise = promise;
499
610
  timeoutCancel = cancel;
500
611
  }
@@ -504,17 +615,12 @@ class PostgresSystemDatabase {
504
615
  finally {
505
616
  timeoutCancel();
506
617
  }
507
- const finalRecvRows = (await this.pool.query(`
508
- SELECT value
509
- FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
510
- WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
511
- if (finalRecvRows.length > 0) {
512
- value = finalRecvRows[0].value;
513
- }
514
618
  }
515
- }
516
- finally {
517
- delete this.workflowEventsMap[payloadKey];
619
+ finally {
620
+ this.workflowEventsMap.deregisterCallback(cbr);
621
+ if (crh)
622
+ this.cancelWakeupMap.deregisterCallback(crh);
623
+ }
518
624
  }
519
625
  // Record the output if it is inside a workflow.
520
626
  if (callerWorkflow) {
@@ -528,10 +634,23 @@ class PostgresSystemDatabase {
528
634
  async setWorkflowStatus(workflowID, status, resetRecoveryAttempts) {
529
635
  await this.recordWorkflowStatusChange(workflowID, status, { resetRecoveryAttempts });
530
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
+ }
531
648
  async cancelWorkflow(workflowID) {
532
649
  const client = await this.pool.connect();
533
650
  try {
534
651
  await client.query('BEGIN');
652
+ await client.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_cancel(workflow_id)
653
+ VALUES ($1)`, [workflowID]);
535
654
  // Remove workflow from queues table
536
655
  await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
537
656
  WHERE workflow_uuid = $1`, [workflowID]);
@@ -540,12 +659,20 @@ class PostgresSystemDatabase {
540
659
  await client.query('COMMIT');
541
660
  }
542
661
  catch (error) {
662
+ this.logger.error(error);
543
663
  await client.query('ROLLBACK');
544
664
  throw error;
545
665
  }
546
666
  finally {
547
667
  client.release();
548
668
  }
669
+ this.setWFCancelMap(workflowID);
670
+ }
671
+ async checkIfCanceled(workflowID) {
672
+ if (this.workflowCancellationMap.get(workflowID) === true) {
673
+ throw new error_1.DBOSWorkflowCancelledError(workflowID);
674
+ }
675
+ return Promise.resolve();
549
676
  }
550
677
  async resumeWorkflow(workflowID) {
551
678
  const client = await this.pool.connect();
@@ -563,17 +690,48 @@ class PostgresSystemDatabase {
563
690
  // Remove the workflow from the queues table so resume can safely be called on an ENQUEUED workflow
564
691
  await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
565
692
  WHERE workflow_uuid = $1`, [workflowID]);
693
+ await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_cancel
694
+ WHERE workflow_id = $1`, [workflowID]);
566
695
  // Update status to pending and reset recovery attempts
567
696
  await this.recordWorkflowStatusChange(workflowID, workflow_1.StatusString.PENDING, { resetRecoveryAttempts: true }, client);
568
697
  await client.query('COMMIT');
569
698
  }
570
699
  catch (error) {
700
+ this.logger.error(error);
571
701
  await client.query('ROLLBACK');
572
702
  throw error;
573
703
  }
574
704
  finally {
575
705
  client.release();
576
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
+ }
577
735
  }
578
736
  async getWorkflowStatus(workflowID, callerID, callerFN) {
579
737
  const internalStatus = await this.getWorkflowStatusInternal(workflowID, callerID, callerFN);
@@ -610,8 +768,8 @@ class PostgresSystemDatabase {
610
768
  queueName: rows[0].queue_name || undefined,
611
769
  authenticatedUser: rows[0].authenticated_user,
612
770
  assumedRole: rows[0].assumed_role,
613
- authenticatedRoles: utils_1.DBOSJSON.parse(rows[0].authenticated_roles),
614
- request: utils_1.DBOSJSON.parse(rows[0].request),
771
+ authenticatedRoles: JSON.parse(rows[0].authenticated_roles),
772
+ request: JSON.parse(rows[0].request),
615
773
  executorId: rows[0].executor_id,
616
774
  createdAt: Number(rows[0].created_at),
617
775
  updatedAt: Number(rows[0].updated_at),
@@ -625,34 +783,79 @@ class PostgresSystemDatabase {
625
783
  }, exports.DBOS_FUNCNAME_GETSTATUS, callerID, callerFN);
626
784
  return sv ? JSON.parse(sv) : null;
627
785
  }
628
- async awaitWorkflowResult(workflowID, timeoutms) {
629
- const pollingIntervalMs = 1000;
630
- const et = timeoutms !== undefined ? new Date().getTime() + timeoutms : undefined;
786
+ async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID) {
787
+ const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
788
+ let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
631
789
  while (true) {
632
- const { rows } = await this.pool.query(`SELECT status, output, error FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE workflow_uuid=$1`, [workflowID]);
633
- if (rows.length > 0) {
634
- const status = rows[0].status;
635
- if (status === workflow_1.StatusString.SUCCESS) {
636
- return { res: rows[0].output };
790
+ let resolveNotification;
791
+ const statusPromise = new Promise((resolve) => {
792
+ resolveNotification = resolve;
793
+ });
794
+ const irh = this.cancelWakeupMap.registerCallback(workflowID, (_res) => {
795
+ resolveNotification();
796
+ });
797
+ const crh = callerID
798
+ ? this.cancelWakeupMap.registerCallback(callerID, (_res) => {
799
+ resolveNotification();
800
+ })
801
+ : undefined;
802
+ try {
803
+ if (callerID)
804
+ await this.checkIfCanceled(callerID);
805
+ try {
806
+ const { rows } = await this.pool.query(`SELECT status, output, error FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE workflow_uuid=$1`, [workflowID]);
807
+ if (rows.length > 0) {
808
+ const status = rows[0].status;
809
+ if (status === workflow_1.StatusString.SUCCESS) {
810
+ return { res: rows[0].output };
811
+ }
812
+ else if (status === workflow_1.StatusString.ERROR) {
813
+ return { err: rows[0].error };
814
+ }
815
+ else if (status === workflow_1.StatusString.CANCELLED) {
816
+ return { cancelled: true };
817
+ }
818
+ else {
819
+ // Status is not actionable
820
+ }
821
+ }
637
822
  }
638
- else if (status === workflow_1.StatusString.ERROR) {
639
- return { err: rows[0].error };
823
+ catch (e) {
824
+ const err = e;
825
+ this.logger.error(`Exception from system database: ${err}`);
826
+ throw err;
640
827
  }
641
- }
642
- if (et !== undefined) {
643
- const ct = new Date().getTime();
644
- if (et > ct) {
645
- await (0, utils_1.sleepms)(Math.min(pollingIntervalMs, et - ct));
828
+ const ct = Date.now();
829
+ if (finishTime && ct > finishTime)
830
+ return undefined; // Time's up
831
+ let timeoutPromise = Promise.resolve();
832
+ let timeoutCancel = () => { };
833
+ if (timerFuncID !== undefined && callerID !== undefined && timeoutms !== undefined) {
834
+ const { promise, cancel, endTime } = await this.durableSleepmsInternal(callerID, timerFuncID, timeoutms, this.dbPollingIntervalMs);
835
+ finishTime = endTime;
836
+ timeoutPromise = promise;
837
+ timeoutCancel = cancel;
646
838
  }
647
839
  else {
648
- break;
840
+ let poll = finishTime ? finishTime - ct : this.dbPollingIntervalMs;
841
+ poll = Math.min(this.dbPollingIntervalMs, poll);
842
+ const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
843
+ timeoutPromise = promise;
844
+ timeoutCancel = cancel;
845
+ }
846
+ try {
847
+ await Promise.race([statusPromise, timeoutPromise]);
848
+ }
849
+ finally {
850
+ timeoutCancel();
649
851
  }
650
852
  }
651
- else {
652
- await (0, utils_1.sleepms)(pollingIntervalMs);
853
+ finally {
854
+ this.cancelWakeupMap.deregisterCallback(irh);
855
+ if (crh)
856
+ this.cancelWakeupMap.deregisterCallback(crh);
653
857
  }
654
858
  }
655
- return undefined;
656
859
  }
657
860
  /* BACKGROUND PROCESSES */
658
861
  /**
@@ -663,15 +866,29 @@ class PostgresSystemDatabase {
663
866
  this.notificationsClient = await this.pool.connect();
664
867
  await this.notificationsClient.query('LISTEN dbos_notifications_channel;');
665
868
  await this.notificationsClient.query('LISTEN dbos_workflow_events_channel;');
869
+ await this.notificationsClient.query('LISTEN dbos_workflow_cancel_channel;');
666
870
  const handler = (msg) => {
871
+ if (!this.shouldUseDBNotifications)
872
+ return; // Testing parameter
667
873
  if (msg.channel === 'dbos_notifications_channel') {
668
- if (msg.payload && msg.payload in this.notificationsMap) {
669
- this.notificationsMap[msg.payload]();
874
+ if (msg.payload) {
875
+ this.notificationsMap.callCallbacks(msg.payload);
670
876
  }
671
877
  }
672
- else {
673
- if (msg.payload && msg.payload in this.workflowEventsMap) {
674
- this.workflowEventsMap[msg.payload]();
878
+ else if (msg.channel === 'dbos_workflow_events_channel') {
879
+ if (msg.payload) {
880
+ this.workflowEventsMap.callCallbacks(msg.payload);
881
+ }
882
+ }
883
+ else if (msg.channel === 'dbos_workflow_cancel_channel') {
884
+ if (msg.payload) {
885
+ const notif = JSON.parse(msg.payload);
886
+ if (notif.cancelled === 't') {
887
+ this.setWFCancelMap(notif.wfid);
888
+ }
889
+ else {
890
+ this.clearWFCancelMap(notif.wfid);
891
+ }
675
892
  }
676
893
  }
677
894
  };
@@ -868,7 +1085,7 @@ class PostgresSystemDatabase {
868
1085
  }
869
1086
  async dequeueWorkflow(workflowId, queue) {
870
1087
  if (queue.rateLimit) {
871
- const time = new Date().getTime();
1088
+ const time = Date.now();
872
1089
  await this.pool.query(`
873
1090
  UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
874
1091
  SET completed_at_epoch_ms = $2
@@ -883,7 +1100,7 @@ class PostgresSystemDatabase {
883
1100
  }
884
1101
  }
885
1102
  async findAndMarkStartableWorkflows(queue, executorID, appVersion) {
886
- const startTimeMs = new Date().getTime();
1103
+ const startTimeMs = Date.now();
887
1104
  const limiterPeriodMS = queue.rateLimit ? queue.rateLimit.periodSec * 1000 : 0;
888
1105
  const claimedIDs = [];
889
1106
  await this.knexDB.transaction(async (trx) => {