@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.
- package/dist/schemas/system_db_schema.d.ts +4 -0
- package/dist/schemas/system_db_schema.d.ts.map +1 -1
- package/dist/src/client.js +2 -2
- package/dist/src/client.js.map +1 -1
- package/dist/src/dbos-executor.d.ts +1 -3
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +26 -63
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos-runtime/workflow_management.d.ts.map +1 -1
- package/dist/src/dbos-runtime/workflow_management.js +2 -1
- package/dist/src/dbos-runtime/workflow_management.js.map +1 -1
- package/dist/src/dbos.d.ts +8 -1
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +28 -8
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/error.d.ts +11 -6
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +27 -16
- package/dist/src/error.js.map +1 -1
- package/dist/src/scheduler/scheduler.js +1 -1
- package/dist/src/scheduler/scheduler.js.map +1 -1
- package/dist/src/system_database.d.ts +48 -15
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +332 -99
- package/dist/src/system_database.js.map +1 -1
- package/dist/src/workflow.d.ts.map +1 -1
- package/dist/src/workflow.js +7 -38
- package/dist/src/workflow.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migrations/20250415000000_triggers_wfstatus.js +43 -0
- package/migrations/20250421000000_workflowcancel.js +15 -0
- package/migrations/20250421100000_triggers_wfcancel.js +35 -0
- 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,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
|
-
|
53
|
-
|
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
|
-
|
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,
|
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
|
-
|
161
|
-
|
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 {
|
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.
|
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
|
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.
|
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
|
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
|
-
|
362
|
-
|
363
|
-
|
364
|
-
//
|
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
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|
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
|
-
|
389
|
-
|
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
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
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
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
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
|
-
|
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
|
-
|
518
|
-
|
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:
|
615
|
-
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,
|
630
|
-
const
|
631
|
-
|
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
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
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
|
-
|
640
|
-
|
827
|
+
catch (e) {
|
828
|
+
const err = e;
|
829
|
+
this.logger.error(`Exception from system database: ${err}`);
|
830
|
+
throw err;
|
641
831
|
}
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
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
|
-
|
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
|
-
|
653
|
-
|
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
|
670
|
-
this.notificationsMap
|
879
|
+
if (msg.payload) {
|
880
|
+
this.notificationsMap.callCallbacks(msg.payload);
|
671
881
|
}
|
672
882
|
}
|
673
|
-
else {
|
674
|
-
if (msg.payload
|
675
|
-
this.workflowEventsMap
|
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 =
|
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 =
|
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) => {
|