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