@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.
Files changed (47) hide show
  1. package/dist/src/client.d.ts +12 -1
  2. package/dist/src/client.d.ts.map +1 -1
  3. package/dist/src/client.js +28 -2
  4. package/dist/src/client.js.map +1 -1
  5. package/dist/src/conductor/conductor.d.ts.map +1 -1
  6. package/dist/src/conductor/conductor.js +26 -11
  7. package/dist/src/conductor/conductor.js.map +1 -1
  8. package/dist/src/conductor/protocol.d.ts +20 -4
  9. package/dist/src/conductor/protocol.d.ts.map +1 -1
  10. package/dist/src/conductor/protocol.js +23 -4
  11. package/dist/src/conductor/protocol.js.map +1 -1
  12. package/dist/src/dbos-executor.d.ts +14 -17
  13. package/dist/src/dbos-executor.d.ts.map +1 -1
  14. package/dist/src/dbos-executor.js +74 -174
  15. package/dist/src/dbos-executor.js.map +1 -1
  16. package/dist/src/dbos-runtime/cli.d.ts.map +1 -1
  17. package/dist/src/dbos-runtime/cli.js +87 -15
  18. package/dist/src/dbos-runtime/cli.js.map +1 -1
  19. package/dist/src/dbos-runtime/workflow_management.d.ts +13 -13
  20. package/dist/src/dbos-runtime/workflow_management.d.ts.map +1 -1
  21. package/dist/src/dbos-runtime/workflow_management.js +90 -108
  22. package/dist/src/dbos-runtime/workflow_management.js.map +1 -1
  23. package/dist/src/dbos.d.ts +29 -11
  24. package/dist/src/dbos.d.ts.map +1 -1
  25. package/dist/src/dbos.js +53 -36
  26. package/dist/src/dbos.js.map +1 -1
  27. package/dist/src/error.d.ts +11 -6
  28. package/dist/src/error.d.ts.map +1 -1
  29. package/dist/src/error.js +28 -17
  30. package/dist/src/error.js.map +1 -1
  31. package/dist/src/eventreceiver.d.ts +13 -8
  32. package/dist/src/eventreceiver.d.ts.map +1 -1
  33. package/dist/src/httpServer/server.d.ts.map +1 -1
  34. package/dist/src/httpServer/server.js +38 -12
  35. package/dist/src/httpServer/server.js.map +1 -1
  36. package/dist/src/scheduler/scheduler.js +1 -1
  37. package/dist/src/scheduler/scheduler.js.map +1 -1
  38. package/dist/src/system_database.d.ts +68 -59
  39. package/dist/src/system_database.d.ts.map +1 -1
  40. package/dist/src/system_database.js +715 -434
  41. package/dist/src/system_database.js.map +1 -1
  42. package/dist/src/workflow.d.ts +8 -0
  43. package/dist/src/workflow.d.ts.map +1 -1
  44. package/dist/src/workflow.js +7 -38
  45. package/dist/src/workflow.js.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. 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
