@dbos-inc/dbos-sdk 2.8.46-preview.g30b0e27ed1 → 2.9.2-preview
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 +28 -19
- package/dist/dbos-config.schema.json +28 -19
- package/dist/schemas/system_db_schema.d.ts +0 -4
- 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 +3 -1
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +66 -28
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos-runtime/config.js +2 -2
- 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 +1 -2
- package/dist/src/dbos-runtime/workflow_management.js.map +1 -1
- package/dist/src/dbos.d.ts +1 -8
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +8 -28
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/error.d.ts +6 -11
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +16 -27
- 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 +15 -42
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +99 -315
- package/dist/src/system_database.js.map +1 -1
- package/dist/src/workflow.d.ts.map +1 -1
- package/dist/src/workflow.js +38 -7
- package/dist/src/workflow.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/migrations/20250421000000_workflowcancel.js +0 -15
- package/migrations/20250421100000_triggers_wfcancel.js +0 -35
@@ -1,4 +1,5 @@
|
|
1
1
|
"use strict";
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
5
|
};
|
@@ -39,37 +40,6 @@ async function migrateSystemDatabase(systemPoolConfig, logger) {
|
|
39
40
|
}
|
40
41
|
}
|
41
42
|
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
|
-
}
|
73
43
|
class PostgresSystemDatabase {
|
74
44
|
pgPoolConfig;
|
75
45
|
systemDatabaseName;
|
@@ -78,32 +48,9 @@ class PostgresSystemDatabase {
|
|
78
48
|
pool;
|
79
49
|
systemPoolConfig;
|
80
50
|
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
|
-
*/
|
99
51
|
notificationsClient = null;
|
100
|
-
|
101
|
-
|
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.
|
52
|
+
notificationsMap = {};
|
53
|
+
workflowEventsMap = {};
|
107
54
|
constructor(pgPoolConfig, systemDatabaseName, logger, sysDbPoolSize) {
|
108
55
|
this.pgPoolConfig = pgPoolConfig;
|
109
56
|
this.systemDatabaseName = systemDatabaseName;
|
@@ -153,9 +100,7 @@ class PostgresSystemDatabase {
|
|
153
100
|
finally {
|
154
101
|
await pgSystemClient.end();
|
155
102
|
}
|
156
|
-
|
157
|
-
await this.listenForNotifications();
|
158
|
-
}
|
103
|
+
await this.listenForNotifications();
|
159
104
|
}
|
160
105
|
async destroy() {
|
161
106
|
await this.knexDB.destroy();
|
@@ -178,7 +123,7 @@ class PostgresSystemDatabase {
|
|
178
123
|
await pgSystemClient.query(`DROP DATABASE IF EXISTS ${dbosConfig.system_database};`);
|
179
124
|
await pgSystemClient.end();
|
180
125
|
}
|
181
|
-
async initWorkflowStatus(initStatus,
|
126
|
+
async initWorkflowStatus(initStatus, args) {
|
182
127
|
const result = await this.pool.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status (
|
183
128
|
workflow_uuid,
|
184
129
|
status,
|
@@ -212,8 +157,8 @@ class PostgresSystemDatabase {
|
|
212
157
|
initStatus.queueName,
|
213
158
|
initStatus.authenticatedUser,
|
214
159
|
initStatus.assumedRole,
|
215
|
-
|
216
|
-
|
160
|
+
utils_1.DBOSJSON.stringify(initStatus.authenticatedRoles),
|
161
|
+
utils_1.DBOSJSON.stringify(initStatus.request),
|
217
162
|
null,
|
218
163
|
initStatus.executorId,
|
219
164
|
initStatus.applicationVersion,
|
@@ -255,11 +200,12 @@ class PostgresSystemDatabase {
|
|
255
200
|
}
|
256
201
|
this.logger.debug(`Workflow ${initStatus.workflowUUID} attempt number: ${attempts}.`);
|
257
202
|
const status = resRow.status;
|
203
|
+
const serializedInputs = utils_1.DBOSJSON.stringify(args);
|
258
204
|
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]);
|
259
205
|
if (serializedInputs !== rows[0].inputs) {
|
260
206
|
this.logger.warn(`Workflow inputs for ${initStatus.workflowUUID} changed since the first call! Use the original inputs.`);
|
261
207
|
}
|
262
|
-
return {
|
208
|
+
return { args: utils_1.DBOSJSON.parse(rows[0].inputs), status };
|
263
209
|
}
|
264
210
|
async recordWorkflowStatusChange(workflowID, status, update, client) {
|
265
211
|
let rec = '';
|
@@ -272,7 +218,7 @@ class PostgresSystemDatabase {
|
|
272
218
|
const wRes = await (client ?? this.pool).query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status
|
273
219
|
SET ${rec} status=$2, output=$3, error=$4, updated_at=$5 WHERE workflow_uuid=$1`, [workflowID, status, update.output, update.error, Date.now()]);
|
274
220
|
if (wRes.rowCount !== 1) {
|
275
|
-
throw new error_1.
|
221
|
+
throw new error_1.DBOSWorkflowConflictUUIDError(`Attempt to record transition of nonexistent workflow ${workflowID}`);
|
276
222
|
}
|
277
223
|
}
|
278
224
|
async recordWorkflowOutput(workflowID, status) {
|
@@ -293,10 +239,9 @@ class PostgresSystemDatabase {
|
|
293
239
|
if (rows.length === 0) {
|
294
240
|
return null;
|
295
241
|
}
|
296
|
-
return rows[0].inputs;
|
242
|
+
return utils_1.DBOSJSON.parse(rows[0].inputs);
|
297
243
|
}
|
298
244
|
async getOperationResult(workflowID, functionID, client) {
|
299
|
-
await this.checkIfCanceled(workflowID);
|
300
245
|
const { rows } = await (client ?? this.pool).query(`SELECT output, error, child_workflow_id, function_name
|
301
246
|
FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs
|
302
247
|
WHERE workflow_uuid=$1 AND function_id=$2`, [workflowID, functionID]);
|
@@ -337,7 +282,7 @@ class PostgresSystemDatabase {
|
|
337
282
|
const err = error;
|
338
283
|
if (err.code === '40001' || err.code === '23505') {
|
339
284
|
// Serialization and primary key conflict (Postgres).
|
340
|
-
throw new error_1.
|
285
|
+
throw new error_1.DBOSWorkflowConflictUUIDError(workflowID);
|
341
286
|
}
|
342
287
|
else {
|
343
288
|
throw err;
|
@@ -362,30 +307,6 @@ class PostgresSystemDatabase {
|
|
362
307
|
return serialOutput;
|
363
308
|
}
|
364
309
|
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;
|
389
310
|
const curTime = Date.now();
|
390
311
|
let endTimeMs = curTime + durationMS;
|
391
312
|
const res = await this.getOperationResult(workflowID, functionID);
|
@@ -398,10 +319,7 @@ class PostgresSystemDatabase {
|
|
398
319
|
else {
|
399
320
|
await this.recordOperationResult(workflowID, functionID, { serialOutput: JSON.stringify(endTimeMs), functionName: exports.DBOS_FUNCNAME_SLEEP }, false);
|
400
321
|
}
|
401
|
-
return
|
402
|
-
...(0, utils_1.cancellableSleep)(Math.max(Math.min(maxSleepPerIteration, endTimeMs - curTime), 0)),
|
403
|
-
endTime: endTimeMs,
|
404
|
-
};
|
322
|
+
return (0, utils_1.cancellableSleep)(Math.max(endTimeMs - curTime, 0));
|
405
323
|
}
|
406
324
|
nullTopic = '__null__topic__';
|
407
325
|
async send(workflowID, functionID, destinationID, message, topic) {
|
@@ -440,56 +358,37 @@ class PostgresSystemDatabase {
|
|
440
358
|
}
|
441
359
|
return res.res.res;
|
442
360
|
}
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
// register the key with the global notifications listener.
|
361
|
+
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
362
|
+
const initRecvRows = (await this.pool.query(`SELECT topic FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.notifications WHERE destination_uuid=$1 AND topic=$2;`, [workflowID, topic])).rows;
|
363
|
+
if (initRecvRows.length === 0) {
|
364
|
+
// Then, register the key with the global notifications listener.
|
447
365
|
let resolveNotification;
|
448
366
|
const messagePromise = new Promise((resolve) => {
|
449
367
|
resolveNotification = resolve;
|
450
368
|
});
|
451
369
|
const payload = `${workflowID}::${topic}`;
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
});
|
370
|
+
this.notificationsMap[payload] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
|
371
|
+
let timeoutPromise = Promise.resolve();
|
372
|
+
let timeoutCancel = () => { };
|
456
373
|
try {
|
457
|
-
await this.
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
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
|
-
}
|
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
|
+
}
|
384
|
+
try {
|
385
|
+
await Promise.race([messagePromise, timeoutPromise]);
|
486
386
|
}
|
487
387
|
finally {
|
488
|
-
|
489
|
-
this.
|
388
|
+
timeoutCancel();
|
389
|
+
delete this.notificationsMap[payload];
|
490
390
|
}
|
491
391
|
}
|
492
|
-
await this.checkIfCanceled(workflowID);
|
493
392
|
// Transactionally consume and return the message if it's in the DB, otherwise return null.
|
494
393
|
let message = null;
|
495
394
|
const client = await this.pool.connect();
|
@@ -563,49 +462,40 @@ class PostgresSystemDatabase {
|
|
563
462
|
// Get the return the value. if it's in the DB, otherwise return null.
|
564
463
|
let value = null;
|
565
464
|
const payloadKey = `${workflowID}::${key}`;
|
566
|
-
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
567
|
-
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
568
465
|
// Register the key with the global notifications listener first... we do not want to look in the DB first
|
569
466
|
// or that would cause a timing hole.
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
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
|
467
|
+
let resolveNotification;
|
468
|
+
const valuePromise = new Promise((resolve) => {
|
469
|
+
resolveNotification = resolve;
|
470
|
+
});
|
471
|
+
this.workflowEventsMap[payloadKey] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
|
472
|
+
try {
|
473
|
+
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
474
|
+
const initRecvRows = (await this.pool.query(`
|
475
|
+
SELECT key, value
|
476
|
+
FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
|
477
|
+
WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
|
478
|
+
if (initRecvRows.length > 0) {
|
479
|
+
value = initRecvRows[0].value;
|
480
|
+
}
|
481
|
+
else {
|
596
482
|
// If we have a callerWorkflow, we want a durable sleep, otherwise, not
|
597
483
|
let timeoutPromise = Promise.resolve();
|
598
484
|
let timeoutCancel = () => { };
|
599
|
-
if (callerWorkflow
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
485
|
+
if (callerWorkflow) {
|
486
|
+
try {
|
487
|
+
const { promise, cancel } = await this.durableSleepms(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutSeconds * 1000);
|
488
|
+
timeoutPromise = promise;
|
489
|
+
timeoutCancel = cancel;
|
490
|
+
}
|
491
|
+
catch (e) {
|
492
|
+
this.logger.error(e);
|
493
|
+
delete this.workflowEventsMap[payloadKey];
|
494
|
+
throw new Error('durable sleepms failed');
|
495
|
+
}
|
604
496
|
}
|
605
497
|
else {
|
606
|
-
|
607
|
-
poll = Math.min(this.dbPollingIntervalMs, poll);
|
608
|
-
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
498
|
+
const { promise, cancel } = (0, utils_1.cancellableSleep)(timeoutSeconds * 1000);
|
609
499
|
timeoutPromise = promise;
|
610
500
|
timeoutCancel = cancel;
|
611
501
|
}
|
@@ -615,12 +505,17 @@ class PostgresSystemDatabase {
|
|
615
505
|
finally {
|
616
506
|
timeoutCancel();
|
617
507
|
}
|
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
|
+
}
|
618
515
|
}
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
this.cancelWakeupMap.deregisterCallback(crh);
|
623
|
-
}
|
516
|
+
}
|
517
|
+
finally {
|
518
|
+
delete this.workflowEventsMap[payloadKey];
|
624
519
|
}
|
625
520
|
// Record the output if it is inside a workflow.
|
626
521
|
if (callerWorkflow) {
|
@@ -634,23 +529,10 @@ class PostgresSystemDatabase {
|
|
634
529
|
async setWorkflowStatus(workflowID, status, resetRecoveryAttempts) {
|
635
530
|
await this.recordWorkflowStatusChange(workflowID, status, { resetRecoveryAttempts });
|
636
531
|
}
|
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
|
-
}
|
648
532
|
async cancelWorkflow(workflowID) {
|
649
533
|
const client = await this.pool.connect();
|
650
534
|
try {
|
651
535
|
await client.query('BEGIN');
|
652
|
-
await client.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_cancel(workflow_id)
|
653
|
-
VALUES ($1)`, [workflowID]);
|
654
536
|
// Remove workflow from queues table
|
655
537
|
await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
|
656
538
|
WHERE workflow_uuid = $1`, [workflowID]);
|
@@ -659,20 +541,12 @@ class PostgresSystemDatabase {
|
|
659
541
|
await client.query('COMMIT');
|
660
542
|
}
|
661
543
|
catch (error) {
|
662
|
-
this.logger.error(error);
|
663
544
|
await client.query('ROLLBACK');
|
664
545
|
throw error;
|
665
546
|
}
|
666
547
|
finally {
|
667
548
|
client.release();
|
668
549
|
}
|
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();
|
676
550
|
}
|
677
551
|
async resumeWorkflow(workflowID) {
|
678
552
|
const client = await this.pool.connect();
|
@@ -690,48 +564,17 @@ class PostgresSystemDatabase {
|
|
690
564
|
// Remove the workflow from the queues table so resume can safely be called on an ENQUEUED workflow
|
691
565
|
await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
|
692
566
|
WHERE workflow_uuid = $1`, [workflowID]);
|
693
|
-
await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_cancel
|
694
|
-
WHERE workflow_id = $1`, [workflowID]);
|
695
567
|
// Update status to pending and reset recovery attempts
|
696
568
|
await this.recordWorkflowStatusChange(workflowID, workflow_1.StatusString.PENDING, { resetRecoveryAttempts: true }, client);
|
697
569
|
await client.query('COMMIT');
|
698
570
|
}
|
699
571
|
catch (error) {
|
700
|
-
this.logger.error(error);
|
701
572
|
await client.query('ROLLBACK');
|
702
573
|
throw error;
|
703
574
|
}
|
704
575
|
finally {
|
705
576
|
client.release();
|
706
577
|
}
|
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
578
|
}
|
736
579
|
async getWorkflowStatus(workflowID, callerID, callerFN) {
|
737
580
|
const internalStatus = await this.getWorkflowStatusInternal(workflowID, callerID, callerFN);
|
@@ -768,8 +611,8 @@ class PostgresSystemDatabase {
|
|
768
611
|
queueName: rows[0].queue_name || undefined,
|
769
612
|
authenticatedUser: rows[0].authenticated_user,
|
770
613
|
assumedRole: rows[0].assumed_role,
|
771
|
-
authenticatedRoles:
|
772
|
-
request:
|
614
|
+
authenticatedRoles: utils_1.DBOSJSON.parse(rows[0].authenticated_roles),
|
615
|
+
request: utils_1.DBOSJSON.parse(rows[0].request),
|
773
616
|
executorId: rows[0].executor_id,
|
774
617
|
createdAt: Number(rows[0].created_at),
|
775
618
|
updatedAt: Number(rows[0].updated_at),
|
@@ -783,79 +626,34 @@ class PostgresSystemDatabase {
|
|
783
626
|
}, exports.DBOS_FUNCNAME_GETSTATUS, callerID, callerFN);
|
784
627
|
return sv ? JSON.parse(sv) : null;
|
785
628
|
}
|
786
|
-
async awaitWorkflowResult(workflowID,
|
787
|
-
const
|
788
|
-
|
629
|
+
async awaitWorkflowResult(workflowID, timeoutms) {
|
630
|
+
const pollingIntervalMs = 1000;
|
631
|
+
const et = timeoutms !== undefined ? new Date().getTime() + timeoutms : undefined;
|
789
632
|
while (true) {
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
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
|
-
}
|
633
|
+
const { rows } = await this.pool.query(`SELECT status, output, error FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE workflow_uuid=$1`, [workflowID]);
|
634
|
+
if (rows.length > 0) {
|
635
|
+
const status = rows[0].status;
|
636
|
+
if (status === workflow_1.StatusString.SUCCESS) {
|
637
|
+
return { res: rows[0].output };
|
822
638
|
}
|
823
|
-
|
824
|
-
|
825
|
-
this.logger.error(`Exception from system database: ${err}`);
|
826
|
-
throw err;
|
639
|
+
else if (status === workflow_1.StatusString.ERROR) {
|
640
|
+
return { err: rows[0].error };
|
827
641
|
}
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
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;
|
642
|
+
}
|
643
|
+
if (et !== undefined) {
|
644
|
+
const ct = new Date().getTime();
|
645
|
+
if (et > ct) {
|
646
|
+
await (0, utils_1.sleepms)(Math.min(pollingIntervalMs, et - ct));
|
838
647
|
}
|
839
648
|
else {
|
840
|
-
|
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
|
+
break;
|
851
650
|
}
|
852
651
|
}
|
853
|
-
|
854
|
-
|
855
|
-
if (crh)
|
856
|
-
this.cancelWakeupMap.deregisterCallback(crh);
|
652
|
+
else {
|
653
|
+
await (0, utils_1.sleepms)(pollingIntervalMs);
|
857
654
|
}
|
858
655
|
}
|
656
|
+
return undefined;
|
859
657
|
}
|
860
658
|
/* BACKGROUND PROCESSES */
|
861
659
|
/**
|
@@ -866,29 +664,15 @@ class PostgresSystemDatabase {
|
|
866
664
|
this.notificationsClient = await this.pool.connect();
|
867
665
|
await this.notificationsClient.query('LISTEN dbos_notifications_channel;');
|
868
666
|
await this.notificationsClient.query('LISTEN dbos_workflow_events_channel;');
|
869
|
-
await this.notificationsClient.query('LISTEN dbos_workflow_cancel_channel;');
|
870
667
|
const handler = (msg) => {
|
871
|
-
if (!this.shouldUseDBNotifications)
|
872
|
-
return; // Testing parameter
|
873
668
|
if (msg.channel === 'dbos_notifications_channel') {
|
874
|
-
if (msg.payload) {
|
875
|
-
this.notificationsMap
|
876
|
-
}
|
877
|
-
}
|
878
|
-
else if (msg.channel === 'dbos_workflow_events_channel') {
|
879
|
-
if (msg.payload) {
|
880
|
-
this.workflowEventsMap.callCallbacks(msg.payload);
|
669
|
+
if (msg.payload && msg.payload in this.notificationsMap) {
|
670
|
+
this.notificationsMap[msg.payload]();
|
881
671
|
}
|
882
672
|
}
|
883
|
-
else
|
884
|
-
if (msg.payload) {
|
885
|
-
|
886
|
-
if (notif.cancelled === 't') {
|
887
|
-
this.setWFCancelMap(notif.wfid);
|
888
|
-
}
|
889
|
-
else {
|
890
|
-
this.clearWFCancelMap(notif.wfid);
|
891
|
-
}
|
673
|
+
else {
|
674
|
+
if (msg.payload && msg.payload in this.workflowEventsMap) {
|
675
|
+
this.workflowEventsMap[msg.payload]();
|
892
676
|
}
|
893
677
|
}
|
894
678
|
};
|
@@ -1085,7 +869,7 @@ class PostgresSystemDatabase {
|
|
1085
869
|
}
|
1086
870
|
async dequeueWorkflow(workflowId, queue) {
|
1087
871
|
if (queue.rateLimit) {
|
1088
|
-
const time = Date.
|
872
|
+
const time = new Date().getTime();
|
1089
873
|
await this.pool.query(`
|
1090
874
|
UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
|
1091
875
|
SET completed_at_epoch_ms = $2
|
@@ -1100,7 +884,7 @@ class PostgresSystemDatabase {
|
|
1100
884
|
}
|
1101
885
|
}
|
1102
886
|
async findAndMarkStartableWorkflows(queue, executorID, appVersion) {
|
1103
|
-
const startTimeMs = Date.
|
887
|
+
const startTimeMs = new Date().getTime();
|
1104
888
|
const limiterPeriodMS = queue.rateLimit ? queue.rateLimit.periodSec * 1000 : 0;
|
1105
889
|
const claimedIDs = [];
|
1106
890
|
await this.knexDB.transaction(async (trx) => {
|