@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.
- 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/config.js +4 -4
- package/dist/src/dbos-runtime/config.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 +42 -15
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +316 -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/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,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
|
-
|
53
|
-
|
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
|
-
|
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,
|
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
|
-
|
160
|
-
|
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 {
|
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.
|
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
|
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.
|
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
|
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
|
-
|
361
|
-
|
362
|
-
|
363
|
-
//
|
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
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
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
|
-
|
388
|
-
|
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
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
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
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
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
|
-
|
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
|
-
|
517
|
-
|
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:
|
614
|
-
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,
|
629
|
-
const
|
630
|
-
|
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
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
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
|
-
|
639
|
-
|
823
|
+
catch (e) {
|
824
|
+
const err = e;
|
825
|
+
this.logger.error(`Exception from system database: ${err}`);
|
826
|
+
throw err;
|
640
827
|
}
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
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
|
-
|
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
|
-
|
652
|
-
|
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
|
669
|
-
this.notificationsMap
|
874
|
+
if (msg.payload) {
|
875
|
+
this.notificationsMap.callCallbacks(msg.payload);
|
670
876
|
}
|
671
877
|
}
|
672
|
-
else {
|
673
|
-
if (msg.payload
|
674
|
-
this.workflowEventsMap
|
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 =
|
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 =
|
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) => {
|