- notificationsMap = {};
53
- workflowEventsMap = {};
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
- await this.listenForNotifications();
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, args, maxRetries) {
127
- const result = await this.pool.query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status (
128
- workflow_uuid,
129
- status,
130
- name,
131
- class_name,
132
- config_name,
133
- queue_name,
134
- authenticated_user,
135
- assumed_role,
136
- authenticated_roles,
137
- request,
138
- executor_id,
139
- application_version,
140
- application_id,
141
- created_at,
142
- recovery_attempts,
143
- updated_at
144
- ) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
145
- ON CONFLICT (workflow_uuid)
146
- DO UPDATE SET
147
- recovery_attempts = workflow_status.recovery_attempts + 1,
148
- updated_at = EXCLUDED.updated_at,
149
- executor_id = EXCLUDED.executor_id
150
- RETURNING recovery_attempts, status, name, class_name, config_name, queue_name`, [
151
- initStatus.workflowUUID,
152
- initStatus.status,
153
- initStatus.workflowName,
154
- initStatus.workflowClassName,
155
- initStatus.workflowConfigName,
156
- initStatus.queueName,
157
- initStatus.authenticatedUser,
158
- initStatus.assumedRole,
159
- utils_1.DBOSJSON.stringify(initStatus.authenticatedRoles),
160
- utils_1.DBOSJSON.stringify(initStatus.request),
161
- initStatus.executorId,
162
- initStatus.applicationVersion,
163
- initStatus.applicationID,
164
- initStatus.createdAt,
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
- const wRes = await (client ?? this.pool).query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status
217
- SET ${rec} status=$2, output=$3, error=$4, updated_at=$5 WHERE workflow_uuid=$1`, [workflowID, status, update.output, update.error, Date.now()]);
218
- if (wRes.rowCount !== 1) {
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.recordWorkflowStatusChange(workflowID, workflow_1.StatusString.SUCCESS, { output: status.output });
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.recordWorkflowStatusChange(workflowID, workflow_1.StatusString.ERROR, { error: status.error });
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 FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE status=$1 AND executor_id=$2 AND application_version=$3`, [workflow_1.StatusString.PENDING, executorID, appVersion]);
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 WHERE workflow_uuid=$1`, [workflowID]);
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 utils_1.DBOSJSON.parse(rows[0].inputs);
411
+ return rows[0].inputs;
241
412
  }
