@dbos-inc/dbos-sdk 2.9.9-preview → 2.9.17-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/dist/src/client.d.ts +12 -1
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +28 -2
- package/dist/src/client.js.map +1 -1
- package/dist/src/conductor/conductor.d.ts.map +1 -1
- package/dist/src/conductor/conductor.js +26 -11
- package/dist/src/conductor/conductor.js.map +1 -1
- package/dist/src/conductor/protocol.d.ts +20 -4
- package/dist/src/conductor/protocol.d.ts.map +1 -1
- package/dist/src/conductor/protocol.js +23 -4
- package/dist/src/conductor/protocol.js.map +1 -1
- package/dist/src/dbos-executor.d.ts +14 -17
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +74 -174
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos-runtime/cli.d.ts.map +1 -1
- package/dist/src/dbos-runtime/cli.js +87 -15
- package/dist/src/dbos-runtime/cli.js.map +1 -1
- package/dist/src/dbos-runtime/workflow_management.d.ts +13 -13
- package/dist/src/dbos-runtime/workflow_management.d.ts.map +1 -1
- package/dist/src/dbos-runtime/workflow_management.js +90 -108
- package/dist/src/dbos-runtime/workflow_management.js.map +1 -1
- package/dist/src/dbos.d.ts +29 -11
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +53 -36
- 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 +28 -17
- package/dist/src/error.js.map +1 -1
- package/dist/src/eventreceiver.d.ts +13 -8
- package/dist/src/eventreceiver.d.ts.map +1 -1
- package/dist/src/httpServer/server.d.ts.map +1 -1
- package/dist/src/httpServer/server.js +38 -12
- package/dist/src/httpServer/server.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 +68 -59
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +715 -434
- package/dist/src/system_database.js.map +1 -1
- package/dist/src/workflow.d.ts +8 -0
- 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/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
|
};
|
@@ -12,6 +11,7 @@ const workflow_1 = require("./workflow");
|
|
12
11
|
const utils_1 = require("./utils");
|
13
12
|
const knex_1 = __importDefault(require("knex"));
|
14
13
|
const path_1 = __importDefault(require("path"));
|
14
|
+
const crypto_1 = require("crypto");
|
15
15
|
exports.DBOS_FUNCNAME_SEND = 'DBOS.send';
|
16
16
|
exports.DBOS_FUNCNAME_RECV = 'DBOS.recv';
|
17
17
|
exports.DBOS_FUNCNAME_SETEVENT = 'DBOS.setEvent';
|
@@ -40,6 +40,186 @@ async function migrateSystemDatabase(systemPoolConfig, logger) {
|
|
40
40
|
}
|
41
41
|
}
|
42
42
|
exports.migrateSystemDatabase = migrateSystemDatabase;
|
43
|
+
class NotificationMap {
|
44
|
+
map = new Map();
|
45
|
+
curCK = 0;
|
46
|
+
registerCallback(key, cb) {
|
47
|
+
if (!this.map.has(key)) {
|
48
|
+
this.map.set(key, new Map());
|
49
|
+
}
|
50
|
+
const ck = this.curCK++;
|
51
|
+
this.map.get(key).set(ck, cb);
|
52
|
+
return { key, ck };
|
53
|
+
}
|
54
|
+
deregisterCallback(k) {
|
55
|
+
if (!this.map.has(k.key))
|
56
|
+
return;
|
57
|
+
const sm = this.map.get(k.key);
|
58
|
+
if (!sm.has(k.ck))
|
59
|
+
return;
|
60
|
+
sm.delete(k.ck);
|
61
|
+
if (sm.size === 0) {
|
62
|
+
this.map.delete(k.key);
|
63
|
+
}
|
64
|
+
}
|
65
|
+
callCallbacks(key, event) {
|
66
|
+
if (!this.map.has(key))
|
67
|
+
return;
|
68
|
+
const sm = this.map.get(key);
|
69
|
+
for (const cb of sm.values()) {
|
70
|
+
cb(event);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
}
|
74
|
+
async function insertWorkflowStatus(client, initStatus) {
|
75
|
+
const { rows } = await client.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status (
|
76
|
+
workflow_uuid,
|
77
|
+
status,
|
78
|
+
name,
|
79
|
+
class_name,
|
80
|
+
config_name,
|
81
|
+
queue_name,
|
82
|
+
authenticated_user,
|
83
|
+
assumed_role,
|
84
|
+
authenticated_roles,
|
85
|
+
request,
|
86
|
+
executor_id,
|
87
|
+
application_version,
|
88
|
+
application_id,
|
89
|
+
created_at,
|
90
|
+
recovery_attempts,
|
91
|
+
updated_at
|
92
|
+
) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
93
|
+
ON CONFLICT (workflow_uuid)
|
94
|
+
DO UPDATE SET
|
95
|
+
recovery_attempts = workflow_status.recovery_attempts + 1,
|
96
|
+
updated_at = EXCLUDED.updated_at,
|
97
|
+
executor_id = EXCLUDED.executor_id
|
98
|
+
RETURNING recovery_attempts, status, name, class_name, config_name, queue_name`, [
|
99
|
+
initStatus.workflowUUID,
|
100
|
+
initStatus.status,
|
101
|
+
initStatus.workflowName,
|
102
|
+
initStatus.workflowClassName,
|
103
|
+
initStatus.workflowConfigName,
|
104
|
+
initStatus.queueName ?? null,
|
105
|
+
initStatus.authenticatedUser,
|
106
|
+
initStatus.assumedRole,
|
107
|
+
JSON.stringify(initStatus.authenticatedRoles),
|
108
|
+
JSON.stringify(initStatus.request),
|
109
|
+
initStatus.executorId,
|
110
|
+
initStatus.applicationVersion ?? null,
|
111
|
+
initStatus.applicationID,
|
112
|
+
initStatus.createdAt,
|
113
|
+
initStatus.status === workflow_1.StatusString.ENQUEUED ? 0 : 1,
|
114
|
+
initStatus.updatedAt ?? Date.now(),
|
115
|
+
]);
|
116
|
+
if (rows.length === 0) {
|
117
|
+
throw new Error(`Attempt to insert workflow ${initStatus.workflowUUID} failed`);
|
118
|
+
}
|
119
|
+
return rows[0];
|
120
|
+
}
|
121
|
+
async function insertWorkflowInputs(client, workflowID, serializedInputs) {
|
122
|
+
const { rows } = await client.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_inputs
|
123
|
+
(workflow_uuid, inputs) VALUES($1, $2)
|
124
|
+
ON CONFLICT (workflow_uuid) DO UPDATE SET workflow_uuid = excluded.workflow_uuid
|
125
|
+
RETURNING inputs`, [workflowID, serializedInputs]);
|
126
|
+
if (rows.length === 0) {
|
127
|
+
throw new Error(`Attempt to insert workflow ${workflowID} inputs failed`);
|
128
|
+
}
|
129
|
+
return rows[0].inputs;
|
130
|
+
}
|
131
|
+
async function enqueueWorkflow(client, workflowID, queueName) {
|
132
|
+
await client.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue (workflow_uuid, queue_name) VALUES ($1, $2)
|
133
|
+
ON CONFLICT (workflow_uuid) DO NOTHING;`, [workflowID, queueName]);
|
134
|
+
}
|
135
|
+
async function deleteQueuedWorkflows(client, workflowID) {
|
136
|
+
await client.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue WHERE workflow_uuid = $1`, [
|
137
|
+
workflowID,
|
138
|
+
]);
|
139
|
+
}
|
140
|
+
async function getWorkflowStatusValue(client, workflowID) {
|
141
|
+
const { rows } = await client.query(`SELECT status FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE workflow_uuid=$1`, [workflowID]);
|
142
|
+
return rows.length === 0 ? undefined : rows[0].status;
|
143
|
+
}
|
144
|
+
async function updateWorkflowStatus(client, workflowID, status, options = {}) {
|
145
|
+
let setClause = `SET status=$2, updated_at=$3`;
|
146
|
+
let whereClause = `WHERE workflow_uuid=$1`;
|
147
|
+
const args = [workflowID, status, Date.now()];
|
148
|
+
const update = options.update ?? {};
|
149
|
+
if (update.output) {
|
150
|
+
const param = args.push(update.output);
|
151
|
+
setClause += `, output=$${param}`;
|
152
|
+
}
|
153
|
+
if (update.error) {
|
154
|
+
const param = args.push(update.error);
|
155
|
+
setClause += `, error=$${param}`;
|
156
|
+
}
|
157
|
+
if (update.resetRecoveryAttempts) {
|
158
|
+
setClause += `, recovery_attempts = 0`;
|
159
|
+
}
|
160
|
+
if (update.queueName) {
|
161
|
+
const param = args.push(update.queueName);
|
162
|
+
setClause += `, queue_name=$${param}`;
|
163
|
+
}
|
164
|
+
const where = options.where ?? {};
|
165
|
+
if (where.status) {
|
166
|
+
const param = args.push(where.status);
|
167
|
+
whereClause += ` AND status=$${param}`;
|
168
|
+
}
|
169
|
+
const result = await client.query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status ${setClause} ${whereClause}`, args);
|
170
|
+
const throwOnFailure = options.throwOnFailure ?? true;
|
171
|
+
if (throwOnFailure && result.rowCount !== 1) {
|
172
|
+
throw new error_1.DBOSWorkflowConflictError(`Attempt to record transition of nonexistent workflow ${workflowID}`);
|
173
|
+
}
|
174
|
+
}
|
175
|
+
async function recordOperationResult(client, workflowID, functionID, functionName, checkConflict, options = {}) {
|
176
|
+
try {
|
177
|
+
await client.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs
|
178
|
+
(workflow_uuid, function_id, output, error, function_name, child_workflow_id)
|
179
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
180
|
+
${checkConflict ? '' : ' ON CONFLICT DO NOTHING'};`, [
|
181
|
+
workflowID,
|
182
|
+
functionID,
|
183
|
+
options.output ?? null,
|
184
|
+
options.error ?? null,
|
185
|
+
functionName,
|
186
|
+
options.childWorkflowID ?? null,
|
187
|
+
]);
|
188
|
+
}
|
189
|
+
catch (error) {
|
190
|
+
const err = error;
|
191
|
+
if (err.code === '40001' || err.code === '23505') {
|
192
|
+
// Serialization and primary key conflict (Postgres).
|
193
|
+
throw new error_1.DBOSWorkflowConflictError(workflowID);
|
194
|
+
}
|
195
|
+
else {
|
196
|
+
throw err;
|
197
|
+
}
|
198
|
+
}
|
199
|
+
}
|
200
|
+
function mapWorkflowStatus(row) {
|
201
|
+
return {
|
202
|
+
workflowUUID: row.workflow_uuid,
|
203
|
+
status: row.status,
|
204
|
+
workflowName: row.name,
|
205
|
+
output: row.output ? row.output : null,
|
206
|
+
error: row.error ? row.error : null,
|
207
|
+
workflowClassName: row.class_name ?? '',
|
208
|
+
workflowConfigName: row.config_name ?? '',
|
209
|
+
queueName: row.queue_name,
|
210
|
+
authenticatedUser: row.authenticated_user,
|
211
|
+
assumedRole: row.assumed_role,
|
212
|
+
authenticatedRoles: JSON.parse(row.authenticated_roles),
|
213
|
+
request: JSON.parse(row.request),
|
214
|
+
executorId: row.executor_id,
|
215
|
+
createdAt: Number(row.created_at),
|
216
|
+
updatedAt: Number(row.updated_at),
|
217
|
+
applicationVersion: row.application_version,
|
218
|
+
applicationID: row.application_id,
|
219
|
+
recoveryAttempts: Number(row.recovery_attempts),
|
220
|
+
input: row.inputs,
|
221
|
+
};
|
222
|
+
}
|
43
223
|
class PostgresSystemDatabase {
|
44
224
|
pgPoolConfig;
|
45
225
|
systemDatabaseName;
|
@@ -47,10 +227,35 @@ class PostgresSystemDatabase {
|
|
47
227
|
sysDbPoolSize;
|
48
228
|
pool;
|
49
229
|
systemPoolConfig;
|
230
|
+
// TODO: remove Knex connection in favor of just using Pool
|
50
231
|
knexDB;
|
232
|
+
/*
|
233
|
+
* Generally, notifications are asynchronous. One should:
|
234
|
+
* Subscribe to updates
|
235
|
+
* Read the database item in question
|
236
|
+
* In response to updates, re-read the database item
|
237
|
+
* Unsubscribe at the end
|
238
|
+
* The notification mechanism is reliable in the sense that it will eventually deliver updates
|
239
|
+
* or the DB connection will get dropped. The right thing to do if you lose connectivity to
|
240
|
+
* the system DB is to exit the process and go through recovery... system DB writes, notifications,
|
241
|
+
* etc may not have completed correctly, and recovery is the way to rebuild in-memory state.
|
242
|
+
*
|
243
|
+
* NOTE:
|
244
|
+
* PG Notifications are not fully reliable.
|
245
|
+
* Dropped connections are recoverable - you just need to restart and scan everything.
|
246
|
+
* (The whole VM being the logical choice, so workflows can recover from any write failures.)
|
247
|
+
* The real problem is, if the pipes out of the server are full... then notifications can be
|
248
|
+
* dropped, and only the PG server log may note it. For those reasons, we do occasional polling
|
249
|
+
*/
|
51
250
|
notificationsClient = null;
|
52
|
-
|
53
|
-
|
251
|
+
dbPollingIntervalResultMs = 1000;
|
252
|
+
dbPollingIntervalEventMs = 10000;
|
253
|
+
shouldUseDBNotifications = true;
|
254
|
+
notificationsMap = new NotificationMap();
|
255
|
+
workflowEventsMap = new NotificationMap();
|
256
|
+
cancelWakeupMap = new NotificationMap();
|
257
|
+
runningWorkflowMap = new Map(); // Map from workflowID to workflow promise
|
258
|
+
workflowCancellationMap = new Map(); // Map from workflowID to its cancellation status.
|
54
259
|
constructor(pgPoolConfig, systemDatabaseName, logger, sysDbPoolSize) {
|
55
260
|
this.pgPoolConfig = pgPoolConfig;
|
56
261
|
this.systemDatabaseName = systemDatabaseName;
|
@@ -100,7 +305,9 @@ class PostgresSystemDatabase {
|
|
100
305
|
finally {
|
101
306
|
await pgSystemClient.end();
|
102
307
|
}
|
103
|
-
|
308
|
+
if (this.shouldUseDBNotifications) {
|
309
|
+
await this.#listenForNotifications();
|
310
|
+
}
|
104
311
|
}
|
105
312
|
async destroy() {
|
106
313
|
await this.knexDB.destroy();
|
@@ -123,258 +330,174 @@ class PostgresSystemDatabase {
|
|
123
330
|
await pgSystemClient.query(`DROP DATABASE IF EXISTS ${dbosConfig.system_database};`);
|
124
331
|
await pgSystemClient.end();
|
125
332
|
}
|
126
|
-
async initWorkflowStatus(initStatus,
|
127
|
-
const
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
initStatus.
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
initStatus.status === workflow_1.StatusString.ENQUEUED ? 0 : 1,
|
166
|
-
Date.now(),
|
167
|
-
]);
|
168
|
-
// Check the started workflow matches the expected name, class_name, config_name, and queue_name
|
169
|
-
// A mismatch indicates a workflow starting with the same UUID but different functions, which should not be allowed.
|
170
|
-
const resRow = result.rows[0];
|
171
|
-
initStatus.workflowConfigName = initStatus.workflowConfigName || '';
|
172
|
-
resRow.config_name = resRow.config_name || '';
|
173
|
-
resRow.queue_name = resRow.queue_name === null ? undefined : resRow.queue_name; // Convert null in SQL to undefined
|
174
|
-
let msg = '';
|
175
|
-
if (resRow.name !== initStatus.workflowName) {
|
176
|
-
msg = `Workflow already exists with a different function name: ${resRow.name}, but the provided function name is: ${initStatus.workflowName}`;
|
177
|
-
}
|
178
|
-
else if (resRow.class_name !== initStatus.workflowClassName) {
|
179
|
-
msg = `Workflow already exists with a different class name: ${resRow.class_name}, but the provided class name is: ${initStatus.workflowClassName}`;
|
180
|
-
}
|
181
|
-
else if (resRow.config_name !== initStatus.workflowConfigName) {
|
182
|
-
msg = `Workflow already exists with a different class configuration: ${resRow.config_name}, but the provided class configuration is: ${initStatus.workflowConfigName}`;
|
183
|
-
}
|
184
|
-
else if (resRow.queue_name !== initStatus.queueName) {
|
185
|
-
// This is a warning because a different queue name is not necessarily an error.
|
186
|
-
this.logger.warn(`Workflow (${initStatus.workflowUUID}) already exists in queue: ${resRow.queue_name}, but the provided queue name is: ${initStatus.queueName}. The queue is not updated. ${new Error().stack}`);
|
187
|
-
}
|
188
|
-
if (msg !== '') {
|
189
|
-
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
190
|
-
}
|
191
|
-
// recovery_attempt means "attempts" (we kept the name for backward compatibility). It's default value is 1.
|
192
|
-
// Every time we init the status, we increment `recovery_attempts` by 1.
|
193
|
-
// Thus, when this number becomes equal to `maxRetries + 1`, we should mark the workflow as `RETRIES_EXCEEDED`.
|
194
|
-
const attempts = resRow.recovery_attempts;
|
195
|
-
if (maxRetries && attempts > maxRetries + 1) {
|
196
|
-
await this.pool.query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status SET status=$1 WHERE workflow_uuid=$2 AND status=$3`, [workflow_1.StatusString.RETRIES_EXCEEDED, initStatus.workflowUUID, workflow_1.StatusString.PENDING]);
|
197
|
-
throw new error_1.DBOSDeadLetterQueueError(initStatus.workflowUUID, maxRetries);
|
198
|
-
}
|
199
|
-
this.logger.debug(`Workflow ${initStatus.workflowUUID} attempt number: ${attempts}.`);
|
200
|
-
const status = resRow.status;
|
201
|
-
const serializedInputs = utils_1.DBOSJSON.stringify(args);
|
202
|
-
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]);
|
203
|
-
if (serializedInputs !== rows[0].inputs) {
|
204
|
-
this.logger.warn(`Workflow inputs for ${initStatus.workflowUUID} changed since the first call! Use the original inputs.`);
|
205
|
-
}
|
206
|
-
return { args: utils_1.DBOSJSON.parse(rows[0].inputs), status };
|
207
|
-
}
|
208
|
-
async recordWorkflowStatusChange(workflowID, status, update, client) {
|
209
|
-
let rec = '';
|
210
|
-
if (update.resetRecoveryAttempts) {
|
211
|
-
rec = ' recovery_attempts = 0, ';
|
212
|
-
}
|
213
|
-
if (update.incrementRecoveryAttempts) {
|
214
|
-
rec = ' recovery_attempts = recovery_attempts + 1';
|
333
|
+
async initWorkflowStatus(initStatus, serializedInputs, maxRetries) {
|
334
|
+
const client = await this.pool.connect();
|
335
|
+
try {
|
336
|
+
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
337
|
+
const resRow = await insertWorkflowStatus(client, initStatus);
|
338
|
+
if (resRow.name !== initStatus.workflowName) {
|
339
|
+
const msg = `Workflow already exists with a different function name: ${resRow.name}, but the provided function name is: ${initStatus.workflowName}`;
|
340
|
+
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
341
|
+
}
|
342
|
+
else if (resRow.class_name !== initStatus.workflowClassName) {
|
343
|
+
const msg = `Workflow already exists with a different class name: ${resRow.class_name}, but the provided class name is: ${initStatus.workflowClassName}`;
|
344
|
+
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
345
|
+
}
|
346
|
+
else if ((resRow.config_name || '') !== (initStatus.workflowConfigName || '')) {
|
347
|
+
const msg = `Workflow already exists with a different class configuration: ${resRow.config_name}, but the provided class configuration is: ${initStatus.workflowConfigName}`;
|
348
|
+
throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
|
349
|
+
}
|
350
|
+
else if ((resRow.queue_name ?? undefined) !== initStatus.queueName) {
|
351
|
+
// This is a warning because a different queue name is not necessarily an error.
|
352
|
+
this.logger.warn(`Workflow (${initStatus.workflowUUID}) already exists in queue: ${resRow.queue_name}, but the provided queue name is: ${initStatus.queueName}. The queue is not updated. ${new Error().stack}`);
|
353
|
+
}
|
354
|
+
// recovery_attempt means "attempts" (we kept the name for backward compatibility). It's default value is 1.
|
355
|
+
// Every time we init the status, we increment `recovery_attempts` by 1.
|
356
|
+
// Thus, when this number becomes equal to `maxRetries + 1`, we should mark the workflow as `RETRIES_EXCEEDED`.
|
357
|
+
const attempts = resRow.recovery_attempts;
|
358
|
+
if (maxRetries && attempts > maxRetries + 1) {
|
359
|
+
await updateWorkflowStatus(client, initStatus.workflowUUID, workflow_1.StatusString.RETRIES_EXCEEDED, {
|
360
|
+
where: { status: workflow_1.StatusString.PENDING },
|
361
|
+
throwOnFailure: false,
|
362
|
+
});
|
363
|
+
throw new error_1.DBOSDeadLetterQueueError(initStatus.workflowUUID, maxRetries);
|
364
|
+
}
|
365
|
+
this.logger.debug(`Workflow ${initStatus.workflowUUID} attempt number: ${attempts}.`);
|
366
|
+
const status = resRow.status;
|
367
|
+
const inputResult = await insertWorkflowInputs(client, initStatus.workflowUUID, serializedInputs);
|
368
|
+
if (serializedInputs !== inputResult) {
|
369
|
+
this.logger.warn(`Workflow inputs for ${initStatus.workflowUUID} changed since the first call! Use the original inputs.`);
|
370
|
+
}
|
371
|
+
return { serializedInputs: inputResult, status };
|
215
372
|
}
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
throw new error_1.DBOSWorkflowConflictUUIDError(`Attempt to record transition of nonexistent workflow ${workflowID}`);
|
373
|
+
finally {
|
374
|
+
await client.query('COMMIT');
|
375
|
+
client.release();
|
220
376
|
}
|
221
377
|
}
|
222
378
|
async recordWorkflowOutput(workflowID, status) {
|
223
|
-
await this.
|
379
|
+
const client = await this.pool.connect();
|
380
|
+
try {
|
381
|
+
await updateWorkflowStatus(client, workflowID, workflow_1.StatusString.SUCCESS, { update: { output: status.output } });
|
382
|
+
}
|
383
|
+
finally {
|
384
|
+
client.release();
|
385
|
+
}
|
224
386
|
}
|
225
387
|
async recordWorkflowError(workflowID, status) {
|
226
|
-
await this.
|
388
|
+
const client = await this.pool.connect();
|
389
|
+
try {
|
390
|
+
await updateWorkflowStatus(client, workflowID, workflow_1.StatusString.ERROR, { update: { error: status.error } });
|
391
|
+
}
|
392
|
+
finally {
|
393
|
+
client.release();
|
394
|
+
}
|
227
395
|
}
|
228
396
|
async getPendingWorkflows(executorID, appVersion) {
|
229
|
-
const getWorkflows = await this.pool.query(`SELECT workflow_uuid, queue_name
|
397
|
+
const getWorkflows = await this.pool.query(`SELECT workflow_uuid, queue_name
|
398
|
+
FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status
|
399
|
+
WHERE status=$1 AND executor_id=$2 AND application_version=$3`, [workflow_1.StatusString.PENDING, executorID, appVersion]);
|
230
400
|
return getWorkflows.rows.map((i) => ({
|
231
401
|
workflowUUID: i.workflow_uuid,
|
232
402
|
queueName: i.queue_name,
|
233
403
|
}));
|
234
404
|
}
|
235
405
|
async getWorkflowInputs(workflowID) {
|
236
|
-
const { rows } = await this.pool.query(`SELECT inputs FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_inputs
|
406
|
+
const { rows } = await this.pool.query(`SELECT inputs FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_inputs
|
407
|
+
WHERE workflow_uuid=$1`, [workflowID]);
|
237
408
|
if (rows.length === 0) {
|
238
409
|
return null;
|
239
410
|
}
|
240
|
-
return
|
411
|
+
return rows[0].inputs;
|
241
412
|
}
|
242
|
-
async
|
243
|
-
|
413
|
+
async #getOperationResultAndThrowIfCancelled(client, workflowID, functionID) {
|
414
|
+
await this.#checkIfCanceled(client, workflowID);
|
415
|
+
const { rows } = await client.query(`SELECT output, error, child_workflow_id, function_name
|
244
416
|
FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs
|
245
417
|
WHERE workflow_uuid=$1 AND function_id=$2`, [workflowID, functionID]);
|
246
418
|
if (rows.length === 0) {
|
247
|
-
return
|
419
|
+
return undefined;
|
248
420
|
}
|
249
421
|
else {
|
250
422
|
return {
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
functionName: rows[0].function_name,
|
256
|
-
},
|
423
|
+
output: rows[0].output,
|
424
|
+
error: rows[0].error,
|
425
|
+
childWorkflowID: rows[0].child_workflow_id,
|
426
|
+
functionName: rows[0].function_name,
|
257
427
|
};
|
258
428
|
}
|
259
429
|
}
|
430
|
+
async getOperationResultAndThrowIfCancelled(workflowID, functionID) {
|
431
|
+
const client = await this.pool.connect();
|
432
|
+
try {
|
433
|
+
return await this.#getOperationResultAndThrowIfCancelled(client, workflowID, functionID);
|
434
|
+
}
|
435
|
+
finally {
|
436
|
+
client.release();
|
437
|
+
}
|
438
|
+
}
|
260
439
|
async getAllOperationResults(workflowID) {
|
261
440
|
const { rows } = await this.pool.query(`SELECT * FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs WHERE workflow_uuid=$1`, [workflowID]);
|
262
441
|
return rows;
|
263
442
|
}
|
264
|
-
async recordOperationResult(workflowID, functionID,
|
443
|
+
async recordOperationResult(workflowID, functionID, functionName, checkConflict, options = {}) {
|
444
|
+
const client = await this.pool.connect();
|
265
445
|
try {
|
266
|
-
await (client
|
267
|
-
(workflow_uuid, function_id, output, error, function_name, child_workflow_id)
|
268
|
-
VALUES ($1, $2, $3, $4, $5, $6)
|
269
|
-
${checkConflict ? '' : ' ON CONFLICT DO NOTHING'}
|
270
|
-
;`, [
|
271
|
-
workflowID,
|
272
|
-
functionID,
|
273
|
-
rec.serialOutput ?? null,
|
274
|
-
rec.serialError ?? null,
|
275
|
-
rec.functionName,
|
276
|
-
rec.childWfId ?? null,
|
277
|
-
]);
|
446
|
+
await recordOperationResult(client, workflowID, functionID, functionName, checkConflict, options);
|
278
447
|
}
|
279
|
-
|
280
|
-
|
281
|
-
if (err.code === '40001' || err.code === '23505') {
|
282
|
-
// Serialization and primary key conflict (Postgres).
|
283
|
-
throw new error_1.DBOSWorkflowConflictUUIDError(workflowID);
|
284
|
-
}
|
285
|
-
else {
|
286
|
-
throw err;
|
287
|
-
}
|
448
|
+
finally {
|
449
|
+
client.release();
|
288
450
|
}
|
289
451
|
}
|
290
452
|
async getMaxFunctionID(workflowID) {
|
291
453
|
const { rows } = await this.pool.query(`SELECT max(function_id) as max_function_id FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs WHERE workflow_uuid=$1`, [workflowID]);
|
292
454
|
return rows.length === 0 ? 0 : rows[0].max_function_id;
|
293
455
|
}
|
294
|
-
async forkWorkflow(
|
295
|
-
const
|
456
|
+
async forkWorkflow(workflowID, startStep, options = {}) {
|
457
|
+
const newWorkflowID = options.newWorkflowID ?? (0, crypto_1.randomUUID)();
|
458
|
+
const workflowStatus = await this.getWorkflowStatus(workflowID);
|
296
459
|
if (workflowStatus === null) {
|
297
|
-
throw new error_1.DBOSNonExistentWorkflowError(`Workflow ${
|
460
|
+
throw new error_1.DBOSNonExistentWorkflowError(`Workflow ${workflowID} does not exist`);
|
461
|
+
}
|
462
|
+
if (!workflowStatus.input) {
|
463
|
+
throw new error_1.DBOSNonExistentWorkflowError(`Workflow ${workflowID} has no input`);
|
298
464
|
}
|
299
465
|
const client = await this.pool.connect();
|
300
466
|
try {
|
301
|
-
await client.query('BEGIN');
|
302
|
-
const
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
await client
|
324
|
-
forkedWorkflowId,
|
325
|
-
workflow_1.StatusString.ENQUEUED,
|
326
|
-
workflowStatus.workflowName,
|
327
|
-
workflowStatus.workflowClassName,
|
328
|
-
workflowStatus.workflowConfigName,
|
329
|
-
utils_1.INTERNAL_QUEUE_NAME,
|
330
|
-
workflowStatus.authenticatedUser,
|
331
|
-
workflowStatus.assumedRole,
|
332
|
-
utils_1.DBOSJSON.stringify(workflowStatus.authenticatedRoles),
|
333
|
-
utils_1.DBOSJSON.stringify(workflowStatus.request),
|
334
|
-
null,
|
335
|
-
null,
|
336
|
-
workflowStatus.applicationVersion,
|
337
|
-
workflowStatus.applicationID,
|
338
|
-
Date.now(),
|
339
|
-
0,
|
340
|
-
Date.now(),
|
341
|
-
]);
|
342
|
-
// Copy the inputs to the new workflow
|
343
|
-
const inputQuery = `INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_inputs (workflow_uuid, inputs)
|
344
|
-
SELECT $2, inputs
|
345
|
-
FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_inputs
|
346
|
-
WHERE workflow_uuid = $1;`;
|
347
|
-
await client.query(inputQuery, [originalWorkflowID, forkedWorkflowId]);
|
467
|
+
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
468
|
+
const now = Date.now();
|
469
|
+
await insertWorkflowStatus(client, {
|
470
|
+
workflowUUID: newWorkflowID,
|
471
|
+
status: workflow_1.StatusString.ENQUEUED,
|
472
|
+
workflowName: workflowStatus.workflowName,
|
473
|
+
workflowClassName: workflowStatus.workflowClassName,
|
474
|
+
workflowConfigName: workflowStatus.workflowConfigName,
|
475
|
+
queueName: utils_1.INTERNAL_QUEUE_NAME,
|
476
|
+
authenticatedUser: workflowStatus.authenticatedUser,
|
477
|
+
assumedRole: workflowStatus.assumedRole,
|
478
|
+
authenticatedRoles: workflowStatus.authenticatedRoles,
|
479
|
+
output: null,
|
480
|
+
error: null,
|
481
|
+
request: workflowStatus.request,
|
482
|
+
executorId: utils_1.globalParams.executorID,
|
483
|
+
applicationVersion: options.applicationVersion ?? workflowStatus.applicationVersion,
|
484
|
+
applicationID: workflowStatus.applicationID,
|
485
|
+
createdAt: now,
|
486
|
+
recoveryAttempts: 0,
|
487
|
+
updatedAt: now,
|
488
|
+
});
|
489
|
+
await insertWorkflowInputs(client, newWorkflowID, workflowStatus.input);
|
348
490
|
if (startStep > 0) {
|
349
|
-
const query = `
|
350
|
-
|
351
|
-
workflow_uuid,
|
352
|
-
function_id,
|
353
|
-
output,
|
354
|
-
error,
|
355
|
-
function_name,
|
356
|
-
child_workflow_id
|
357
|
-
)
|
358
|
-
SELECT
|
359
|
-
$1 AS workflow_uuid,
|
360
|
-
function_id,
|
361
|
-
output,
|
362
|
-
error,
|
363
|
-
function_name,
|
364
|
-
child_workflow_id
|
491
|
+
const query = `INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs
|
492
|
+
(workflow_uuid, function_id, output, error, function_name, child_workflow_id )
|
493
|
+
SELECT $1 AS workflow_uuid, function_id, output, error, function_name, child_workflow_id
|
365
494
|
FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs
|
366
|
-
WHERE workflow_uuid = $2 AND function_id < $3
|
367
|
-
|
368
|
-
await client.query(query, [forkedWorkflowId, originalWorkflowID, startStep]);
|
495
|
+
WHERE workflow_uuid = $2 AND function_id < $3`;
|
496
|
+
await client.query(query, [newWorkflowID, workflowID, startStep]);
|
369
497
|
}
|
370
|
-
await client.
|
371
|
-
INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue (workflow_uuid, queue_name)
|
372
|
-
VALUES ($1, $2)
|
373
|
-
ON CONFLICT (workflow_uuid)
|
374
|
-
DO NOTHING;
|
375
|
-
`, [forkedWorkflowId, utils_1.INTERNAL_QUEUE_NAME]);
|
498
|
+
await enqueueWorkflow(client, newWorkflowID, utils_1.INTERNAL_QUEUE_NAME);
|
376
499
|
await client.query('COMMIT');
|
377
|
-
return
|
500
|
+
return newWorkflowID;
|
378
501
|
}
|
379
502
|
catch (error) {
|
380
503
|
await client.query('ROLLBACK');
|
@@ -384,37 +507,67 @@ class PostgresSystemDatabase {
|
|
384
507
|
client.release();
|
385
508
|
}
|
386
509
|
}
|
387
|
-
async
|
388
|
-
|
389
|
-
|
390
|
-
if (
|
391
|
-
|
392
|
-
throw new error_1.DBOSUnexpectedStepError(workflowID, functionID, functionName, res.res.functionName);
|
393
|
-
}
|
394
|
-
await client?.query('ROLLBACK');
|
395
|
-
return res.res.res;
|
510
|
+
async #runAndRecordResult(client, functionName, workflowID, functionID, func) {
|
511
|
+
const result = await this.#getOperationResultAndThrowIfCancelled(client, workflowID, functionID);
|
512
|
+
if (result !== undefined) {
|
513
|
+
if (result.functionName !== functionName) {
|
514
|
+
throw new error_1.DBOSUnexpectedStepError(workflowID, functionID, functionName, result.functionName);
|
396
515
|
}
|
516
|
+
return result.output;
|
397
517
|
}
|
398
|
-
const
|
399
|
-
|
400
|
-
|
401
|
-
}
|
402
|
-
return serialOutput;
|
518
|
+
const output = await func();
|
519
|
+
await recordOperationResult(client, workflowID, functionID, functionName, true, { output });
|
520
|
+
return output;
|
403
521
|
}
|
404
522
|
async durableSleepms(workflowID, functionID, durationMS) {
|
523
|
+
let resolveNotification;
|
524
|
+
const cancelPromise = new Promise((resolve) => {
|
525
|
+
resolveNotification = resolve;
|
526
|
+
});
|
527
|
+
const cbr = this.cancelWakeupMap.registerCallback(workflowID, resolveNotification);
|
528
|
+
try {
|
529
|
+
let timeoutPromise = Promise.resolve();
|
530
|
+
const { promise, cancel: timeoutCancel } = await this.#durableSleep(workflowID, functionID, durationMS);
|
531
|
+
timeoutPromise = promise;
|
532
|
+
try {
|
533
|
+
await Promise.race([cancelPromise, timeoutPromise]);
|
534
|
+
}
|
535
|
+
finally {
|
536
|
+
timeoutCancel();
|
537
|
+
}
|
538
|
+
}
|
539
|
+
finally {
|
540
|
+
this.cancelWakeupMap.deregisterCallback(cbr);
|
541
|
+
}
|
542
|
+
await this.checkIfCanceled(workflowID);
|
543
|
+
}
|
544
|
+
async #durableSleep(workflowID, functionID, durationMS, maxSleepPerIteration) {
|
545
|
+
if (maxSleepPerIteration === undefined)
|
546
|
+
maxSleepPerIteration = durationMS;
|
405
547
|
const curTime = Date.now();
|
406
548
|
let endTimeMs = curTime + durationMS;
|
407
|
-
const
|
408
|
-
|
409
|
-
|
410
|
-
|
549
|
+
const client = await this.pool.connect();
|
550
|
+
try {
|
551
|
+
const res = await this.#getOperationResultAndThrowIfCancelled(client, workflowID, functionID);
|
552
|
+
if (res) {
|
553
|
+
if (res.functionName !== exports.DBOS_FUNCNAME_SLEEP) {
|
554
|
+
throw new error_1.DBOSUnexpectedStepError(workflowID, functionID, exports.DBOS_FUNCNAME_SLEEP, res.functionName);
|
555
|
+
}
|
556
|
+
endTimeMs = JSON.parse(res.output);
|
557
|
+
}
|
558
|
+
else {
|
559
|
+
await recordOperationResult(client, workflowID, functionID, exports.DBOS_FUNCNAME_SLEEP, false, {
|
560
|
+
output: JSON.stringify(endTimeMs),
|
561
|
+
});
|
411
562
|
}
|
412
|
-
|
563
|
+
return {
|
564
|
+
...(0, utils_1.cancellableSleep)(Math.max(Math.min(maxSleepPerIteration, endTimeMs - curTime), 0)),
|
565
|
+
endTime: endTimeMs,
|
566
|
+
};
|
413
567
|
}
|
414
|
-
|
415
|
-
|
568
|
+
finally {
|
569
|
+
client.release();
|
416
570
|
}
|
417
|
-
return (0, utils_1.cancellableSleep)(Math.max(endTimeMs - curTime, 0));
|
418
571
|
}
|
419
572
|
nullTopic = '__null__topic__';
|
420
573
|
async send(workflowID, functionID, destinationID, message, topic) {
|
@@ -422,11 +575,11 @@ class PostgresSystemDatabase {
|
|
422
575
|
const client = await this.pool.connect();
|
423
576
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
424
577
|
try {
|
425
|
-
await this.
|
578
|
+
await this.#runAndRecordResult(client, exports.DBOS_FUNCNAME_SEND, workflowID, functionID, async () => {
|
426
579
|
await client.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.notifications (destination_uuid, topic, message) VALUES ($1, $2, $3);`, [destinationID, topic, message]);
|
427
580
|
await client.query('COMMIT');
|
428
581
|
return undefined;
|
429
|
-
}
|
582
|
+
});
|
430
583
|
}
|
431
584
|
catch (error) {
|
432
585
|
await client.query('ROLLBACK');
|
@@ -446,44 +599,63 @@ class PostgresSystemDatabase {
|
|
446
599
|
async recv(workflowID, functionID, timeoutFunctionID, topic, timeoutSeconds = dbos_executor_1.DBOSExecutor.defaultNotificationTimeoutSec) {
|
447
600
|
topic = topic ?? this.nullTopic;
|
448
601
|
// First, check for previous executions.
|
449
|
-
const res = await this.
|
450
|
-
if (res
|
451
|
-
if (res.
|
452
|
-
throw new error_1.DBOSUnexpectedStepError(workflowID, functionID, exports.DBOS_FUNCNAME_RECV, res.
|
602
|
+
const res = await this.getOperationResultAndThrowIfCancelled(workflowID, functionID);
|
603
|
+
if (res) {
|
604
|
+
if (res.functionName !== exports.DBOS_FUNCNAME_RECV) {
|
605
|
+
throw new error_1.DBOSUnexpectedStepError(workflowID, functionID, exports.DBOS_FUNCNAME_RECV, res.functionName);
|
453
606
|
}
|
454
|
-
return res.
|
607
|
+
return res.output;
|
455
608
|
}
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
//
|
609
|
+
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
610
|
+
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
611
|
+
while (true) {
|
612
|
+
// register the key with the global notifications listener.
|
460
613
|
let resolveNotification;
|
461
614
|
const messagePromise = new Promise((resolve) => {
|
462
615
|
resolveNotification = resolve;
|
463
616
|
});
|
464
617
|
const payload = `${workflowID}::${topic}`;
|
465
|
-
this.notificationsMap
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
const { promise, cancel } = await this.durableSleepms(workflowID, timeoutFunctionID, timeoutSeconds * 1000);
|
470
|
-
timeoutPromise = promise;
|
471
|
-
timeoutCancel = cancel;
|
472
|
-
}
|
473
|
-
catch (e) {
|
474
|
-
this.logger.error(e);
|
475
|
-
delete this.notificationsMap[payload];
|
476
|
-
timeoutCancel();
|
477
|
-
throw new Error('durable sleepms failed');
|
478
|
-
}
|
618
|
+
const cbr = this.notificationsMap.registerCallback(payload, resolveNotification);
|
619
|
+
const crh = this.cancelWakeupMap.registerCallback(workflowID, (_res) => {
|
620
|
+
resolveNotification();
|
621
|
+
});
|
479
622
|
try {
|
480
|
-
await
|
623
|
+
await this.checkIfCanceled(workflowID);
|
624
|
+
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
625
|
+
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;
|
626
|
+
if (initRecvRows.length !== 0)
|
627
|
+
break;
|
628
|
+
const ct = Date.now();
|
629
|
+
if (finishTime && ct > finishTime)
|
630
|
+
break; // Time's up
|
631
|
+
let timeoutPromise = Promise.resolve();
|
632
|
+
let timeoutCancel = () => { };
|
633
|
+
if (timeoutms) {
|
634
|
+
const { promise, cancel, endTime } = await this.#durableSleep(workflowID, timeoutFunctionID, timeoutms, this.dbPollingIntervalEventMs);
|
635
|
+
timeoutPromise = promise;
|
636
|
+
timeoutCancel = cancel;
|
637
|
+
finishTime = endTime;
|
638
|
+
}
|
639
|
+
else {
|
640
|
+
let poll = finishTime ? finishTime - ct : this.dbPollingIntervalEventMs;
|
641
|
+
poll = Math.min(this.dbPollingIntervalEventMs, poll);
|
642
|
+
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
643
|
+
timeoutPromise = promise;
|
644
|
+
timeoutCancel = cancel;
|
645
|
+
}
|
646
|
+
try {
|
647
|
+
await Promise.race([messagePromise, timeoutPromise]);
|
648
|
+
}
|
649
|
+
finally {
|
650
|
+
timeoutCancel();
|
651
|
+
}
|
481
652
|
}
|
482
653
|
finally {
|
483
|
-
|
484
|
-
|
654
|
+
this.notificationsMap.deregisterCallback(cbr);
|
655
|
+
this.cancelWakeupMap.deregisterCallback(crh);
|
485
656
|
}
|
486
657
|
}
|
658
|
+
await this.checkIfCanceled(workflowID);
|
487
659
|
// Transactionally consume and return the message if it's in the DB, otherwise return null.
|
488
660
|
let message = null;
|
489
661
|
const client = await this.pool.connect();
|
@@ -507,7 +679,7 @@ class PostgresSystemDatabase {
|
|
507
679
|
if (finalRecvRows.length > 0) {
|
508
680
|
message = finalRecvRows[0].message;
|
509
681
|
}
|
510
|
-
await
|
682
|
+
await recordOperationResult(client, workflowID, functionID, exports.DBOS_FUNCNAME_RECV, true, { output: message });
|
511
683
|
await client.query(`COMMIT`);
|
512
684
|
}
|
513
685
|
catch (e) {
|
@@ -524,7 +696,7 @@ class PostgresSystemDatabase {
|
|
524
696
|
const client = await this.pool.connect();
|
525
697
|
try {
|
526
698
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
527
|
-
await this.
|
699
|
+
await this.#runAndRecordResult(client, exports.DBOS_FUNCNAME_SETEVENT, workflowID, functionID, async () => {
|
528
700
|
await client.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events (workflow_uuid, key, value)
|
529
701
|
VALUES ($1, $2, $3)
|
530
702
|
ON CONFLICT (workflow_uuid, key)
|
@@ -532,7 +704,7 @@ class PostgresSystemDatabase {
|
|
532
704
|
RETURNING workflow_uuid;`, [workflowID, key, message]);
|
533
705
|
await client.query('COMMIT');
|
534
706
|
return undefined;
|
535
|
-
}
|
707
|
+
});
|
536
708
|
}
|
537
709
|
catch (e) {
|
538
710
|
this.logger.error(e);
|
@@ -546,51 +718,59 @@ class PostgresSystemDatabase {
|
|
546
718
|
async getEvent(workflowID, key, timeoutSeconds, callerWorkflow) {
|
547
719
|
// Check if the operation has been done before for OAOO (only do this inside a workflow).
|
548
720
|
if (callerWorkflow) {
|
549
|
-
const res = await this.
|
550
|
-
if (res
|
551
|
-
if (res.
|
552
|
-
throw new error_1.DBOSUnexpectedStepError(callerWorkflow.workflowID, callerWorkflow.functionID, exports.DBOS_FUNCNAME_GETEVENT, res.
|
721
|
+
const res = await this.getOperationResultAndThrowIfCancelled(callerWorkflow.workflowID, callerWorkflow.functionID);
|
722
|
+
if (res) {
|
723
|
+
if (res.functionName !== exports.DBOS_FUNCNAME_GETEVENT) {
|
724
|
+
throw new error_1.DBOSUnexpectedStepError(callerWorkflow.workflowID, callerWorkflow.functionID, exports.DBOS_FUNCNAME_GETEVENT, res.functionName);
|
553
725
|
}
|
554
|
-
return res.
|
726
|
+
return res.output;
|
555
727
|
}
|
556
728
|
}
|
557
729
|
// Get the return the value. if it's in the DB, otherwise return null.
|
558
730
|
let value = null;
|
559
731
|
const payloadKey = `${workflowID}::${key}`;
|
732
|
+
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
733
|
+
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
560
734
|
// Register the key with the global notifications listener first... we do not want to look in the DB first
|
561
735
|
// or that would cause a timing hole.
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
736
|
+
while (true) {
|
737
|
+
let resolveNotification;
|
738
|
+
const valuePromise = new Promise((resolve) => {
|
739
|
+
resolveNotification = resolve;
|
740
|
+
});
|
741
|
+
const cbr = this.workflowEventsMap.registerCallback(payloadKey, resolveNotification);
|
742
|
+
const crh = callerWorkflow?.workflowID
|
743
|
+
? this.cancelWakeupMap.registerCallback(callerWorkflow.workflowID, (_res) => {
|
744
|
+
resolveNotification();
|
745
|
+
})
|
746
|
+
: undefined;
|
747
|
+
try {
|
748
|
+
if (callerWorkflow?.workflowID)
|
749
|
+
await this.checkIfCanceled(callerWorkflow?.workflowID);
|
750
|
+
// Check if the key is already in the DB, then wait for the notification if it isn't.
|
751
|
+
const initRecvRows = (await this.pool.query(`SELECT key, value
|
752
|
+
FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
|
753
|
+
WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
|
754
|
+
if (initRecvRows.length > 0) {
|
755
|
+
value = initRecvRows[0].value;
|
756
|
+
break;
|
757
|
+
}
|
758
|
+
const ct = Date.now();
|
759
|
+
if (finishTime && ct > finishTime)
|
760
|
+
break; // Time's up
|
577
761
|
// If we have a callerWorkflow, we want a durable sleep, otherwise, not
|
578
762
|
let timeoutPromise = Promise.resolve();
|
579
763
|
let timeoutCancel = () => { };
|
580
|
-
if (callerWorkflow) {
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
}
|
586
|
-
catch (e) {
|
587
|
-
this.logger.error(e);
|
588
|
-
delete this.workflowEventsMap[payloadKey];
|
589
|
-
throw new Error('durable sleepms failed');
|
590
|
-
}
|
764
|
+
if (callerWorkflow && timeoutms) {
|
765
|
+
const { promise, cancel, endTime } = await this.#durableSleep(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutms, this.dbPollingIntervalEventMs);
|
766
|
+
timeoutPromise = promise;
|
767
|
+
timeoutCancel = cancel;
|
768
|
+
finishTime = endTime;
|
591
769
|
}
|
592
770
|
else {
|
593
|
-
|
771
|
+
let poll = finishTime ? finishTime - ct : this.dbPollingIntervalEventMs;
|
772
|
+
poll = Math.min(this.dbPollingIntervalEventMs, poll);
|
773
|
+
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
594
774
|
timeoutPromise = promise;
|
595
775
|
timeoutCancel = cancel;
|
596
776
|
}
|
@@ -600,78 +780,112 @@ class PostgresSystemDatabase {
|
|
600
780
|
finally {
|
601
781
|
timeoutCancel();
|
602
782
|
}
|
603
|
-
const finalRecvRows = (await this.pool.query(`
|
604
|
-
SELECT value
|
605
|
-
FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
|
606
|
-
WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
|
607
|
-
if (finalRecvRows.length > 0) {
|
608
|
-
value = finalRecvRows[0].value;
|
609
|
-
}
|
610
783
|
}
|
611
|
-
|
612
|
-
|
613
|
-
|
784
|
+
finally {
|
785
|
+
this.workflowEventsMap.deregisterCallback(cbr);
|
786
|
+
if (crh)
|
787
|
+
this.cancelWakeupMap.deregisterCallback(crh);
|
788
|
+
}
|
614
789
|
}
|
615
790
|
// Record the output if it is inside a workflow.
|
616
791
|
if (callerWorkflow) {
|
617
|
-
await this.recordOperationResult(callerWorkflow.workflowID, callerWorkflow.functionID, {
|
618
|
-
serialOutput: value,
|
619
|
-
functionName: exports.DBOS_FUNCNAME_GETEVENT,
|
620
|
-
}, true);
|
792
|
+
await this.recordOperationResult(callerWorkflow.workflowID, callerWorkflow.functionID, exports.DBOS_FUNCNAME_GETEVENT, true, { output: value });
|
621
793
|
}
|
622
794
|
return value;
|
623
795
|
}
|
624
796
|
async setWorkflowStatus(workflowID, status, resetRecoveryAttempts) {
|
625
|
-
await this.
|
797
|
+
const client = await this.pool.connect();
|
798
|
+
try {
|
799
|
+
await updateWorkflowStatus(client, workflowID, status, { update: { resetRecoveryAttempts } });
|
800
|
+
}
|
801
|
+
finally {
|
802
|
+
client.release();
|
803
|
+
}
|
804
|
+
}
|
805
|
+
#setWFCancelMap(workflowID) {
|
806
|
+
if (this.runningWorkflowMap.has(workflowID)) {
|
807
|
+
this.workflowCancellationMap.set(workflowID, true);
|
808
|
+
}
|
809
|
+
this.cancelWakeupMap.callCallbacks(workflowID);
|
626
810
|
}
|
811
|
+
#clearWFCancelMap(workflowID) {
|
812
|
+
if (this.workflowCancellationMap.has(workflowID)) {
|
813
|
+
this.workflowCancellationMap.delete(workflowID);
|
814
|
+
}
|
815
|
+
}
|
816
|
+
// TODO: make cancel throw an error if the workflow doesn't exist.
|
627
817
|
async cancelWorkflow(workflowID) {
|
628
818
|
const client = await this.pool.connect();
|
629
819
|
try {
|
630
|
-
await client.query('BEGIN');
|
820
|
+
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
631
821
|
// Remove workflow from queues table
|
632
|
-
await client
|
633
|
-
|
634
|
-
|
635
|
-
|
822
|
+
await deleteQueuedWorkflows(client, workflowID);
|
823
|
+
const statusResult = await getWorkflowStatusValue(client, workflowID);
|
824
|
+
if (!statusResult || statusResult === workflow_1.StatusString.SUCCESS || statusResult === workflow_1.StatusString.ERROR) {
|
825
|
+
await client.query('COMMIT');
|
826
|
+
return;
|
827
|
+
}
|
828
|
+
await updateWorkflowStatus(client, workflowID, workflow_1.StatusString.CANCELLED);
|
636
829
|
await client.query('COMMIT');
|
637
830
|
}
|
638
831
|
catch (error) {
|
832
|
+
this.logger.error(error);
|
639
833
|
await client.query('ROLLBACK');
|
640
834
|
throw error;
|
641
835
|
}
|
642
836
|
finally {
|
643
837
|
client.release();
|
644
838
|
}
|
839
|
+
this.#setWFCancelMap(workflowID);
|
840
|
+
}
|
841
|
+
async #checkIfCanceled(client, workflowID) {
|
842
|
+
if (this.workflowCancellationMap.get(workflowID) === true) {
|
843
|
+
throw new error_1.DBOSWorkflowCancelledError(workflowID);
|
844
|
+
}
|
845
|
+
const statusValue = await getWorkflowStatusValue(client, workflowID);
|
846
|
+
if (statusValue === workflow_1.StatusString.CANCELLED) {
|
847
|
+
throw new error_1.DBOSWorkflowCancelledError(workflowID);
|
848
|
+
}
|
849
|
+
}
|
850
|
+
async checkIfCanceled(workflowID) {
|
851
|
+
const client = await this.pool.connect();
|
852
|
+
try {
|
853
|
+
await this.#checkIfCanceled(client, workflowID);
|
854
|
+
}
|
855
|
+
finally {
|
856
|
+
client.release();
|
857
|
+
}
|
645
858
|
}
|
646
859
|
async resumeWorkflow(workflowID) {
|
860
|
+
this.#clearWFCancelMap(workflowID);
|
647
861
|
const client = await this.pool.connect();
|
648
862
|
try {
|
649
|
-
await client.query('BEGIN');
|
650
|
-
await client.query('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
|
863
|
+
await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ');
|
651
864
|
// Check workflow status. If it is complete, do nothing.
|
652
|
-
const statusResult = await client
|
653
|
-
|
654
|
-
|
655
|
-
statusResult
|
656
|
-
|
657
|
-
|
865
|
+
const statusResult = await getWorkflowStatusValue(client, workflowID);
|
866
|
+
if (!statusResult || statusResult === workflow_1.StatusString.SUCCESS || statusResult === workflow_1.StatusString.ERROR) {
|
867
|
+
await client.query('ROLLBACK');
|
868
|
+
if (!statusResult) {
|
869
|
+
if (statusResult === undefined) {
|
870
|
+
throw new error_1.DBOSNonExistentWorkflowError(`Workflow ${workflowID} does not exist`);
|
871
|
+
}
|
872
|
+
}
|
658
873
|
return;
|
659
874
|
}
|
660
875
|
// Remove the workflow from the queues table so resume can safely be called on an ENQUEUED workflow
|
661
|
-
await client
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
DO NOTHING;
|
671
|
-
`, [workflowID, utils_1.INTERNAL_QUEUE_NAME]);
|
876
|
+
await deleteQueuedWorkflows(client, workflowID);
|
877
|
+
await updateWorkflowStatus(client, workflowID, workflow_1.StatusString.ENQUEUED, {
|
878
|
+
update: {
|
879
|
+
queueName: utils_1.INTERNAL_QUEUE_NAME,
|
880
|
+
resetRecoveryAttempts: true,
|
881
|
+
},
|
882
|
+
throwOnFailure: false,
|
883
|
+
});
|
884
|
+
await enqueueWorkflow(client, workflowID, utils_1.INTERNAL_QUEUE_NAME);
|
672
885
|
await client.query('COMMIT');
|
673
886
|
}
|
674
887
|
catch (error) {
|
888
|
+
this.logger.error(error);
|
675
889
|
await client.query('ROLLBACK');
|
676
890
|
throw error;
|
677
891
|
}
|
@@ -679,76 +893,166 @@ class PostgresSystemDatabase {
|
|
679
893
|
client.release();
|
680
894
|
}
|
681
895
|
}
|
896
|
+
registerRunningWorkflow(workflowID, workflowPromise) {
|
897
|
+
// Need to await for the workflow and capture errors.
|
898
|
+
const awaitWorkflowPromise = workflowPromise
|
899
|
+
.catch((error) => {
|
900
|
+
this.logger.debug('Captured error in awaitWorkflowPromise: ' + error);
|
901
|
+
})
|
902
|
+
.finally(() => {
|
903
|
+
// Remove itself from pending workflow map.
|
904
|
+
this.runningWorkflowMap.delete(workflowID);
|
905
|
+
this.workflowCancellationMap.delete(workflowID);
|
906
|
+
});
|
907
|
+
this.runningWorkflowMap.set(workflowID, awaitWorkflowPromise);
|
908
|
+
}
|
909
|
+
async awaitRunningWorkflows() {
|
910
|
+
if (this.runningWorkflowMap.size > 0) {
|
911
|
+
this.logger.info('Waiting for pending workflows to finish.');
|
912
|
+
await Promise.allSettled(this.runningWorkflowMap.values());
|
913
|
+
}
|
914
|
+
if (this.workflowEventsMap.map.size > 0) {
|
915
|
+
this.logger.warn('Workflow events map is not empty - shutdown is not clean.');
|
916
|
+
//throw new Error('Workflow events map is not empty - shutdown is not clean.');
|
917
|
+
}
|
918
|
+
if (this.notificationsMap.map.size > 0) {
|
919
|
+
this.logger.warn('Message notification map is not empty - shutdown is not clean.');
|
920
|
+
//throw new Error('Message notification map is not empty - shutdown is not clean.');
|
921
|
+
}
|
922
|
+
}
|
682
923
|
async getWorkflowStatus(workflowID, callerID, callerFN) {
|
683
|
-
|
684
|
-
const sv = await this.runAsStep(async () => {
|
924
|
+
const funcGetStatus = async () => {
|
685
925
|
const statuses = await this.listWorkflows({ workflowIDs: [workflowID] });
|
686
926
|
const status = statuses.find((s) => s.workflowUUID === workflowID);
|
687
927
|
return status ? JSON.stringify(status) : null;
|
688
|
-
}
|
689
|
-
|
928
|
+
};
|
929
|
+
if (callerID && callerFN) {
|
930
|
+
const client = await this.pool.connect();
|
931
|
+
try {
|
932
|
+
// Check if the operation has been done before for OAOO (only do this inside a workflow).
|
933
|
+
const json = await this.#runAndRecordResult(client, exports.DBOS_FUNCNAME_GETSTATUS, callerID, callerFN, async () => {
|
934
|
+
const statuses = await this.listWorkflows({ workflowIDs: [workflowID] });
|
935
|
+
const status = statuses.find((s) => s.workflowUUID === workflowID);
|
936
|
+
return status ? JSON.stringify(status) : null;
|
937
|
+
});
|
938
|
+
return parseStatus(json);
|
939
|
+
}
|
940
|
+
finally {
|
941
|
+
client.release();
|
942
|
+
}
|
943
|
+
}
|
944
|
+
else {
|
945
|
+
const json = await funcGetStatus();
|
946
|
+
return parseStatus(json);
|
947
|
+
}
|
948
|
+
function parseStatus(json) {
|
949
|
+
return json ? JSON.parse(json) : null;
|
950
|
+
}
|
690
951
|
}
|
691
|
-
async awaitWorkflowResult(workflowID,
|
692
|
-
const
|
693
|
-
|
952
|
+
async awaitWorkflowResult(workflowID, timeoutSeconds, callerID, timerFuncID) {
|
953
|
+
const timeoutms = timeoutSeconds !== undefined ? timeoutSeconds * 1000 : undefined;
|
954
|
+
let finishTime = timeoutms !== undefined ? Date.now() + timeoutms : undefined;
|
694
955
|
while (true) {
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
956
|
+
let resolveNotification;
|
957
|
+
const statusPromise = new Promise((resolve) => {
|
958
|
+
resolveNotification = resolve;
|
959
|
+
});
|
960
|
+
const irh = this.cancelWakeupMap.registerCallback(workflowID, (_res) => {
|
961
|
+
resolveNotification();
|
962
|
+
});
|
963
|
+
const crh = callerID
|
964
|
+
? this.cancelWakeupMap.registerCallback(callerID, (_res) => {
|
965
|
+
resolveNotification();
|
966
|
+
})
|
967
|
+
: undefined;
|
968
|
+
try {
|
969
|
+
if (callerID)
|
970
|
+
await this.checkIfCanceled(callerID);
|
971
|
+
try {
|
972
|
+
const { rows } = await this.pool.query(`SELECT status, output, error FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status
|
973
|
+
WHERE workflow_uuid=$1`, [workflowID]);
|
974
|
+
if (rows.length > 0) {
|
975
|
+
const status = rows[0].status;
|
976
|
+
if (status === workflow_1.StatusString.SUCCESS) {
|
977
|
+
return { output: rows[0].output };
|
978
|
+
}
|
979
|
+
else if (status === workflow_1.StatusString.ERROR) {
|
980
|
+
return { error: rows[0].error };
|
981
|
+
}
|
982
|
+
else if (status === workflow_1.StatusString.CANCELLED) {
|
983
|
+
return { cancelled: true };
|
984
|
+
}
|
985
|
+
else {
|
986
|
+
// Status is not actionable
|
987
|
+
}
|
988
|
+
}
|
700
989
|
}
|
701
|
-
|
702
|
-
|
990
|
+
catch (e) {
|
991
|
+
const err = e;
|
992
|
+
this.logger.error(`Exception from system database: ${err}`);
|
993
|
+
throw err;
|
703
994
|
}
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
995
|
+
const ct = Date.now();
|
996
|
+
if (finishTime && ct > finishTime)
|
997
|
+
return undefined; // Time's up
|
998
|
+
let timeoutPromise = Promise.resolve();
|
999
|
+
let timeoutCancel = () => { };
|
1000
|
+
if (timerFuncID !== undefined && callerID !== undefined && timeoutms !== undefined) {
|
1001
|
+
const { promise, cancel, endTime } = await this.#durableSleep(callerID, timerFuncID, timeoutms, this.dbPollingIntervalResultMs);
|
1002
|
+
finishTime = endTime;
|
1003
|
+
timeoutPromise = promise;
|
1004
|
+
timeoutCancel = cancel;
|
709
1005
|
}
|
710
1006
|
else {
|
711
|
-
|
1007
|
+
let poll = finishTime ? finishTime - ct : this.dbPollingIntervalResultMs;
|
1008
|
+
poll = Math.min(this.dbPollingIntervalResultMs, poll);
|
1009
|
+
const { promise, cancel } = (0, utils_1.cancellableSleep)(poll);
|
1010
|
+
timeoutPromise = promise;
|
1011
|
+
timeoutCancel = cancel;
|
1012
|
+
}
|
1013
|
+
try {
|
1014
|
+
await Promise.race([statusPromise, timeoutPromise]);
|
1015
|
+
}
|
1016
|
+
finally {
|
1017
|
+
timeoutCancel();
|
712
1018
|
}
|
713
1019
|
}
|
714
|
-
|
715
|
-
|
1020
|
+
finally {
|
1021
|
+
this.cancelWakeupMap.deregisterCallback(irh);
|
1022
|
+
if (crh)
|
1023
|
+
this.cancelWakeupMap.deregisterCallback(crh);
|
716
1024
|
}
|
717
1025
|
}
|
718
|
-
return undefined;
|
719
1026
|
}
|
720
1027
|
/* BACKGROUND PROCESSES */
|
721
1028
|
/**
|
722
1029
|
* A background process that listens for notifications from Postgres then signals the appropriate
|
723
1030
|
* workflow listener by resolving its promise.
|
724
1031
|
*/
|
725
|
-
async listenForNotifications() {
|
1032
|
+
async #listenForNotifications() {
|
726
1033
|
this.notificationsClient = await this.pool.connect();
|
727
1034
|
await this.notificationsClient.query('LISTEN dbos_notifications_channel;');
|
728
1035
|
await this.notificationsClient.query('LISTEN dbos_workflow_events_channel;');
|
729
1036
|
const handler = (msg) => {
|
1037
|
+
if (!this.shouldUseDBNotifications)
|
1038
|
+
return; // Testing parameter
|
730
1039
|
if (msg.channel === 'dbos_notifications_channel') {
|
731
|
-
if (msg.payload
|
732
|
-
this.notificationsMap
|
1040
|
+
if (msg.payload) {
|
1041
|
+
this.notificationsMap.callCallbacks(msg.payload);
|
733
1042
|
}
|
734
1043
|
}
|
735
|
-
else {
|
736
|
-
if (msg.payload
|
737
|
-
this.workflowEventsMap
|
1044
|
+
else if (msg.channel === 'dbos_workflow_events_channel') {
|
1045
|
+
if (msg.payload) {
|
1046
|
+
this.workflowEventsMap.callCallbacks(msg.payload);
|
738
1047
|
}
|
739
1048
|
}
|
740
1049
|
};
|
741
1050
|
this.notificationsClient.on('notification', handler);
|
742
1051
|
}
|
743
1052
|
// Event dispatcher queries / updates
|
744
|
-
async getEventDispatchState(
|
745
|
-
const res = await this.pool.query(`
|
746
|
-
|
747
|
-
FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.event_dispatch_kv
|
748
|
-
WHERE workflow_fn_name = $1
|
749
|
-
AND service_name = $2
|
750
|
-
AND key = $3;
|
751
|
-
`, [wfn, svc, key]);
|
1053
|
+
async getEventDispatchState(service, workflowName, key) {
|
1054
|
+
const res = await this.pool.query(`SELECT * FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.event_dispatch_kv
|
1055
|
+
WHERE workflow_fn_name = $1 AND service_name = $2 AND key = $3;`, [workflowName, service, key]);
|
752
1056
|
if (res.rows.length === 0)
|
753
1057
|
return undefined;
|
754
1058
|
return {
|
@@ -763,19 +1067,18 @@ class PostgresSystemDatabase {
|
|
763
1067
|
};
|
764
1068
|
}
|
765
1069
|
async upsertEventDispatchState(state) {
|
766
|
-
const res = await this.pool.query(`
|
767
|
-
INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.event_dispatch_kv (
|
1070
|
+
const res = await this.pool.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.event_dispatch_kv (
|
768
1071
|
service_name, workflow_fn_name, key, value, update_time, update_seq)
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
1072
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
1073
|
+
ON CONFLICT (service_name, workflow_fn_name, key)
|
1074
|
+
DO UPDATE SET
|
1075
|
+
update_time = GREATEST(EXCLUDED.update_time, event_dispatch_kv.update_time),
|
1076
|
+
update_seq = GREATEST(EXCLUDED.update_seq, event_dispatch_kv.update_seq),
|
1077
|
+
value = CASE WHEN (EXCLUDED.update_time > event_dispatch_kv.update_time
|
1078
|
+
OR EXCLUDED.update_seq > event_dispatch_kv.update_seq
|
1079
|
+
OR (event_dispatch_kv.update_time IS NULL and event_dispatch_kv.update_seq IS NULL)
|
1080
|
+
) THEN EXCLUDED.value ELSE event_dispatch_kv.value END
|
1081
|
+
RETURNING value, update_time, update_seq;`, [state.service, state.workflowFnName, state.key, state.value, state.updateTime, state.updateSeq]);
|
779
1082
|
return {
|
780
1083
|
service: state.service,
|
781
1084
|
workflowFnName: state.workflowFnName,
|
@@ -824,7 +1127,7 @@ class PostgresSystemDatabase {
|
|
824
1127
|
query = query.offset(input.offset);
|
825
1128
|
}
|
826
1129
|
const rows = await query;
|
827
|
-
return rows.map(
|
1130
|
+
return rows.map(mapWorkflowStatus);
|
828
1131
|
}
|
829
1132
|
async listQueuedWorkflows(input) {
|
830
1133
|
const schemaName = dbos_executor_1.DBOSExecutor.systemDBSchemaName;
|
@@ -855,30 +1158,7 @@ class PostgresSystemDatabase {
|
|
855
1158
|
query = query.offset(input.offset);
|
856
1159
|
}
|
857
1160
|
const rows = await query;
|
858
|
-
return rows.map(
|
859
|
-
}
|
860
|
-
static #mapWorkflowStatus(row) {
|
861
|
-
return {
|
862
|
-
workflowUUID: row.workflow_uuid,
|
863
|
-
status: row.status,
|
864
|
-
workflowName: row.name,
|
865
|
-
output: row.output ? row.output : null,
|
866
|
-
error: row.error ? row.error : null,
|
867
|
-
workflowClassName: row.class_name ?? '',
|
868
|
-
workflowConfigName: row.config_name ?? '',
|
869
|
-
queueName: row.queue_name,
|
870
|
-
authenticatedUser: row.authenticated_user,
|
871
|
-
assumedRole: row.assumed_role,
|
872
|
-
authenticatedRoles: JSON.parse(row.authenticated_roles),
|
873
|
-
request: JSON.parse(row.request),
|
874
|
-
executorId: row.executor_id,
|
875
|
-
createdAt: Number(row.created_at),
|
876
|
-
updatedAt: Number(row.updated_at),
|
877
|
-
applicationVersion: row.application_version,
|
878
|
-
applicationID: row.application_id,
|
879
|
-
recoveryAttempts: Number(row.recovery_attempts),
|
880
|
-
input: row.inputs,
|
881
|
-
};
|
1161
|
+
return rows.map(mapWorkflowStatus);
|
882
1162
|
}
|
883
1163
|
async getWorkflowQueue(input) {
|
884
1164
|
// Create the initial query with a join to workflow_status table to get executor_id
|
@@ -920,29 +1200,29 @@ class PostgresSystemDatabase {
|
|
920
1200
|
return { workflows };
|
921
1201
|
}
|
922
1202
|
async enqueueWorkflow(workflowId, queueName) {
|
923
|
-
await this.pool.
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
1203
|
+
const client = await this.pool.connect();
|
1204
|
+
try {
|
1205
|
+
await enqueueWorkflow(client, workflowId, queueName);
|
1206
|
+
}
|
1207
|
+
finally {
|
1208
|
+
client.release();
|
1209
|
+
}
|
929
1210
|
}
|
930
|
-
async clearQueueAssignment(
|
1211
|
+
async clearQueueAssignment(workflowID) {
|
931
1212
|
const client = await this.pool.connect();
|
932
1213
|
try {
|
1214
|
+
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
933
1215
|
// Reset the start time in the queue to mark it as not started
|
934
|
-
const wqRes = await client.query(`
|
935
|
-
|
936
|
-
|
937
|
-
WHERE workflow_uuid = $1 AND completed_at_epoch_ms IS NULL;
|
938
|
-
`, [workflowId]);
|
1216
|
+
const wqRes = await client.query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
|
1217
|
+
SET started_at_epoch_ms = NULL
|
1218
|
+
WHERE workflow_uuid = $1 AND completed_at_epoch_ms IS NULL;`, [workflowID]);
|
939
1219
|
// If no rows were affected, the workflow is not anymore in the queue or was already completed
|
940
1220
|
if (wqRes.rowCount === 0) {
|
941
1221
|
await client.query('ROLLBACK');
|
942
1222
|
return false;
|
943
1223
|
}
|
944
1224
|
// Reset the status of the task to "ENQUEUED"
|
945
|
-
await
|
1225
|
+
await updateWorkflowStatus(client, workflowID, workflow_1.StatusString.ENQUEUED);
|
946
1226
|
await client.query('COMMIT');
|
947
1227
|
return true;
|
948
1228
|
}
|
@@ -954,24 +1234,25 @@ class PostgresSystemDatabase {
|
|
954
1234
|
client.release();
|
955
1235
|
}
|
956
1236
|
}
|
957
|
-
async dequeueWorkflow(
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
1237
|
+
async dequeueWorkflow(workflowID, queue) {
|
1238
|
+
const client = await this.pool.connect();
|
1239
|
+
try {
|
1240
|
+
if (queue.rateLimit) {
|
1241
|
+
const time = Date.now();
|
1242
|
+
await client.query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
|
1243
|
+
SET completed_at_epoch_ms = $2
|
1244
|
+
WHERE workflow_uuid = $1;`, [workflowID, time]);
|
1245
|
+
}
|
1246
|
+
else {
|
1247
|
+
await deleteQueuedWorkflows(client, workflowID);
|
1248
|
+
}
|
965
1249
|
}
|
966
|
-
|
967
|
-
|
968
|
-
DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
|
969
|
-
WHERE workflow_uuid = $1;
|
970
|
-
`, [workflowId]);
|
1250
|
+
finally {
|
1251
|
+
client.release();
|
971
1252
|
}
|
972
1253
|
}
|
973
1254
|
async findAndMarkStartableWorkflows(queue, executorID, appVersion) {
|
974
|
-
const startTimeMs =
|
1255
|
+
const startTimeMs = Date.now();
|
975
1256
|
const limiterPeriodMS = queue.rateLimit ? queue.rateLimit.periodSec * 1000 : 0;
|
976
1257
|
const claimedIDs = [];
|
977
1258
|
await this.knexDB.transaction(async (trx) => {
|