242
- async getOperationResult(workflowID, functionID, client) {
243
- const { rows } = await (client ?? this.pool).query(`SELECT output, error, child_workflow_id, function_name
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
- res: {
252
- res: rows[0].output,
253
- err: rows[0].error,
254
- child: rows[0].child_workflow_id,
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, rec, checkConflict, client) {
443
+ async recordOperationResult(workflowID, functionID, functionName, checkConflict, options = {}) {
444
+ const client = await this.pool.connect();
265
445
  try {
266
- await (client ?? this.pool).query(`INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs
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
- catch (error) {
280
- const err = error;
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(originalWorkflowID, forkedWorkflowId, startStep = 0) {
295
- const workflowStatus = await this.getWorkflowStatus(originalWorkflowID);
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 ${originalWorkflowID} does not exist`);
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 query = `
303
- INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status (
304
- workflow_uuid,
305
- status,
306
- name,
307
- class_name,
308
- config_name,
309
- queue_name,
310
- authenticated_user,
311
- assumed_role,
312
- authenticated_roles,
313
- request,
314
- output,
315
- executor_id,
316
- application_version,
317
- application_id,
318
- created_at,
319
- recovery_attempts,
320
- updated_at
321
- ) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
322
- `;
323
- await client.query(query, [
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
- INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.operation_outputs (
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.query(`
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 forkedWorkflowId;
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 runAsStep(callback, functionName, workflowID, functionID, client) {
388
- if (workflowID !== undefined && functionID !== undefined) {
389
- const res = await this.getOperationResult(workflowID, functionID, client);
390
- if (res.res !== undefined) {
391
- if (res.res.functionName !== functionName) {
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 serialOutput = await callback();
399
- if (workflowID !== undefined && functionID !== undefined) {
400
- await this.recordOperationResult(workflowID, functionID, { serialOutput, functionName }, true, client);
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 res = await this.getOperationResult(workflowID, functionID);
408
- if (res.res !== undefined) {
409
- if (res.res.functionName !== exports.DBOS_FUNCNAME_SLEEP) {
410
- throw new error_1.DBOSUnexpectedStepError(workflowID, functionID, exports.DBOS_FUNCNAME_SLEEP, res.res.functionName);
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
- endTimeMs = JSON.parse(res.res.res);
563
+ return {
564
+ ...(0, utils_1.cancellableSleep)(Math.max(Math.min(maxSleepPerIteration, endTimeMs - curTime), 0)),
565
+ endTime: endTimeMs,
566
+ };
413
567
  }
414
- else {
415
- await this.recordOperationResult(workflowID, functionID, { serialOutput: JSON.stringify(endTimeMs), functionName: exports.DBOS_FUNCNAME_SLEEP }, false);
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.runAsStep(async () => {
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
- }, exports.DBOS_FUNCNAME_SEND, workflowID, functionID, client);
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.getOperationResult(workflowID, functionID);
450
- if (res.res) {
451
- if (res.res.functionName !== exports.DBOS_FUNCNAME_RECV) {
452
- throw new error_1.DBOSUnexpectedStepError(workflowID, functionID, exports.DBOS_FUNCNAME_RECV, res.res.functionName);
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.res.res;
607
+ return res.output;
455
608
  }
456
- // Check if the key is already in the DB, then wait for the notification if it isn't.
457
- 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;
458
- if (initRecvRows.length === 0) {
459
- // Then, register the key with the global notifications listener.
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[payload] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
466
- let timeoutPromise = Promise.resolve();
467
- let timeoutCancel = () => { };
468
- try {
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 Promise.race([messagePromise, timeoutPromise]);
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
- timeoutCancel();
484
- delete this.notificationsMap[payload];
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 this.recordOperationResult(workflowID, functionID, { serialOutput: message, functionName: exports.DBOS_FUNCNAME_RECV }, true, client);
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.runAsStep(async () => {
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
- }, exports.DBOS_FUNCNAME_SETEVENT, workflowID, functionID, client);
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.getOperationResult(callerWorkflow.workflowID, callerWorkflow.functionID);
550
- if (res.res !== undefined) {
551
- if (res.res.functionName !== exports.DBOS_FUNCNAME_GETEVENT) {
552
- throw new error_1.DBOSUnexpectedStepError(callerWorkflow.workflowID, callerWorkflow.functionID, exports.DBOS_FUNCNAME_GETEVENT, res.res.functionName);
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.res.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
- let resolveNotification;
563
- const valuePromise = new Promise((resolve) => {
564
- resolveNotification = resolve;
565
- });
566
- this.workflowEventsMap[payloadKey] = resolveNotification; // The resolver assignment in the Promise definition runs synchronously.
567
- try {
568
- // Check if the key is already in the DB, then wait for the notification if it isn't.
569
- const initRecvRows = (await this.pool.query(`
570
- SELECT key, value
571
- FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_events
572
- WHERE workflow_uuid=$1 AND key=$2;`, [workflowID, key])).rows;
573
- if (initRecvRows.length > 0) {
574
- value = initRecvRows[0].value;
575
- }
576
- else {
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
- try {
582
- const { promise, cancel } = await this.durableSleepms(callerWorkflow.workflowID, callerWorkflow.timeoutFunctionID ?? -1, timeoutSeconds * 1000);
583
- timeoutPromise = promise;
584
- timeoutCancel = cancel;
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
- const { promise, cancel } = (0, utils_1.cancellableSleep)(timeoutSeconds * 1000);
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
- finally {
613
- delete this.workflowEventsMap[payloadKey];
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.recordWorkflowStatusChange(workflowID, status, { resetRecoveryAttempts });
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.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
633
- WHERE workflow_uuid = $1`, [workflowID]);
634
- // Should we check if it is incomplete first?
635
- await this.recordWorkflowStatusChange(workflowID, workflow_1.StatusString.CANCELLED, {}, client);
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.query(`SELECT status FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status
653
- WHERE workflow_uuid = $1`, [workflowID]);
654
- if (statusResult.rows.length === 0 ||
655
- statusResult.rows[0].status === workflow_1.StatusString.SUCCESS ||
656
- statusResult.rows[0].status === workflow_1.StatusString.ERROR) {
657
- await client.query('COMMIT');
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.query(`DELETE FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
662
- WHERE workflow_uuid = $1`, [workflowID]);
663
- await client.query(`UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status
664
- SET status = $1, queue_name= $2, recovery_attempts = 0
665
- WHERE workflow_uuid = $3`, [workflow_1.StatusString.ENQUEUED, utils_1.INTERNAL_QUEUE_NAME, workflowID]);
666
- await client.query(`
667
- INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue (workflow_uuid, queue_name)
668
- VALUES ($1, $2)
669
- ON CONFLICT (workflow_uuid)
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
- // Check if the operation has been done before for OAOO (only do this inside a workflow).
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
- }, exports.DBOS_FUNCNAME_GETSTATUS, callerID, callerFN);
689
- return sv ? JSON.parse(sv) : null;
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, timeoutms) {
692
- const pollingIntervalMs = 1000;
693
- const et = timeoutms !== undefined ? new Date().getTime() + timeoutms : undefined;
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
- const { rows } = await this.pool.query(`SELECT status, output, error FROM ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_status WHERE workflow_uuid=$1`, [workflowID]);
696
- if (rows.length > 0) {
697
- const status = rows[0].status;
698
- if (status === workflow_1.StatusString.SUCCESS) {
699
- return { res: rows[0].output };
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
- else if (status === workflow_1.StatusString.ERROR) {
702
- return { err: rows[0].error };
990
+ catch (e) {
991
+ const err = e;
992
+ this.logger.error(`Exception from system database: ${err}`);
993
+ throw err;
703
994
  }
704
- }
705
- if (et !== undefined) {
706
- const ct = new Date().getTime();
707
- if (et > ct) {
708
- await (0, utils_1.sleepms)(Math.min(pollingIntervalMs, et - ct));
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
- break;
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
- else {
715
- await (0, utils_1.sleepms)(pollingIntervalMs);
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 && msg.payload in this.notificationsMap) {
732
- this.notificationsMap[msg.payload]();
1040
+ if (msg.payload) {
1041
+ this.notificationsMap.callCallbacks(msg.payload);
733
1042
  }
734
1043
  }
735
- else {
736
- if (msg.payload && msg.payload in this.workflowEventsMap) {
737
- this.workflowEventsMap[msg.payload]();
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(svc, wfn, key) {
745
- const res = await this.pool.query(`
746
- SELECT *
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
- VALUES ($1, $2, $3, $4, $5, $6)
770
- ON CONFLICT (service_name, workflow_fn_name, key)
771
- DO UPDATE SET
772
- update_time = GREATEST(EXCLUDED.update_time, event_dispatch_kv.update_time),
773
- update_seq = GREATEST(EXCLUDED.update_seq, event_dispatch_kv.update_seq),
774
- value = CASE WHEN (EXCLUDED.update_time > event_dispatch_kv.update_time OR EXCLUDED.update_seq > event_dispatch_kv.update_seq OR
775
- (event_dispatch_kv.update_time IS NULL and event_dispatch_kv.update_seq IS NULL))
776
- THEN EXCLUDED.value ELSE event_dispatch_kv.value END
777
- RETURNING value, update_time, update_seq;
778
- `, [state.service, state.workflowFnName, state.key, state.value, state.updateTime, state.updateSeq]);
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(PostgresSystemDatabase.#mapWorkflowStatus);
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(PostgresSystemDatabase.#mapWorkflowStatus);
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.query(`
924
- INSERT INTO ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue (workflow_uuid, queue_name)
925
- VALUES ($1, $2)
926
- ON CONFLICT (workflow_uuid)
927
- DO NOTHING;
928
- `, [workflowId, queueName]);
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(workflowId) {
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
- UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
936
- SET started_at_epoch_ms = NULL
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 this.recordWorkflowStatusChange(workflowId, workflow_1.StatusString.ENQUEUED, {}, client);
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(workflowId, queue) {
958
- if (queue.rateLimit) {
959
- const time = new Date().getTime();
960
- await this.pool.query(`
961
- UPDATE ${dbos_executor_1.DBOSExecutor.systemDBSchemaName}.workflow_queue
962
- SET completed_at_epoch_ms = $2
963
- WHERE workflow_uuid = $1;
964
- `, [workflowId, time]);
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
- else {
967
- await this.pool.query(`
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 = new Date().getTime();
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) => {