@dbos-inc/dbos-enterprise 5.4.1-preview → 5.4.2-preview

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +0 -2
  2. package/dist/src/auto_upgrade.d.ts +7 -20
  3. package/dist/src/auto_upgrade.d.ts.map +1 -1
  4. package/dist/src/auto_upgrade.js +7 -51
  5. package/dist/src/auto_upgrade.js.map +1 -1
  6. package/dist/src/cockroach.d.ts +7 -1
  7. package/dist/src/cockroach.d.ts.map +1 -1
  8. package/dist/src/cockroach.js +7 -1
  9. package/dist/src/cockroach.js.map +1 -1
  10. package/dist/src/conductor/conductor.d.ts +8 -5
  11. package/dist/src/conductor/conductor.d.ts.map +1 -1
  12. package/dist/src/conductor/conductor.js +765 -735
  13. package/dist/src/conductor/conductor.js.map +1 -1
  14. package/dist/src/conductor/protocol.d.ts +31 -7
  15. package/dist/src/conductor/protocol.d.ts.map +1 -1
  16. package/dist/src/conductor/protocol.js +84 -17
  17. package/dist/src/conductor/protocol.js.map +1 -1
  18. package/dist/src/export.d.ts +7 -0
  19. package/dist/src/export.d.ts.map +1 -1
  20. package/dist/src/export.js +15 -21
  21. package/dist/src/export.js.map +1 -1
  22. package/dist/src/fork.d.ts +7 -0
  23. package/dist/src/fork.d.ts.map +1 -1
  24. package/dist/src/fork.js +11 -3
  25. package/dist/src/fork.js.map +1 -1
  26. package/dist/src/index.d.ts +8 -0
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +10 -1
  29. package/dist/src/index.js.map +1 -1
  30. package/dist/src/locks.d.ts +20 -0
  31. package/dist/src/locks.d.ts.map +1 -0
  32. package/dist/src/locks.js +47 -0
  33. package/dist/src/locks.js.map +1 -0
  34. package/dist/src/observability.d.ts +8 -0
  35. package/dist/src/observability.d.ts.map +1 -1
  36. package/dist/src/observability.js +63 -43
  37. package/dist/src/observability.js.map +1 -1
  38. package/dist/src/retention.d.ts +16 -34
  39. package/dist/src/retention.d.ts.map +1 -1
  40. package/dist/src/retention.js +148 -176
  41. package/dist/src/retention.js.map +1 -1
  42. package/dist/src/sharding/index.d.ts +9 -0
  43. package/dist/src/sharding/index.d.ts.map +1 -0
  44. package/dist/src/sharding/index.js +13 -0
  45. package/dist/src/sharding/index.js.map +1 -0
  46. package/dist/src/sharding/migrations.d.ts +20 -0
  47. package/dist/src/sharding/migrations.d.ts.map +1 -0
  48. package/dist/src/sharding/migrations.js +486 -0
  49. package/dist/src/sharding/migrations.js.map +1 -0
  50. package/dist/src/sharding/schema.d.ts +9 -0
  51. package/dist/src/sharding/schema.d.ts.map +1 -0
  52. package/dist/src/sharding/schema.js +12 -0
  53. package/dist/src/sharding/schema.js.map +1 -0
  54. package/dist/src/sharding/sys_db.d.ts +101 -0
  55. package/dist/src/sharding/sys_db.d.ts.map +1 -0
  56. package/dist/src/sharding/sys_db.js +717 -0
  57. package/dist/src/sharding/sys_db.js.map +1 -0
  58. package/package.json +3 -3
@@ -1,3 +1,10 @@
1
+ // Copyright (c) 2026 DBOS, Inc. All rights reserved.
2
+ //
3
+ // This file is part of Transact Enterprise, proprietary software licensed under the
4
+ // DBOS Conductor Software License: https://www.dbos.dev/conductor-license
5
+ // You may not use, copy, modify, or distribute this file except as that license permits.
6
+ // Disabling or circumventing the entitlement checks in @dbos-inc/dbos-sdk's src/enterprise.ts
7
+ // that gate it is a material breach of that license.
1
8
  "use strict";
2
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
10
  if (k2 === undefined) k2 = k;
@@ -40,9 +47,9 @@ const fork_1 = require("../fork");
40
47
  const observability_1 = require("../observability");
41
48
  const retention_1 = require("../retention");
42
49
  const scheduler_1 = require("@dbos-inc/dbos-sdk/internal/scheduler/scheduler");
50
+ const shard_keys_1 = require("@dbos-inc/dbos-sdk/internal/shard_keys");
43
51
  const gzip = (0, node_util_1.promisify)(zlib.gzip);
44
52
  const gunzip = (0, node_util_1.promisify)(zlib.gunzip);
45
- // Commands that exist only to move workflow data, refused in metadata-only mode.
46
53
  const DATA_MESSAGE_TYPES = new Set([
47
54
  protocol.MessageType.GET_WORKFLOW_EVENTS,
48
55
  protocol.MessageType.GET_WORKFLOW_NOTIFICATIONS,
@@ -50,17 +57,26 @@ const DATA_MESSAGE_TYPES = new Set([
50
57
  protocol.MessageType.EXPORT_WORKFLOW,
51
58
  protocol.MessageType.IMPORT_WORKFLOW,
52
59
  ]);
60
+ function positionsByKey(ids, shardKeys) {
61
+ const groups = new Map();
62
+ ids.forEach((workflowID, i) => {
63
+ const key = shardKeys.get(workflowID);
64
+ const group = groups.get(key) ?? [];
65
+ group.push(i);
66
+ groups.set(key, group);
67
+ });
68
+ return Array.from(groups.values());
69
+ }
53
70
  class ConductorWebsocket {
54
71
  dbosExec;
55
72
  url;
56
73
  websocket = undefined;
57
- isShuttingDown = false; // Is in the process of shutting down the connection
58
- pingPeriodMs = 20000; // Time in milliseconds to wait before sending a ping message to the conductor
59
- pingTimeoutMs = 15000; // Time in milliseconds to wait for a response to a ping message before considering the connection dead
60
- pingIntervalTimeout = undefined; // Combined interval and timeout for pinging Conductor
74
+ isShuttingDown = false;
75
+ pingPeriodMs = 20000;
76
+ pingTimeoutMs = 15000;
77
+ pingIntervalTimeout = undefined;
61
78
  reconnectDelayMs = 1000;
62
79
  reconnectTimeout = undefined;
63
- // The retention round running off the message loop, if there is one.
64
80
  retentionRound = undefined;
65
81
  appName;
66
82
  executorMetadata;
@@ -77,7 +93,6 @@ class ConductorWebsocket {
77
93
  start() {
78
94
  this.dispatchLoop();
79
95
  }
80
- /** Logs a command's exception in full and returns the error message Conductor may see. */
81
96
  reportException(context, e) {
82
97
  const errorMsg = `${context}: ${e.message}`;
83
98
  this.dbosExec.logger.error(errorMsg);
@@ -91,9 +106,8 @@ class ConductorWebsocket {
91
106
  clearInterval(currPing?.interval);
92
107
  clearTimeout(currPing?.timeout);
93
108
  if (currWebsocket) {
94
- currWebsocket.terminate(); // Terminate the existing connection
109
+ currWebsocket.terminate();
95
110
  if (this.websocket === currWebsocket) {
96
- // Only clear if it's the same websocket
97
111
  this.websocket = undefined;
98
112
  }
99
113
  }
@@ -105,23 +119,747 @@ class ConductorWebsocket {
105
119
  this.dispatchLoop();
106
120
  }, this.reconnectDelayMs);
107
121
  }
122
+ async #handleMessage(currWebsocket, baseMsg, shardKeys) {
123
+ const msgType = baseMsg.type;
124
+ let errorMsg = undefined;
125
+ if (this.metadataOnlyMode && DATA_MESSAGE_TYPES.has(msgType)) {
126
+ const refusedResp = new protocol.BaseResponse(msgType, baseMsg.request_id, `${msgType} is not allowed in conductor metadata-only mode`);
127
+ currWebsocket.send(JSON.stringify(refusedResp));
128
+ return;
129
+ }
130
+ switch (msgType) {
131
+ case protocol.MessageType.EXECUTOR_INFO:
132
+ const infoResp = new protocol.ExecutorInfoResponse(baseMsg.request_id, utils_1.globalParams.executorID, utils_1.globalParams.appVersion, (0, node_os_1.hostname)(), 'typescript', utils_1.globalParams.dbosVersion, undefined, this.executorMetadata, this.dbosExec.systemDatabase.sharded);
133
+ currWebsocket.send(JSON.stringify(infoResp));
134
+ this.dbosExec.logger.info('Connected to DBOS conductor');
135
+ break;
136
+ case protocol.MessageType.RECOVERY:
137
+ const recoveryMsg = baseMsg;
138
+ let success = true;
139
+ try {
140
+ await this.dbosExec.recoverPendingWorkflows(recoveryMsg.executor_ids);
141
+ }
142
+ catch (e) {
143
+ errorMsg = this.reportException('Exception encountered when recovering workflows', e);
144
+ success = false;
145
+ }
146
+ const recoveryResp = new protocol.RecoveryResponse(baseMsg.request_id, success, errorMsg);
147
+ currWebsocket.send(JSON.stringify(recoveryResp));
148
+ break;
149
+ case protocol.MessageType.CANCEL:
150
+ const cancelMsg = baseMsg;
151
+ const cancelIds = cancelMsg.workflow_ids?.length ? cancelMsg.workflow_ids : [cancelMsg.workflow_id];
152
+ let cancelSuccess = true;
153
+ try {
154
+ for (const group of positionsByKey(cancelIds, shardKeys)) {
155
+ await this.dbosExec.systemDatabase.cancelWorkflows(group.map((i) => cancelIds[i]), cancelMsg.cancel_children);
156
+ }
157
+ }
158
+ catch (e) {
159
+ errorMsg = this.reportException(`Exception encountered when cancelling workflow(s) ${String(cancelIds)}`, e);
160
+ cancelSuccess = false;
161
+ }
162
+ const cancelResp = new protocol.CancelResponse(baseMsg.request_id, cancelSuccess, errorMsg);
163
+ currWebsocket.send(JSON.stringify(cancelResp));
164
+ break;
165
+ case protocol.MessageType.DELETE:
166
+ const deleteMsg = baseMsg;
167
+ const deleteIds = deleteMsg.workflow_ids?.length ? deleteMsg.workflow_ids : [deleteMsg.workflow_id];
168
+ let deleteSuccess = true;
169
+ try {
170
+ for (const group of positionsByKey(deleteIds, shardKeys)) {
171
+ await this.dbosExec.systemDatabase.deleteWorkflows(group.map((i) => deleteIds[i]), deleteMsg.delete_children ?? false);
172
+ }
173
+ }
174
+ catch (e) {
175
+ errorMsg = this.reportException(`Exception encountered when deleting workflow(s) ${String(deleteIds)}`, e);
176
+ deleteSuccess = false;
177
+ }
178
+ const deleteResp = new protocol.DeleteResponse(baseMsg.request_id, deleteSuccess, errorMsg);
179
+ currWebsocket.send(JSON.stringify(deleteResp));
180
+ break;
181
+ case protocol.MessageType.RESUME:
182
+ const resumeMsg = baseMsg;
183
+ const resumeIds = resumeMsg.workflow_ids?.length ? resumeMsg.workflow_ids : [resumeMsg.workflow_id];
184
+ let resumeSuccess = true;
185
+ try {
186
+ for (const group of positionsByKey(resumeIds, shardKeys)) {
187
+ await this.dbosExec.systemDatabase.resumeWorkflows(group.map((i) => resumeIds[i]), resumeMsg.queue_name);
188
+ }
189
+ }
190
+ catch (e) {
191
+ errorMsg = this.reportException(`Exception encountered when resuming workflow(s) ${String(resumeIds)}`, e);
192
+ resumeSuccess = false;
193
+ }
194
+ const resumeResp = new protocol.ResumeResponse(baseMsg.request_id, resumeSuccess, errorMsg);
195
+ currWebsocket.send(JSON.stringify(resumeResp));
196
+ break;
197
+ case protocol.MessageType.REWIND_WORKFLOW:
198
+ const rewindMsg = baseMsg;
199
+ const rewindBody = rewindMsg.body;
200
+ let rewindSuccess = true;
201
+ try {
202
+ await this.dbosExec.rewindWorkflow(rewindBody.workflow_id, rewindBody.start_step ?? 0, {
203
+ applicationVersion: rewindBody.application_version,
204
+ queueName: rewindBody.queue_name,
205
+ queuePartitionKey: rewindBody.queue_partition_key,
206
+ });
207
+ }
208
+ catch (e) {
209
+ errorMsg = this.reportException(`Exception encountered when rewinding workflow ${rewindBody.workflow_id} to step ${rewindBody.start_step ?? 0}`, e);
210
+ rewindSuccess = false;
211
+ }
212
+ const rewindResp = new protocol.RewindWorkflowResponse(baseMsg.request_id, rewindSuccess, errorMsg);
213
+ currWebsocket.send(JSON.stringify(rewindResp));
214
+ break;
215
+ case protocol.MessageType.FORK_WORKFLOW:
216
+ const forkMsg = baseMsg;
217
+ let newWorkflowID = forkMsg.body.new_workflow_id;
218
+ try {
219
+ newWorkflowID = await this.dbosExec.forkWorkflow(forkMsg.body.workflow_id, forkMsg.body.start_step, {
220
+ newWorkflowID: newWorkflowID,
221
+ applicationVersion: forkMsg.body.application_version,
222
+ queueName: forkMsg.body.queue_name,
223
+ queuePartitionKey: forkMsg.body.queue_partition_key,
224
+ });
225
+ }
226
+ catch (e) {
227
+ errorMsg = this.reportException(`Exception encountered when forking workflow ${forkMsg.body.workflow_id} to new workflow ${newWorkflowID} on step ${forkMsg.body.start_step}, app version ${forkMsg.body.application_version}`, e);
228
+ newWorkflowID = undefined;
229
+ }
230
+ const forkResp = new protocol.ForkWorkflowResponse(baseMsg.request_id, newWorkflowID, errorMsg);
231
+ currWebsocket.send(JSON.stringify(forkResp));
232
+ break;
233
+ case protocol.MessageType.FORK_FROM_FAILURE:
234
+ const forkFromFailureMsg = baseMsg;
235
+ const forkFromFailureBody = forkFromFailureMsg.body;
236
+ let forkedWorkflowIDs = undefined;
237
+ try {
238
+ forkedWorkflowIDs = await (0, fork_1.forkFromFailure)(this.dbosExec.systemDatabase, forkFromFailureBody.workflow_ids, {
239
+ applicationVersion: forkFromFailureBody.application_version,
240
+ queueName: forkFromFailureBody.queue_name,
241
+ queuePartitionKey: forkFromFailureBody.queue_partition_key,
242
+ fromLastFailure: forkFromFailureBody.from_last_failure ?? false,
243
+ fromLastStep: forkFromFailureBody.from_last_step ?? false,
244
+ fromStep: forkFromFailureBody.from_step,
245
+ fromStepName: forkFromFailureBody.from_step_name,
246
+ });
247
+ }
248
+ catch (e) {
249
+ errorMsg = this.reportException('Exception encountered when bulk forking workflows', e);
250
+ }
251
+ const forkFromFailureResp = new protocol.ForkFromFailureResponse(baseMsg.request_id, forkedWorkflowIDs, errorMsg);
252
+ currWebsocket.send(JSON.stringify(forkFromFailureResp));
253
+ break;
254
+ case protocol.MessageType.LIST_WORKFLOWS:
255
+ const listWFMsg = baseMsg;
256
+ const body = listWFMsg.body;
257
+ const listWFReq = {
258
+ workflowIDs: body.workflow_uuids,
259
+ shardKey: body.shard_key ?? undefined,
260
+ parentShardKey: body.parent_shard_key ?? undefined,
261
+ workflowName: body.workflow_name,
262
+ authenticatedUser: body.authenticated_user,
263
+ startTime: body.start_time,
264
+ endTime: body.end_time,
265
+ completedAfter: body.completed_after,
266
+ completedBefore: body.completed_before,
267
+ dequeuedAfter: body.dequeued_after,
268
+ dequeuedBefore: body.dequeued_before,
269
+ status: body.status,
270
+ applicationVersion: body.application_version,
271
+ forkedFrom: body.forked_from,
272
+ parentWorkflowID: body.parent_workflow_id,
273
+ queueName: body.queue_name,
274
+ limit: body.limit,
275
+ offset: body.offset,
276
+ sortDesc: body.sort_desc,
277
+ workflow_id_prefix: body.workflow_id_prefix,
278
+ loadInput: (body.load_input ?? false) && !this.metadataOnlyMode,
279
+ loadOutput: (body.load_output ?? false) && !this.metadataOnlyMode,
280
+ executorId: body.executor_id,
281
+ queuesOnly: body.queues_only,
282
+ wasForkedFrom: body.was_forked_from,
283
+ isFork: body.is_fork,
284
+ hasParent: body.has_parent,
285
+ attributes: body.attributes,
286
+ scheduleName: body.schedule_name,
287
+ applicationName: body.application_name,
288
+ };
289
+ let workflowsOutput = [];
290
+ try {
291
+ const workflows = await this.dbosExec.listWorkflows(listWFReq);
292
+ workflowsOutput = workflows.map((wf) => new protocol.WorkflowsOutput(wf));
293
+ }
294
+ catch (e) {
295
+ errorMsg = this.reportException('Exception encountered when listing workflows', e);
296
+ }
297
+ const wfsResp = new protocol.ListWorkflowsResponse(listWFMsg.request_id, workflowsOutput, errorMsg);
298
+ currWebsocket.send(JSON.stringify(wfsResp));
299
+ break;
300
+ case protocol.MessageType.LIST_QUEUED_WORKFLOWS:
301
+ const listQueuedWFMsg = baseMsg;
302
+ const bodyQueued = listQueuedWFMsg.body;
303
+ const listQueuedWFReq = {
304
+ workflowIDs: bodyQueued.workflow_uuids,
305
+ shardKey: bodyQueued.shard_key ?? undefined,
306
+ parentShardKey: bodyQueued.parent_shard_key ?? undefined,
307
+ workflowName: bodyQueued.workflow_name,
308
+ authenticatedUser: bodyQueued.authenticated_user,
309
+ startTime: bodyQueued.start_time,
310
+ endTime: bodyQueued.end_time,
311
+ completedAfter: bodyQueued.completed_after,
312
+ completedBefore: bodyQueued.completed_before,
313
+ dequeuedAfter: bodyQueued.dequeued_after,
314
+ dequeuedBefore: bodyQueued.dequeued_before,
315
+ status: bodyQueued.status,
316
+ applicationVersion: bodyQueued.application_version,
317
+ forkedFrom: bodyQueued.forked_from,
318
+ parentWorkflowID: bodyQueued.parent_workflow_id,
319
+ queueName: bodyQueued.queue_name,
320
+ limit: bodyQueued.limit,
321
+ offset: bodyQueued.offset,
322
+ sortDesc: bodyQueued.sort_desc,
323
+ workflow_id_prefix: bodyQueued.workflow_id_prefix,
324
+ loadInput: (bodyQueued.load_input ?? false) && !this.metadataOnlyMode,
325
+ loadOutput: (bodyQueued.load_output ?? false) && !this.metadataOnlyMode,
326
+ executorId: bodyQueued.executor_id,
327
+ wasForkedFrom: bodyQueued.was_forked_from,
328
+ isFork: bodyQueued.is_fork,
329
+ hasParent: bodyQueued.has_parent,
330
+ attributes: bodyQueued.attributes,
331
+ scheduleName: bodyQueued.schedule_name,
332
+ applicationName: bodyQueued.application_name,
333
+ };
334
+ let queuedWFOutput = [];
335
+ try {
336
+ const workflows = await this.dbosExec.listQueuedWorkflows(listQueuedWFReq);
337
+ queuedWFOutput = workflows.map((wf) => new protocol.WorkflowsOutput(wf));
338
+ }
339
+ catch (e) {
340
+ errorMsg = this.reportException('Exception encountered when listing queued workflows', e);
341
+ }
342
+ const queuedWfsResp = new protocol.ListQueuedWorkflowsResponse(listQueuedWFMsg.request_id, queuedWFOutput, errorMsg);
343
+ currWebsocket.send(JSON.stringify(queuedWfsResp));
344
+ break;
345
+ case protocol.MessageType.GET_WORKFLOW:
346
+ const getWFMsg = baseMsg;
347
+ let wfOutput = undefined;
348
+ try {
349
+ const workflows = await this.dbosExec.listWorkflows({
350
+ workflowIDs: [getWFMsg.workflow_id],
351
+ shardKey: this.dbosExec.systemDatabase.keyHint(getWFMsg.workflow_id),
352
+ loadInput: (getWFMsg.load_input ?? true) && !this.metadataOnlyMode,
353
+ loadOutput: (getWFMsg.load_output ?? true) && !this.metadataOnlyMode,
354
+ });
355
+ if (workflows.length > 0) {
356
+ wfOutput = new protocol.WorkflowsOutput(workflows[0]);
357
+ }
358
+ }
359
+ catch (e) {
360
+ errorMsg = this.reportException(`Exception encountered when getting workflow ${getWFMsg.workflow_id}`, e);
361
+ }
362
+ const getWFResp = new protocol.GetWorkflowResponse(getWFMsg.request_id, wfOutput, errorMsg);
363
+ currWebsocket.send(JSON.stringify(getWFResp));
364
+ break;
365
+ case protocol.MessageType.EXIST_PENDING_WORKFLOWS:
366
+ const existPendingMsg = baseMsg;
367
+ let hasPendingWFs = false;
368
+ try {
369
+ const pendingWFs = await (0, observability_1.getPendingWorkflows)(this.dbosExec.systemDatabase, existPendingMsg.executor_id, existPendingMsg.application_version);
370
+ hasPendingWFs = pendingWFs.length > 0;
371
+ }
372
+ catch (e) {
373
+ errorMsg = this.reportException('Exception encountered when checking for pending workflows', e);
374
+ }
375
+ const existPendingResp = new protocol.ExistPendingWorkflowsResponse(baseMsg.request_id, hasPendingWFs, errorMsg);
376
+ currWebsocket.send(JSON.stringify(existPendingResp));
377
+ break;
378
+ case protocol.MessageType.LIST_STEPS:
379
+ const listStepsMessage = baseMsg;
380
+ let workflowSteps = undefined;
381
+ try {
382
+ const stepsInfo = await this.dbosExec.listWorkflowSteps(listStepsMessage.workflow_id, (listStepsMessage.load_output ?? true) && !this.metadataOnlyMode, { limit: listStepsMessage.limit, offset: listStepsMessage.offset });
383
+ workflowSteps = stepsInfo?.map((i) => new protocol.WorkflowSteps(i));
384
+ }
385
+ catch (e) {
386
+ errorMsg = this.reportException(`Exception encountered when listing steps ${listStepsMessage.workflow_id}`, e);
387
+ }
388
+ const listStepsResponse = new protocol.ListStepsResponse(listStepsMessage.request_id, workflowSteps, errorMsg);
389
+ currWebsocket.send(JSON.stringify(listStepsResponse));
390
+ break;
391
+ case protocol.MessageType.RETENTION:
392
+ const retentionMessage = baseMsg;
393
+ const retentionBody = retentionMessage.body;
394
+ let rowsThreshold = retentionBody.gc_rows_threshold;
395
+ if (rowsThreshold !== undefined && rowsThreshold !== null && this.dbosExec.systemDatabase.sharded) {
396
+ this.dbosExec.logger.warn("Ignoring Conductor's rows-based retention: a sharded system database supports only time-based retention.");
397
+ rowsThreshold = undefined;
398
+ }
399
+ if (this.retentionRound !== undefined) {
400
+ this.dbosExec.logger.warn('Skipping retention: the previous round on this executor is still running.');
401
+ }
402
+ else {
403
+ this.retentionRound = (async () => {
404
+ try {
405
+ await (0, retention_1.garbageCollect)(this.dbosExec.systemDatabase, retentionBody.gc_cutoff_epoch_ms, rowsThreshold, { batchSize: retentionBody.gc_batch_size || undefined });
406
+ if (retentionBody.timeout_cutoff_epoch_ms) {
407
+ await (0, retention_1.globalTimeout)(this.dbosExec.systemDatabase, retentionBody.timeout_cutoff_epoch_ms);
408
+ }
409
+ }
410
+ catch (e) {
411
+ if (this.isShuttingDown) {
412
+ this.dbosExec.logger.debug('Retention interrupted by shutdown');
413
+ return;
414
+ }
415
+ this.dbosExec.logger.error(`Exception encountered during enforcing retention policy: ${e.message}`);
416
+ }
417
+ })().finally(() => {
418
+ this.retentionRound = undefined;
419
+ });
420
+ }
421
+ currWebsocket.send(JSON.stringify(new protocol.RetentionResponse(retentionMessage.request_id, true)));
422
+ break;
423
+ case protocol.MessageType.GET_METRICS:
424
+ const getMetricsMessage = baseMsg;
425
+ this.dbosExec.logger.debug(`Received metrics request for time range ${getMetricsMessage.start_time} to ${getMetricsMessage.end_time}`);
426
+ let metricsData = [];
427
+ if (getMetricsMessage.metric_class === 'workflow_step_count') {
428
+ try {
429
+ const sysMetrics = await (0, observability_1.getMetrics)(this.dbosExec.systemDatabase, getMetricsMessage.start_time, getMetricsMessage.end_time, getMetricsMessage.application_name);
430
+ metricsData = sysMetrics.map((m) => new protocol.MetricDataOutput(m.metricType, m.metricName, m.value));
431
+ }
432
+ catch (e) {
433
+ errorMsg = this.reportException('Exception encountered when getting metrics', e);
434
+ }
435
+ }
436
+ else {
437
+ errorMsg = `Unexpected metric class: ${getMetricsMessage.metric_class}`;
438
+ this.dbosExec.logger.warn(errorMsg);
439
+ }
440
+ const getMetricsResponse = new protocol.GetMetricsResponse(getMetricsMessage.request_id, metricsData, errorMsg);
441
+ currWebsocket.send(JSON.stringify(getMetricsResponse));
442
+ break;
443
+ case protocol.MessageType.EXPORT_WORKFLOW:
444
+ const exportMsg = baseMsg;
445
+ let serializedWorkflow = null;
446
+ try {
447
+ const exported = await (0, export_1.exportWorkflow)(this.dbosExec.systemDatabase, exportMsg.workflow_id, exportMsg.export_children ?? false);
448
+ if (exported.length > 0) {
449
+ const jsonStr = JSON.stringify(exported);
450
+ const compressed = await gzip(jsonStr);
451
+ serializedWorkflow = compressed.toString('base64');
452
+ }
453
+ }
454
+ catch (e) {
455
+ errorMsg = this.reportException(`Exception encountered when exporting workflow ${exportMsg.workflow_id}`, e);
456
+ }
457
+ const exportResp = new protocol.ExportWorkflowResponse(baseMsg.request_id, serializedWorkflow, errorMsg);
458
+ currWebsocket.send(JSON.stringify(exportResp));
459
+ break;
460
+ case protocol.MessageType.IMPORT_WORKFLOW:
461
+ const importMsg = baseMsg;
462
+ let importSuccess = true;
463
+ try {
464
+ const compressedData = Buffer.from(importMsg.serialized_workflow, 'base64');
465
+ const decompressed = await gunzip(compressedData);
466
+ const workflows = JSON.parse(decompressed.toString());
467
+ await (0, export_1.importWorkflow)(this.dbosExec.systemDatabase, workflows);
468
+ }
469
+ catch (e) {
470
+ errorMsg = this.reportException('Exception encountered when importing workflow', e);
471
+ importSuccess = false;
472
+ }
473
+ const importResp = new protocol.ImportWorkflowResponse(baseMsg.request_id, importSuccess, errorMsg);
474
+ currWebsocket.send(JSON.stringify(importResp));
475
+ break;
476
+ case protocol.MessageType.ALERT:
477
+ const alertMsg = baseMsg;
478
+ let alertSuccess = true;
479
+ try {
480
+ const handler = (0, decorators_1.getAlertHandler)();
481
+ if (handler) {
482
+ await Promise.resolve(handler(alertMsg.name, alertMsg.message, alertMsg.metadata));
483
+ }
484
+ else {
485
+ this.dbosExec.logger.warn(`Alert received (no handler registered): [${alertMsg.name}] ${alertMsg.message} | Metadata: ${JSON.stringify(alertMsg.metadata)}`);
486
+ }
487
+ }
488
+ catch (e) {
489
+ errorMsg = this.reportException('Exception in alert handler', e);
490
+ alertSuccess = false;
491
+ }
492
+ const alertResp = new protocol.AlertResponse(baseMsg.request_id, alertSuccess, errorMsg);
493
+ currWebsocket.send(JSON.stringify(alertResp));
494
+ break;
495
+ case protocol.MessageType.LIST_SCHEDULES:
496
+ const listSchedMsg = baseMsg;
497
+ const loadContextList = (listSchedMsg.body.load_context ?? true) && !this.metadataOnlyMode;
498
+ let schedOutput = [];
499
+ try {
500
+ const scheds = await this.dbosExec.systemDatabase.listSchedules({
501
+ status: listSchedMsg.body.status,
502
+ workflowName: listSchedMsg.body.workflow_name,
503
+ scheduleNamePrefix: listSchedMsg.body.schedule_name_prefix,
504
+ applicationName: listSchedMsg.body.application_name,
505
+ });
506
+ schedOutput = await Promise.all(scheds.map(async (s) => ({
507
+ schedule_id: s.scheduleId,
508
+ schedule_name: s.scheduleName,
509
+ workflow_name: s.workflowName,
510
+ workflow_class_name: s.workflowClassName || undefined,
511
+ schedule: s.schedule,
512
+ status: s.status,
513
+ context: loadContextList
514
+ ? protocol.inspectForConductor(await this.dbosExec.serializer.parse(s.context))
515
+ : undefined,
516
+ last_fired_at: s.lastFiredAt,
517
+ automatic_backfill: s.automaticBackfill,
518
+ cron_timezone: s.cronTimezone,
519
+ queue_name: s.queueName,
520
+ application_name: s.applicationName ?? null,
521
+ shard_key: s.shardKey ?? null,
522
+ })));
523
+ }
524
+ catch (e) {
525
+ errorMsg = this.reportException('Exception encountered when listing schedules', e);
526
+ }
527
+ const listSchedResp = new protocol.ListSchedulesResponse(listSchedMsg.request_id, schedOutput, errorMsg);
528
+ currWebsocket.send(JSON.stringify(listSchedResp));
529
+ break;
530
+ case protocol.MessageType.GET_SCHEDULE:
531
+ const getSchedMsg = baseMsg;
532
+ const loadContextGet = (getSchedMsg.load_context ?? true) && !this.metadataOnlyMode;
533
+ let getSchedOutput = undefined;
534
+ try {
535
+ const sched = await this.dbosExec.systemDatabase.getSchedule(getSchedMsg.schedule_name);
536
+ if (sched) {
537
+ getSchedOutput = {
538
+ schedule_id: sched.scheduleId,
539
+ schedule_name: sched.scheduleName,
540
+ workflow_name: sched.workflowName,
541
+ workflow_class_name: sched.workflowClassName || undefined,
542
+ schedule: sched.schedule,
543
+ status: sched.status,
544
+ context: loadContextGet
545
+ ? protocol.inspectForConductor(await this.dbosExec.serializer.parse(sched.context))
546
+ : undefined,
547
+ last_fired_at: sched.lastFiredAt,
548
+ automatic_backfill: sched.automaticBackfill,
549
+ cron_timezone: sched.cronTimezone,
550
+ queue_name: sched.queueName,
551
+ application_name: sched.applicationName ?? null,
552
+ shard_key: sched.shardKey ?? null,
553
+ };
554
+ }
555
+ }
556
+ catch (e) {
557
+ errorMsg = this.reportException(`Exception encountered when getting schedule ${getSchedMsg.schedule_name}`, e);
558
+ }
559
+ const getSchedResp = new protocol.GetScheduleResponse(getSchedMsg.request_id, getSchedOutput, errorMsg);
560
+ currWebsocket.send(JSON.stringify(getSchedResp));
561
+ break;
562
+ case protocol.MessageType.PAUSE_SCHEDULE:
563
+ const pauseSchedMsg = baseMsg;
564
+ let pauseSuccess = true;
565
+ try {
566
+ await this.dbosExec.systemDatabase.setScheduleStatus(pauseSchedMsg.schedule_name, 'PAUSED');
567
+ }
568
+ catch (e) {
569
+ errorMsg = this.reportException(`Exception encountered when pausing schedule '${pauseSchedMsg.schedule_name}'`, e);
570
+ pauseSuccess = false;
571
+ }
572
+ const pauseSchedResp = new protocol.PauseScheduleResponse(baseMsg.request_id, pauseSuccess, errorMsg);
573
+ currWebsocket.send(JSON.stringify(pauseSchedResp));
574
+ break;
575
+ case protocol.MessageType.RESUME_SCHEDULE:
576
+ const resumeSchedMsg = baseMsg;
577
+ let resumeSchedSuccess = true;
578
+ try {
579
+ await this.dbosExec.systemDatabase.setScheduleStatus(resumeSchedMsg.schedule_name, 'ACTIVE');
580
+ }
581
+ catch (e) {
582
+ errorMsg = this.reportException(`Exception encountered when resuming schedule '${resumeSchedMsg.schedule_name}'`, e);
583
+ resumeSchedSuccess = false;
584
+ }
585
+ const resumeSchedResp = new protocol.ResumeScheduleResponse(baseMsg.request_id, resumeSchedSuccess, errorMsg);
586
+ currWebsocket.send(JSON.stringify(resumeSchedResp));
587
+ break;
588
+ case protocol.MessageType.BACKFILL_SCHEDULE:
589
+ const backfillSchedMsg = baseMsg;
590
+ let backfillWorkflowIds = [];
591
+ try {
592
+ backfillWorkflowIds = await (0, scheduler_1.backfillSchedule)(this.dbosExec.systemDatabase, this.dbosExec.serializer, backfillSchedMsg.schedule_name, new Date(backfillSchedMsg.start), new Date(backfillSchedMsg.end));
593
+ }
594
+ catch (e) {
595
+ errorMsg = this.reportException(`Exception encountered when backfilling schedule '${backfillSchedMsg.schedule_name}'`, e);
596
+ }
597
+ const backfillSchedResp = new protocol.BackfillScheduleResponse(baseMsg.request_id, backfillWorkflowIds, errorMsg);
598
+ currWebsocket.send(JSON.stringify(backfillSchedResp));
599
+ break;
600
+ case protocol.MessageType.TRIGGER_SCHEDULE:
601
+ const triggerSchedMsg = baseMsg;
602
+ let triggerWorkflowId = undefined;
603
+ try {
604
+ triggerWorkflowId = await (0, scheduler_1.triggerSchedule)(this.dbosExec.systemDatabase, this.dbosExec.serializer, triggerSchedMsg.schedule_name);
605
+ }
606
+ catch (e) {
607
+ errorMsg = this.reportException(`Exception encountered when triggering schedule '${triggerSchedMsg.schedule_name}'`, e);
608
+ }
609
+ const triggerSchedResp = new protocol.TriggerScheduleResponse(baseMsg.request_id, triggerWorkflowId, errorMsg);
610
+ currWebsocket.send(JSON.stringify(triggerSchedResp));
611
+ break;
612
+ case protocol.MessageType.LIST_APPLICATION_VERSIONS:
613
+ let versionsOutput = [];
614
+ try {
615
+ const versions = await this.dbosExec.systemDatabase.listApplicationVersions();
616
+ versionsOutput = versions.map((v) => ({
617
+ version_id: v.versionId,
618
+ version_name: v.versionName,
619
+ version_timestamp: v.versionTimestamp,
620
+ created_at: v.createdAt,
621
+ }));
622
+ }
623
+ catch (e) {
624
+ errorMsg = this.reportException('Exception encountered when listing application versions', e);
625
+ }
626
+ const listVersionsResp = new protocol.ListApplicationVersionsResponse(baseMsg.request_id, versionsOutput, errorMsg);
627
+ currWebsocket.send(JSON.stringify(listVersionsResp));
628
+ break;
629
+ case protocol.MessageType.SET_LATEST_APPLICATION_VERSION:
630
+ const setVersionMsg = baseMsg;
631
+ let setVersionSuccess = true;
632
+ try {
633
+ await this.dbosExec.systemDatabase.updateApplicationVersionTimestamp(setVersionMsg.version_name, Date.now());
634
+ }
635
+ catch (e) {
636
+ errorMsg = this.reportException(`Exception encountered when setting latest application version '${setVersionMsg.version_name}'`, e);
637
+ setVersionSuccess = false;
638
+ }
639
+ const setVersionResp = new protocol.SetLatestApplicationVersionResponse(baseMsg.request_id, setVersionSuccess, errorMsg);
640
+ currWebsocket.send(JSON.stringify(setVersionResp));
641
+ break;
642
+ case protocol.MessageType.GET_WORKFLOW_EVENTS:
643
+ const eventsMsg = baseMsg;
644
+ let eventOutputs = undefined;
645
+ try {
646
+ const rawEvents = await (0, observability_1.getAllEvents)(this.dbosExec.systemDatabase, eventsMsg.workflow_id);
647
+ eventOutputs = Object.entries(rawEvents).map(([key, value]) => ({
648
+ key,
649
+ value: protocol.inspectForConductor(value),
650
+ }));
651
+ }
652
+ catch (e) {
653
+ errorMsg = this.reportException(`Exception encountered when getting events for workflow ${eventsMsg.workflow_id}`, e);
654
+ }
655
+ const eventsResp = new protocol.GetWorkflowEventsResponse(baseMsg.request_id, eventOutputs, errorMsg);
656
+ currWebsocket.send(JSON.stringify(eventsResp));
657
+ break;
658
+ case protocol.MessageType.GET_WORKFLOW_NOTIFICATIONS:
659
+ const notifsMsg = baseMsg;
660
+ let notifOutputs = undefined;
661
+ try {
662
+ const rawNotifs = await (0, observability_1.getAllNotifications)(this.dbosExec.systemDatabase, notifsMsg.workflow_id);
663
+ notifOutputs = rawNotifs.map((n) => ({
664
+ topic: n.topic,
665
+ message: protocol.inspectForConductor(n.message),
666
+ created_at_epoch_ms: n.createdAtEpochMs,
667
+ consumed: n.consumed,
668
+ }));
669
+ }
670
+ catch (e) {
671
+ errorMsg = this.reportException(`Exception encountered when getting notifications for workflow ${notifsMsg.workflow_id}`, e);
672
+ }
673
+ const notifsResp = new protocol.GetWorkflowNotificationsResponse(baseMsg.request_id, notifOutputs, errorMsg);
674
+ currWebsocket.send(JSON.stringify(notifsResp));
675
+ break;
676
+ case protocol.MessageType.GET_WORKFLOW_STREAMS:
677
+ const streamsMsg = baseMsg;
678
+ let streamOutputs = undefined;
679
+ try {
680
+ const rawStreams = await (0, observability_1.getAllStreamEntries)(this.dbosExec.systemDatabase, streamsMsg.workflow_id);
681
+ streamOutputs = Object.entries(rawStreams).map(([key, values]) => ({
682
+ key,
683
+ values: values.map((v) => protocol.inspectForConductor(v)),
684
+ }));
685
+ }
686
+ catch (e) {
687
+ errorMsg = this.reportException(`Exception encountered when getting streams for workflow ${streamsMsg.workflow_id}`, e);
688
+ }
689
+ const streamsResp = new protocol.GetWorkflowStreamsResponse(baseMsg.request_id, streamOutputs, errorMsg);
690
+ currWebsocket.send(JSON.stringify(streamsResp));
691
+ break;
692
+ case protocol.MessageType.GET_WORKFLOW_AGGREGATES:
693
+ const aggMsg = baseMsg;
694
+ const aggBody = aggMsg.body;
695
+ let aggOutput = [];
696
+ let selectCount = aggBody.select_count ?? false;
697
+ const selectMinCreatedAt = aggBody.select_min_created_at ?? false;
698
+ const selectMaxQueueWaitMs = aggBody.select_max_queue_wait_ms ?? false;
699
+ const selectMaxTotalLatencyMs = aggBody.select_max_total_latency_ms ?? false;
700
+ if (!(selectCount || selectMinCreatedAt || selectMaxQueueWaitMs || selectMaxTotalLatencyMs)) {
701
+ selectCount = true;
702
+ }
703
+ try {
704
+ const rows = await (0, observability_1.getWorkflowAggregates)(this.dbosExec.systemDatabase, {
705
+ groupByStatus: aggBody.group_by_status ?? false,
706
+ groupByName: aggBody.group_by_name ?? false,
707
+ groupByQueueName: aggBody.group_by_queue_name ?? false,
708
+ groupByExecutorId: aggBody.group_by_executor_id ?? false,
709
+ groupByApplicationVersion: aggBody.group_by_application_version ?? false,
710
+ groupByApplicationName: aggBody.group_by_application_name ?? false,
711
+ selectCount,
712
+ selectMinCreatedAt,
713
+ selectMaxQueueWaitMs,
714
+ selectMaxTotalLatencyMs,
715
+ timeBucketSizeMs: aggBody.time_bucket_size_ms ?? undefined,
716
+ status: aggBody.status,
717
+ startTime: aggBody.start_time,
718
+ endTime: aggBody.end_time,
719
+ completedAfter: aggBody.completed_after,
720
+ completedBefore: aggBody.completed_before,
721
+ dequeuedAfter: aggBody.dequeued_after,
722
+ dequeuedBefore: aggBody.dequeued_before,
723
+ name: aggBody.name,
724
+ appVersion: aggBody.app_version,
725
+ executorId: aggBody.executor_id,
726
+ queueName: aggBody.queue_name,
727
+ workflowIdPrefix: aggBody.workflow_id_prefix,
728
+ workflowIDs: aggBody.workflow_ids,
729
+ authenticatedUser: aggBody.user,
730
+ forkedFrom: aggBody.forked_from,
731
+ parentWorkflowID: aggBody.parent_workflow_id,
732
+ scheduleName: aggBody.schedule_name,
733
+ applicationName: aggBody.application_name,
734
+ wasForkedFrom: aggBody.was_forked_from,
735
+ isFork: aggBody.is_fork,
736
+ hasParent: aggBody.has_parent,
737
+ attributes: aggBody.attributes,
738
+ });
739
+ aggOutput = rows.map((r) => ({
740
+ group: r.group,
741
+ count: r.count,
742
+ min_created_at: r.minCreatedAt,
743
+ max_queue_wait_ms: r.maxQueueWaitMs,
744
+ max_total_latency_ms: r.maxTotalLatencyMs,
745
+ }));
746
+ }
747
+ catch (e) {
748
+ errorMsg = this.reportException('Exception encountered when getting workflow aggregates', e);
749
+ }
750
+ const aggResp = new protocol.GetWorkflowAggregatesResponse(baseMsg.request_id, aggOutput, errorMsg);
751
+ currWebsocket.send(JSON.stringify(aggResp));
752
+ break;
753
+ case protocol.MessageType.GET_STEP_AGGREGATES:
754
+ const stepAggMsg = baseMsg;
755
+ const stepAggBody = stepAggMsg.body;
756
+ let stepAggOutput = [];
757
+ let stepSelectCount = stepAggBody.select_count ?? false;
758
+ const stepSelectMaxDurationMs = stepAggBody.select_max_duration_ms ?? false;
759
+ if (!(stepSelectCount || stepSelectMaxDurationMs)) {
760
+ stepSelectCount = true;
761
+ }
762
+ try {
763
+ const rows = await (0, observability_1.getStepAggregates)(this.dbosExec.systemDatabase, {
764
+ groupByFunctionName: stepAggBody.group_by_function_name ?? false,
765
+ groupByStatus: stepAggBody.group_by_status ?? false,
766
+ selectCount: stepSelectCount,
767
+ selectMaxDurationMs: stepSelectMaxDurationMs,
768
+ timeBucketSizeMs: stepAggBody.time_bucket_size_ms ?? undefined,
769
+ status: stepAggBody.status,
770
+ functionName: stepAggBody.function_name,
771
+ workflowIdPrefix: stepAggBody.workflow_id_prefix,
772
+ completedAfter: stepAggBody.completed_after,
773
+ completedBefore: stepAggBody.completed_before,
774
+ applicationName: stepAggBody.application_name,
775
+ });
776
+ stepAggOutput = rows.map((r) => ({
777
+ group: r.group,
778
+ count: r.count,
779
+ max_duration_ms: r.maxDurationMs,
780
+ }));
781
+ }
782
+ catch (e) {
783
+ errorMsg = this.reportException('Exception encountered when getting step aggregates', e);
784
+ }
785
+ const stepAggResp = new protocol.GetStepAggregatesResponse(baseMsg.request_id, stepAggOutput, errorMsg);
786
+ currWebsocket.send(JSON.stringify(stepAggResp));
787
+ break;
788
+ case protocol.MessageType.LIST_QUEUES:
789
+ const listQueuesMsg = baseMsg;
790
+ let queueOutputs = [];
791
+ try {
792
+ const queues = await this.dbosExec.systemDatabase.listQueues(listQueuesMsg.body?.application_name);
793
+ queueOutputs = queues.map((q) => ({
794
+ name: q.name,
795
+ concurrency: q.concurrency,
796
+ worker_concurrency: q.workerConcurrency,
797
+ rate_limit_max: q.rateLimitMax,
798
+ rate_limit_period_sec: q.rateLimitPeriodSec,
799
+ priority_enabled: true,
800
+ partition_queue: q.partitionQueue,
801
+ polling_interval_sec: q.pollingIntervalSec,
802
+ application_name: q.applicationName ?? null,
803
+ partition_concurrency: q.partitionConcurrency,
804
+ partition_worker_concurrency: q.partitionWorkerConcurrency,
805
+ partition_rate_limit_max: q.partitionRateLimitMax,
806
+ partition_rate_limit_period_sec: q.partitionRateLimitPeriodSec,
807
+ }));
808
+ }
809
+ catch (e) {
810
+ errorMsg = this.reportException('Exception encountered when listing queues', e);
811
+ }
812
+ const listQueuesResp = new protocol.ListQueuesResponse(baseMsg.request_id, queueOutputs, errorMsg);
813
+ currWebsocket.send(JSON.stringify(listQueuesResp));
814
+ break;
815
+ case protocol.MessageType.GET_QUEUE:
816
+ const getQueueMsg = baseMsg;
817
+ let getQueueOutput = null;
818
+ try {
819
+ const queue = await this.dbosExec.systemDatabase.getQueue(getQueueMsg.name);
820
+ if (queue !== null) {
821
+ getQueueOutput = {
822
+ name: queue.name,
823
+ concurrency: queue.concurrency,
824
+ worker_concurrency: queue.workerConcurrency,
825
+ rate_limit_max: queue.rateLimitMax,
826
+ rate_limit_period_sec: queue.rateLimitPeriodSec,
827
+ priority_enabled: true,
828
+ partition_queue: queue.partitionQueue,
829
+ polling_interval_sec: queue.pollingIntervalSec,
830
+ application_name: queue.applicationName ?? null,
831
+ partition_concurrency: queue.partitionConcurrency,
832
+ partition_worker_concurrency: queue.partitionWorkerConcurrency,
833
+ partition_rate_limit_max: queue.partitionRateLimitMax,
834
+ partition_rate_limit_period_sec: queue.partitionRateLimitPeriodSec,
835
+ };
836
+ }
837
+ }
838
+ catch (e) {
839
+ errorMsg = this.reportException(`Exception encountered when getting queue ${getQueueMsg.name}`, e);
840
+ }
841
+ const getQueueResp = new protocol.GetQueueResponse(baseMsg.request_id, getQueueOutput, errorMsg);
842
+ currWebsocket.send(JSON.stringify(getQueueResp));
843
+ break;
844
+ default:
845
+ this.dbosExec.logger.warn(`Unknown message type: ${baseMsg.type}`);
846
+ const unknownResp = new protocol.BaseResponse(baseMsg.type, baseMsg.request_id, 'Unknown message type');
847
+ currWebsocket.send(JSON.stringify(unknownResp));
848
+ }
849
+ }
108
850
  setPingInterval(currWebsocket, currPing) {
109
- // Clear any existing ping interval to avoid multiple intervals being set
110
851
  clearInterval(currPing.interval);
111
852
  clearTimeout(currPing.timeout);
112
853
  currPing.interval = setInterval(() => {
113
- // Set ping timeout before sending ping. This prevents the socket from hanging at a non-open state indefinitely
114
854
  currPing.timeout = setTimeout(() => {
115
855
  if (this.isShuttingDown) {
116
856
  return;
117
857
  }
118
858
  else if (this.reconnectTimeout === undefined) {
119
- // Otherwise, try to reconnect if we haven't already
120
859
  this.dbosExec.logger.warn('Connection to conductor lost. Reconnecting...');
121
860
  this.resetWebsocket(currWebsocket, currPing);
122
861
  }
123
862
  }, this.pingTimeoutMs);
124
- // Only ping if the connection is open
125
863
  if (currWebsocket.readyState === ws_1.default.OPEN) {
126
864
  this.dbosExec.logger.debug('Sending ping to conductor');
127
865
  currWebsocket.ping();
@@ -144,10 +882,8 @@ class ConductorWebsocket {
144
882
  }
145
883
  try {
146
884
  this.dbosExec.logger.debug(`Connecting to conductor at ${this.url}`);
147
- // Start a new websocket connection
148
885
  const currWebsocket = new ws_1.default(this.url, { handshakeTimeout: 5000 });
149
886
  this.websocket = currWebsocket;
150
- // Start ping interval
151
887
  const currPing = { interval: undefined, timeout: undefined };
152
888
  this.setPingInterval(currWebsocket, currPing);
153
889
  this.pingIntervalTimeout = currPing;
@@ -161,724 +897,23 @@ class ConductorWebsocket {
161
897
  });
162
898
  currWebsocket.on('message', async (data) => {
163
899
  this.dbosExec.logger.debug(`Received message from conductor: ${data}`);
164
- const baseMsg = JSON.parse(data);
165
- const msgType = baseMsg.type;
166
- let errorMsg = undefined;
900
+ const parsed = JSON.parse(data);
901
+ const baseMsg = parsed;
167
902
  clearTimeout(currPing.timeout);
168
- if (this.metadataOnlyMode && DATA_MESSAGE_TYPES.has(msgType)) {
169
- const refusedResp = new protocol.BaseResponse(msgType, baseMsg.request_id, `${msgType} is not allowed in conductor metadata-only mode`);
170
- currWebsocket.send(JSON.stringify(refusedResp));
171
- return;
903
+ let shardKeys;
904
+ try {
905
+ shardKeys = protocol.messageShardKeys(parsed);
172
906
  }
173
- switch (msgType) {
174
- case protocol.MessageType.EXECUTOR_INFO:
175
- const infoResp = new protocol.ExecutorInfoResponse(baseMsg.request_id, utils_1.globalParams.executorID, utils_1.globalParams.appVersion, (0, node_os_1.hostname)(), 'typescript', utils_1.globalParams.dbosVersion, undefined, this.executorMetadata);
176
- currWebsocket.send(JSON.stringify(infoResp));
177
- this.dbosExec.logger.info('Connected to DBOS conductor');
178
- break;
179
- case protocol.MessageType.RECOVERY:
180
- const recoveryMsg = baseMsg;
181
- let success = true;
182
- try {
183
- await this.dbosExec.recoverPendingWorkflows(recoveryMsg.executor_ids);
184
- }
185
- catch (e) {
186
- errorMsg = this.reportException('Exception encountered when recovering workflows', e);
187
- success = false;
188
- }
189
- const recoveryResp = new protocol.RecoveryResponse(baseMsg.request_id, success, errorMsg);
190
- currWebsocket.send(JSON.stringify(recoveryResp));
191
- break;
192
- case protocol.MessageType.CANCEL:
193
- const cancelMsg = baseMsg;
194
- const cancelIds = cancelMsg.workflow_ids ?? [cancelMsg.workflow_id];
195
- let cancelSuccess = true;
196
- try {
197
- await this.dbosExec.systemDatabase.cancelWorkflows(cancelIds, cancelMsg.cancel_children);
198
- }
199
- catch (e) {
200
- errorMsg = this.reportException(`Exception encountered when cancelling workflow(s) ${String(cancelIds)}`, e);
201
- cancelSuccess = false;
202
- }
203
- const cancelResp = new protocol.CancelResponse(baseMsg.request_id, cancelSuccess, errorMsg);
204
- currWebsocket.send(JSON.stringify(cancelResp));
205
- break;
206
- case protocol.MessageType.DELETE:
207
- const deleteMsg = baseMsg;
208
- const deleteIds = deleteMsg.workflow_ids ?? [deleteMsg.workflow_id];
209
- let deleteSuccess = true;
210
- try {
211
- await this.dbosExec.systemDatabase.deleteWorkflows(deleteIds, deleteMsg.delete_children ?? false);
212
- }
213
- catch (e) {
214
- errorMsg = this.reportException(`Exception encountered when deleting workflow(s) ${String(deleteIds)}`, e);
215
- deleteSuccess = false;
216
- }
217
- const deleteResp = new protocol.DeleteResponse(baseMsg.request_id, deleteSuccess, errorMsg);
218
- currWebsocket.send(JSON.stringify(deleteResp));
219
- break;
220
- case protocol.MessageType.RESUME:
221
- const resumeMsg = baseMsg;
222
- const resumeIds = resumeMsg.workflow_ids ?? [resumeMsg.workflow_id];
223
- let resumeSuccess = true;
224
- try {
225
- await this.dbosExec.systemDatabase.resumeWorkflows(resumeIds, resumeMsg.queue_name);
226
- }
227
- catch (e) {
228
- errorMsg = this.reportException(`Exception encountered when resuming workflow(s) ${String(resumeIds)}`, e);
229
- resumeSuccess = false;
230
- }
231
- const resumeResp = new protocol.ResumeResponse(baseMsg.request_id, resumeSuccess, errorMsg);
232
- currWebsocket.send(JSON.stringify(resumeResp));
233
- break;
234
- case protocol.MessageType.REWIND_WORKFLOW:
235
- const rewindMsg = baseMsg;
236
- const rewindBody = rewindMsg.body;
237
- let rewindSuccess = true;
238
- try {
239
- await this.dbosExec.rewindWorkflow(rewindBody.workflow_id, rewindBody.start_step ?? 0, {
240
- applicationVersion: rewindBody.application_version,
241
- queueName: rewindBody.queue_name,
242
- queuePartitionKey: rewindBody.queue_partition_key,
243
- });
244
- }
245
- catch (e) {
246
- errorMsg = this.reportException(`Exception encountered when rewinding workflow ${rewindBody.workflow_id} to step ${rewindBody.start_step ?? 0}`, e);
247
- rewindSuccess = false;
248
- }
249
- const rewindResp = new protocol.RewindWorkflowResponse(baseMsg.request_id, rewindSuccess, errorMsg);
250
- currWebsocket.send(JSON.stringify(rewindResp));
251
- break;
252
- case protocol.MessageType.FORK_WORKFLOW:
253
- const forkMsg = baseMsg;
254
- let newWorkflowID = forkMsg.body.new_workflow_id;
255
- try {
256
- newWorkflowID = await this.dbosExec.forkWorkflow(forkMsg.body.workflow_id, forkMsg.body.start_step, {
257
- newWorkflowID: newWorkflowID,
258
- applicationVersion: forkMsg.body.application_version,
259
- queueName: forkMsg.body.queue_name,
260
- queuePartitionKey: forkMsg.body.queue_partition_key,
261
- });
262
- }
263
- catch (e) {
264
- errorMsg = this.reportException(`Exception encountered when forking workflow ${forkMsg.body.workflow_id} to new workflow ${newWorkflowID} on step ${forkMsg.body.start_step}, app version ${forkMsg.body.application_version}`, e);
265
- newWorkflowID = undefined;
266
- }
267
- const forkResp = new protocol.ForkWorkflowResponse(baseMsg.request_id, newWorkflowID, errorMsg);
268
- currWebsocket.send(JSON.stringify(forkResp));
269
- break;
270
- case protocol.MessageType.FORK_FROM_FAILURE:
271
- const forkFromFailureMsg = baseMsg;
272
- const forkFromFailureBody = forkFromFailureMsg.body;
273
- let forkedWorkflowIDs = undefined;
274
- try {
275
- forkedWorkflowIDs = await (0, fork_1.forkFromFailure)(this.dbosExec.systemDatabase, forkFromFailureBody.workflow_ids, {
276
- applicationVersion: forkFromFailureBody.application_version,
277
- queueName: forkFromFailureBody.queue_name,
278
- queuePartitionKey: forkFromFailureBody.queue_partition_key,
279
- fromLastFailure: forkFromFailureBody.from_last_failure ?? false,
280
- fromLastStep: forkFromFailureBody.from_last_step ?? false,
281
- fromStep: forkFromFailureBody.from_step,
282
- fromStepName: forkFromFailureBody.from_step_name,
283
- });
284
- }
285
- catch (e) {
286
- errorMsg = this.reportException('Exception encountered when bulk forking workflows', e);
287
- }
288
- const forkFromFailureResp = new protocol.ForkFromFailureResponse(baseMsg.request_id, forkedWorkflowIDs, errorMsg);
289
- currWebsocket.send(JSON.stringify(forkFromFailureResp));
290
- break;
291
- case protocol.MessageType.LIST_WORKFLOWS:
292
- const listWFMsg = baseMsg;
293
- const body = listWFMsg.body;
294
- const listWFReq = {
295
- workflowIDs: body.workflow_uuids,
296
- workflowName: body.workflow_name,
297
- authenticatedUser: body.authenticated_user,
298
- startTime: body.start_time,
299
- endTime: body.end_time,
300
- completedAfter: body.completed_after,
301
- completedBefore: body.completed_before,
302
- dequeuedAfter: body.dequeued_after,
303
- dequeuedBefore: body.dequeued_before,
304
- status: body.status,
305
- applicationVersion: body.application_version,
306
- forkedFrom: body.forked_from,
307
- parentWorkflowID: body.parent_workflow_id,
308
- queueName: body.queue_name,
309
- limit: body.limit,
310
- offset: body.offset,
311
- sortDesc: body.sort_desc,
312
- workflow_id_prefix: body.workflow_id_prefix,
313
- loadInput: (body.load_input ?? false) && !this.metadataOnlyMode, // Default to false if not provided
314
- loadOutput: (body.load_output ?? false) && !this.metadataOnlyMode, // Default to false if not provided
315
- executorId: body.executor_id,
316
- queuesOnly: body.queues_only,
317
- wasForkedFrom: body.was_forked_from,
318
- isFork: body.is_fork,
319
- hasParent: body.has_parent,
320
- attributes: body.attributes,
321
- scheduleName: body.schedule_name,
322
- applicationName: body.application_name,
323
- };
324
- let workflowsOutput = [];
325
- try {
326
- const workflows = await this.dbosExec.listWorkflows(listWFReq);
327
- workflowsOutput = workflows.map((wf) => new protocol.WorkflowsOutput(wf));
328
- }
329
- catch (e) {
330
- errorMsg = this.reportException('Exception encountered when listing workflows', e);
331
- }
332
- const wfsResp = new protocol.ListWorkflowsResponse(listWFMsg.request_id, workflowsOutput, errorMsg);
333
- currWebsocket.send(JSON.stringify(wfsResp));
334
- break;
335
- case protocol.MessageType.LIST_QUEUED_WORKFLOWS:
336
- const listQueuedWFMsg = baseMsg;
337
- const bodyQueued = listQueuedWFMsg.body;
338
- const listQueuedWFReq = {
339
- workflowIDs: bodyQueued.workflow_uuids,
340
- workflowName: bodyQueued.workflow_name,
341
- authenticatedUser: bodyQueued.authenticated_user,
342
- startTime: bodyQueued.start_time,
343
- endTime: bodyQueued.end_time,
344
- completedAfter: bodyQueued.completed_after,
345
- completedBefore: bodyQueued.completed_before,
346
- dequeuedAfter: bodyQueued.dequeued_after,
347
- dequeuedBefore: bodyQueued.dequeued_before,
348
- status: bodyQueued.status,
349
- applicationVersion: bodyQueued.application_version,
350
- forkedFrom: bodyQueued.forked_from,
351
- parentWorkflowID: bodyQueued.parent_workflow_id,
352
- queueName: bodyQueued.queue_name,
353
- limit: bodyQueued.limit,
354
- offset: bodyQueued.offset,
355
- sortDesc: bodyQueued.sort_desc,
356
- workflow_id_prefix: bodyQueued.workflow_id_prefix,
357
- loadInput: (bodyQueued.load_input ?? false) && !this.metadataOnlyMode, // Default to false if not provided
358
- loadOutput: (bodyQueued.load_output ?? false) && !this.metadataOnlyMode, // Default to false if not provided
359
- executorId: bodyQueued.executor_id,
360
- wasForkedFrom: bodyQueued.was_forked_from,
361
- isFork: bodyQueued.is_fork,
362
- hasParent: bodyQueued.has_parent,
363
- attributes: bodyQueued.attributes,
364
- scheduleName: bodyQueued.schedule_name,
365
- applicationName: bodyQueued.application_name,
366
- };
367
- let queuedWFOutput = [];
368
- try {
369
- const workflows = await this.dbosExec.listQueuedWorkflows(listQueuedWFReq);
370
- queuedWFOutput = workflows.map((wf) => new protocol.WorkflowsOutput(wf));
371
- }
372
- catch (e) {
373
- errorMsg = this.reportException('Exception encountered when listing queued workflows', e);
374
- }
375
- const queuedWfsResp = new protocol.ListQueuedWorkflowsResponse(listQueuedWFMsg.request_id, queuedWFOutput, errorMsg);
376
- currWebsocket.send(JSON.stringify(queuedWfsResp));
377
- break;
378
- case protocol.MessageType.GET_WORKFLOW:
379
- const getWFMsg = baseMsg;
380
- let wfOutput = undefined;
381
- try {
382
- const workflows = await this.dbosExec.listWorkflows({
383
- workflowIDs: [getWFMsg.workflow_id],
384
- loadInput: (getWFMsg.load_input ?? true) && !this.metadataOnlyMode,
385
- loadOutput: (getWFMsg.load_output ?? true) && !this.metadataOnlyMode,
386
- });
387
- if (workflows.length > 0) {
388
- wfOutput = new protocol.WorkflowsOutput(workflows[0]);
389
- }
390
- }
391
- catch (e) {
392
- errorMsg = this.reportException(`Exception encountered when getting workflow ${getWFMsg.workflow_id}`, e);
393
- }
394
- const getWFResp = new protocol.GetWorkflowResponse(getWFMsg.request_id, wfOutput, errorMsg);
395
- currWebsocket.send(JSON.stringify(getWFResp));
396
- break;
397
- case protocol.MessageType.EXIST_PENDING_WORKFLOWS:
398
- const existPendingMsg = baseMsg;
399
- let hasPendingWFs = false;
400
- try {
401
- const pendingWFs = await (0, observability_1.getPendingWorkflows)(this.dbosExec.systemDatabase, existPendingMsg.executor_id, existPendingMsg.application_version);
402
- hasPendingWFs = pendingWFs.length > 0;
403
- }
404
- catch (e) {
405
- errorMsg = this.reportException('Exception encountered when checking for pending workflows', e);
406
- }
407
- const existPendingResp = new protocol.ExistPendingWorkflowsResponse(baseMsg.request_id, hasPendingWFs, errorMsg);
408
- currWebsocket.send(JSON.stringify(existPendingResp));
409
- break;
410
- case protocol.MessageType.LIST_STEPS:
411
- const listStepsMessage = baseMsg;
412
- let workflowSteps = undefined;
413
- try {
414
- const stepsInfo = await this.dbosExec.listWorkflowSteps(listStepsMessage.workflow_id, (listStepsMessage.load_output ?? true) && !this.metadataOnlyMode, { limit: listStepsMessage.limit, offset: listStepsMessage.offset });
415
- workflowSteps = stepsInfo?.map((i) => new protocol.WorkflowSteps(i));
416
- }
417
- catch (e) {
418
- errorMsg = this.reportException(`Exception encountered when listing steps ${listStepsMessage.workflow_id}`, e);
419
- }
420
- const listStepsResponse = new protocol.ListStepsResponse(listStepsMessage.request_id, workflowSteps, errorMsg);
421
- currWebsocket.send(JSON.stringify(listStepsResponse));
422
- break;
423
- case protocol.MessageType.RETENTION:
424
- const retentionMessage = baseMsg;
425
- const retentionBody = retentionMessage.body;
426
- // Off the message loop: a round takes minutes, and awaiting it here stops every
427
- // other command and lets the dispatch time out into a second round on another executor.
428
- if (this.retentionRound !== undefined) {
429
- this.dbosExec.logger.warn('Skipping retention: the previous round on this executor is still running.');
430
- }
431
- else {
432
- this.retentionRound = (async () => {
433
- try {
434
- await (0, retention_1.garbageCollect)(this.dbosExec.systemDatabase, retentionBody.gc_cutoff_epoch_ms, retentionBody.gc_rows_threshold,
435
- // An omitted, null, or zero gc_batch_size takes the default.
436
- { batchSize: retentionBody.gc_batch_size || undefined });
437
- if (retentionBody.timeout_cutoff_epoch_ms) {
438
- await (0, retention_1.globalTimeout)(this.dbosExec.systemDatabase, retentionBody.timeout_cutoff_epoch_ms);
439
- }
440
- }
441
- catch (e) {
442
- if (this.isShuttingDown) {
443
- // Shutdown took the system database away mid-round; the next round resumes the work.
444
- this.dbosExec.logger.debug('Retention interrupted by shutdown');
445
- return;
446
- }
447
- this.dbosExec.logger.error(`Exception encountered during enforcing retention policy: ${e.message}`);
448
- }
449
- })().finally(() => {
450
- this.retentionRound = undefined;
451
- });
452
- }
453
- currWebsocket.send(JSON.stringify(new protocol.RetentionResponse(retentionMessage.request_id, true)));
454
- break;
455
- case protocol.MessageType.GET_METRICS:
456
- const getMetricsMessage = baseMsg;
457
- this.dbosExec.logger.debug(`Received metrics request for time range ${getMetricsMessage.start_time} to ${getMetricsMessage.end_time}`);
458
- let metricsData = [];
459
- if (getMetricsMessage.metric_class === 'workflow_step_count') {
460
- try {
461
- const sysMetrics = await (0, observability_1.getMetrics)(this.dbosExec.systemDatabase, getMetricsMessage.start_time, getMetricsMessage.end_time, getMetricsMessage.application_name);
462
- metricsData = sysMetrics.map((m) => new protocol.MetricDataOutput(m.metricType, m.metricName, m.value));
463
- }
464
- catch (e) {
465
- errorMsg = this.reportException('Exception encountered when getting metrics', e);
466
- }
467
- }
468
- else {
469
- errorMsg = `Unexpected metric class: ${getMetricsMessage.metric_class}`;
470
- this.dbosExec.logger.warn(errorMsg);
471
- }
472
- const getMetricsResponse = new protocol.GetMetricsResponse(getMetricsMessage.request_id, metricsData, errorMsg);
473
- currWebsocket.send(JSON.stringify(getMetricsResponse));
474
- break;
475
- case protocol.MessageType.EXPORT_WORKFLOW:
476
- const exportMsg = baseMsg;
477
- let serializedWorkflow = null;
478
- try {
479
- const exported = await (0, export_1.exportWorkflow)(this.dbosExec.systemDatabase, exportMsg.workflow_id, exportMsg.export_children ?? false);
480
- if (exported.length > 0) {
481
- const jsonStr = JSON.stringify(exported);
482
- const compressed = await gzip(jsonStr);
483
- serializedWorkflow = compressed.toString('base64');
484
- }
485
- }
486
- catch (e) {
487
- errorMsg = this.reportException(`Exception encountered when exporting workflow ${exportMsg.workflow_id}`, e);
488
- }
489
- const exportResp = new protocol.ExportWorkflowResponse(baseMsg.request_id, serializedWorkflow, errorMsg);
490
- currWebsocket.send(JSON.stringify(exportResp));
491
- break;
492
- case protocol.MessageType.IMPORT_WORKFLOW:
493
- const importMsg = baseMsg;
494
- let importSuccess = true;
495
- try {
496
- const compressedData = Buffer.from(importMsg.serialized_workflow, 'base64');
497
- const decompressed = await gunzip(compressedData);
498
- const workflows = JSON.parse(decompressed.toString());
499
- await (0, export_1.importWorkflow)(this.dbosExec.systemDatabase, workflows);
500
- }
501
- catch (e) {
502
- errorMsg = this.reportException('Exception encountered when importing workflow', e);
503
- importSuccess = false;
504
- }
505
- const importResp = new protocol.ImportWorkflowResponse(baseMsg.request_id, importSuccess, errorMsg);
506
- currWebsocket.send(JSON.stringify(importResp));
507
- break;
508
- case protocol.MessageType.ALERT:
509
- const alertMsg = baseMsg;
510
- let alertSuccess = true;
511
- try {
512
- const handler = (0, decorators_1.getAlertHandler)();
513
- if (handler) {
514
- await Promise.resolve(handler(alertMsg.name, alertMsg.message, alertMsg.metadata));
515
- }
516
- else {
517
- this.dbosExec.logger.warn(`Alert received (no handler registered): [${alertMsg.name}] ${alertMsg.message} | Metadata: ${JSON.stringify(alertMsg.metadata)}`);
518
- }
519
- }
520
- catch (e) {
521
- errorMsg = this.reportException('Exception in alert handler', e);
522
- alertSuccess = false;
523
- }
524
- const alertResp = new protocol.AlertResponse(baseMsg.request_id, alertSuccess, errorMsg);
525
- currWebsocket.send(JSON.stringify(alertResp));
526
- break;
527
- case protocol.MessageType.LIST_SCHEDULES:
528
- const listSchedMsg = baseMsg;
529
- const loadContextList = (listSchedMsg.body.load_context ?? true) && !this.metadataOnlyMode;
530
- let schedOutput = [];
531
- try {
532
- const scheds = await this.dbosExec.systemDatabase.listSchedules({
533
- status: listSchedMsg.body.status,
534
- workflowName: listSchedMsg.body.workflow_name,
535
- scheduleNamePrefix: listSchedMsg.body.schedule_name_prefix,
536
- applicationName: listSchedMsg.body.application_name,
537
- });
538
- schedOutput = await Promise.all(scheds.map(async (s) => ({
539
- schedule_id: s.scheduleId,
540
- schedule_name: s.scheduleName,
541
- workflow_name: s.workflowName,
542
- workflow_class_name: s.workflowClassName || undefined,
543
- schedule: s.schedule,
544
- status: s.status,
545
- context: loadContextList
546
- ? protocol.inspectForConductor(await this.dbosExec.serializer.parse(s.context))
547
- : undefined,
548
- last_fired_at: s.lastFiredAt,
549
- automatic_backfill: s.automaticBackfill,
550
- cron_timezone: s.cronTimezone,
551
- queue_name: s.queueName,
552
- application_name: s.applicationName ?? null,
553
- })));
554
- }
555
- catch (e) {
556
- errorMsg = this.reportException('Exception encountered when listing schedules', e);
557
- }
558
- const listSchedResp = new protocol.ListSchedulesResponse(listSchedMsg.request_id, schedOutput, errorMsg);
559
- currWebsocket.send(JSON.stringify(listSchedResp));
560
- break;
561
- case protocol.MessageType.GET_SCHEDULE:
562
- const getSchedMsg = baseMsg;
563
- const loadContextGet = (getSchedMsg.load_context ?? true) && !this.metadataOnlyMode;
564
- let getSchedOutput = undefined;
565
- try {
566
- const sched = await this.dbosExec.systemDatabase.getSchedule(getSchedMsg.schedule_name);
567
- if (sched) {
568
- getSchedOutput = {
569
- schedule_id: sched.scheduleId,
570
- schedule_name: sched.scheduleName,
571
- workflow_name: sched.workflowName,
572
- workflow_class_name: sched.workflowClassName || undefined,
573
- schedule: sched.schedule,
574
- status: sched.status,
575
- context: loadContextGet
576
- ? protocol.inspectForConductor(await this.dbosExec.serializer.parse(sched.context))
577
- : undefined,
578
- last_fired_at: sched.lastFiredAt,
579
- automatic_backfill: sched.automaticBackfill,
580
- cron_timezone: sched.cronTimezone,
581
- queue_name: sched.queueName,
582
- application_name: sched.applicationName ?? null,
583
- };
584
- }
585
- }
586
- catch (e) {
587
- errorMsg = this.reportException(`Exception encountered when getting schedule ${getSchedMsg.schedule_name}`, e);
588
- }
589
- const getSchedResp = new protocol.GetScheduleResponse(getSchedMsg.request_id, getSchedOutput, errorMsg);
590
- currWebsocket.send(JSON.stringify(getSchedResp));
591
- break;
592
- case protocol.MessageType.PAUSE_SCHEDULE:
593
- const pauseSchedMsg = baseMsg;
594
- let pauseSuccess = true;
595
- try {
596
- await this.dbosExec.systemDatabase.setScheduleStatus(pauseSchedMsg.schedule_name, 'PAUSED');
597
- }
598
- catch (e) {
599
- errorMsg = this.reportException(`Exception encountered when pausing schedule '${pauseSchedMsg.schedule_name}'`, e);
600
- pauseSuccess = false;
601
- }
602
- const pauseSchedResp = new protocol.PauseScheduleResponse(baseMsg.request_id, pauseSuccess, errorMsg);
603
- currWebsocket.send(JSON.stringify(pauseSchedResp));
604
- break;
605
- case protocol.MessageType.RESUME_SCHEDULE:
606
- const resumeSchedMsg = baseMsg;
607
- let resumeSchedSuccess = true;
608
- try {
609
- await this.dbosExec.systemDatabase.setScheduleStatus(resumeSchedMsg.schedule_name, 'ACTIVE');
610
- }
611
- catch (e) {
612
- errorMsg = this.reportException(`Exception encountered when resuming schedule '${resumeSchedMsg.schedule_name}'`, e);
613
- resumeSchedSuccess = false;
614
- }
615
- const resumeSchedResp = new protocol.ResumeScheduleResponse(baseMsg.request_id, resumeSchedSuccess, errorMsg);
616
- currWebsocket.send(JSON.stringify(resumeSchedResp));
617
- break;
618
- case protocol.MessageType.BACKFILL_SCHEDULE:
619
- const backfillSchedMsg = baseMsg;
620
- let backfillWorkflowIds = [];
621
- try {
622
- backfillWorkflowIds = await (0, scheduler_1.backfillSchedule)(this.dbosExec.systemDatabase, this.dbosExec.serializer, backfillSchedMsg.schedule_name, new Date(backfillSchedMsg.start), new Date(backfillSchedMsg.end));
623
- }
624
- catch (e) {
625
- errorMsg = this.reportException(`Exception encountered when backfilling schedule '${backfillSchedMsg.schedule_name}'`, e);
626
- }
627
- const backfillSchedResp = new protocol.BackfillScheduleResponse(baseMsg.request_id, backfillWorkflowIds, errorMsg);
628
- currWebsocket.send(JSON.stringify(backfillSchedResp));
629
- break;
630
- case protocol.MessageType.TRIGGER_SCHEDULE:
631
- const triggerSchedMsg = baseMsg;
632
- let triggerWorkflowId = undefined;
633
- try {
634
- triggerWorkflowId = await (0, scheduler_1.triggerSchedule)(this.dbosExec.systemDatabase, this.dbosExec.serializer, triggerSchedMsg.schedule_name);
635
- }
636
- catch (e) {
637
- errorMsg = this.reportException(`Exception encountered when triggering schedule '${triggerSchedMsg.schedule_name}'`, e);
638
- }
639
- const triggerSchedResp = new protocol.TriggerScheduleResponse(baseMsg.request_id, triggerWorkflowId, errorMsg);
640
- currWebsocket.send(JSON.stringify(triggerSchedResp));
641
- break;
642
- case protocol.MessageType.LIST_APPLICATION_VERSIONS:
643
- let versionsOutput = [];
644
- try {
645
- const versions = await this.dbosExec.systemDatabase.listApplicationVersions();
646
- versionsOutput = versions.map((v) => ({
647
- version_id: v.versionId,
648
- version_name: v.versionName,
649
- version_timestamp: v.versionTimestamp,
650
- created_at: v.createdAt,
651
- }));
652
- }
653
- catch (e) {
654
- errorMsg = this.reportException('Exception encountered when listing application versions', e);
655
- }
656
- const listVersionsResp = new protocol.ListApplicationVersionsResponse(baseMsg.request_id, versionsOutput, errorMsg);
657
- currWebsocket.send(JSON.stringify(listVersionsResp));
658
- break;
659
- case protocol.MessageType.SET_LATEST_APPLICATION_VERSION:
660
- const setVersionMsg = baseMsg;
661
- let setVersionSuccess = true;
662
- try {
663
- await this.dbosExec.systemDatabase.updateApplicationVersionTimestamp(setVersionMsg.version_name, Date.now());
664
- }
665
- catch (e) {
666
- errorMsg = this.reportException(`Exception encountered when setting latest application version '${setVersionMsg.version_name}'`, e);
667
- setVersionSuccess = false;
668
- }
669
- const setVersionResp = new protocol.SetLatestApplicationVersionResponse(baseMsg.request_id, setVersionSuccess, errorMsg);
670
- currWebsocket.send(JSON.stringify(setVersionResp));
671
- break;
672
- case protocol.MessageType.GET_WORKFLOW_EVENTS:
673
- const eventsMsg = baseMsg;
674
- let eventOutputs = undefined;
675
- try {
676
- const rawEvents = await (0, observability_1.getAllEvents)(this.dbosExec.systemDatabase, eventsMsg.workflow_id);
677
- eventOutputs = Object.entries(rawEvents).map(([key, value]) => ({
678
- key,
679
- value: protocol.inspectForConductor(value),
680
- }));
681
- }
682
- catch (e) {
683
- errorMsg = this.reportException(`Exception encountered when getting events for workflow ${eventsMsg.workflow_id}`, e);
684
- }
685
- const eventsResp = new protocol.GetWorkflowEventsResponse(baseMsg.request_id, eventOutputs, errorMsg);
686
- currWebsocket.send(JSON.stringify(eventsResp));
687
- break;
688
- case protocol.MessageType.GET_WORKFLOW_NOTIFICATIONS:
689
- const notifsMsg = baseMsg;
690
- let notifOutputs = undefined;
691
- try {
692
- const rawNotifs = await (0, observability_1.getAllNotifications)(this.dbosExec.systemDatabase, notifsMsg.workflow_id);
693
- notifOutputs = rawNotifs.map((n) => ({
694
- topic: n.topic,
695
- message: protocol.inspectForConductor(n.message),
696
- created_at_epoch_ms: n.createdAtEpochMs,
697
- consumed: n.consumed,
698
- }));
699
- }
700
- catch (e) {
701
- errorMsg = this.reportException(`Exception encountered when getting notifications for workflow ${notifsMsg.workflow_id}`, e);
702
- }
703
- const notifsResp = new protocol.GetWorkflowNotificationsResponse(baseMsg.request_id, notifOutputs, errorMsg);
704
- currWebsocket.send(JSON.stringify(notifsResp));
705
- break;
706
- case protocol.MessageType.GET_WORKFLOW_STREAMS:
707
- const streamsMsg = baseMsg;
708
- let streamOutputs = undefined;
709
- try {
710
- const rawStreams = await (0, observability_1.getAllStreamEntries)(this.dbosExec.systemDatabase, streamsMsg.workflow_id);
711
- streamOutputs = Object.entries(rawStreams).map(([key, values]) => ({
712
- key,
713
- values: values.map((v) => protocol.inspectForConductor(v)),
714
- }));
715
- }
716
- catch (e) {
717
- errorMsg = this.reportException(`Exception encountered when getting streams for workflow ${streamsMsg.workflow_id}`, e);
718
- }
719
- const streamsResp = new protocol.GetWorkflowStreamsResponse(baseMsg.request_id, streamOutputs, errorMsg);
720
- currWebsocket.send(JSON.stringify(streamsResp));
721
- break;
722
- case protocol.MessageType.GET_WORKFLOW_AGGREGATES:
723
- const aggMsg = baseMsg;
724
- const aggBody = aggMsg.body;
725
- let aggOutput = [];
726
- // Backwards compat: older clients send no select_* flags and expect counts.
727
- let selectCount = aggBody.select_count ?? false;
728
- const selectMinCreatedAt = aggBody.select_min_created_at ?? false;
729
- const selectMaxQueueWaitMs = aggBody.select_max_queue_wait_ms ?? false;
730
- const selectMaxTotalLatencyMs = aggBody.select_max_total_latency_ms ?? false;
731
- if (!(selectCount || selectMinCreatedAt || selectMaxQueueWaitMs || selectMaxTotalLatencyMs)) {
732
- selectCount = true;
733
- }
734
- try {
735
- const rows = await (0, observability_1.getWorkflowAggregates)(this.dbosExec.systemDatabase, {
736
- groupByStatus: aggBody.group_by_status ?? false,
737
- groupByName: aggBody.group_by_name ?? false,
738
- groupByQueueName: aggBody.group_by_queue_name ?? false,
739
- groupByExecutorId: aggBody.group_by_executor_id ?? false,
740
- groupByApplicationVersion: aggBody.group_by_application_version ?? false,
741
- groupByApplicationName: aggBody.group_by_application_name ?? false,
742
- selectCount,
743
- selectMinCreatedAt,
744
- selectMaxQueueWaitMs,
745
- selectMaxTotalLatencyMs,
746
- timeBucketSizeMs: aggBody.time_bucket_size_ms ?? undefined,
747
- status: aggBody.status,
748
- startTime: aggBody.start_time,
749
- endTime: aggBody.end_time,
750
- completedAfter: aggBody.completed_after,
751
- completedBefore: aggBody.completed_before,
752
- dequeuedAfter: aggBody.dequeued_after,
753
- dequeuedBefore: aggBody.dequeued_before,
754
- name: aggBody.name,
755
- appVersion: aggBody.app_version,
756
- executorId: aggBody.executor_id,
757
- queueName: aggBody.queue_name,
758
- workflowIdPrefix: aggBody.workflow_id_prefix,
759
- workflowIDs: aggBody.workflow_ids,
760
- authenticatedUser: aggBody.user,
761
- forkedFrom: aggBody.forked_from,
762
- parentWorkflowID: aggBody.parent_workflow_id,
763
- scheduleName: aggBody.schedule_name,
764
- applicationName: aggBody.application_name,
765
- wasForkedFrom: aggBody.was_forked_from,
766
- isFork: aggBody.is_fork,
767
- hasParent: aggBody.has_parent,
768
- attributes: aggBody.attributes,
769
- });
770
- aggOutput = rows.map((r) => ({
771
- group: r.group,
772
- count: r.count,
773
- min_created_at: r.minCreatedAt,
774
- max_queue_wait_ms: r.maxQueueWaitMs,
775
- max_total_latency_ms: r.maxTotalLatencyMs,
776
- }));
777
- }
778
- catch (e) {
779
- errorMsg = this.reportException('Exception encountered when getting workflow aggregates', e);
780
- }
781
- const aggResp = new protocol.GetWorkflowAggregatesResponse(baseMsg.request_id, aggOutput, errorMsg);
782
- currWebsocket.send(JSON.stringify(aggResp));
783
- break;
784
- case protocol.MessageType.GET_STEP_AGGREGATES:
785
- const stepAggMsg = baseMsg;
786
- const stepAggBody = stepAggMsg.body;
787
- let stepAggOutput = [];
788
- // Backwards compat: older clients send no select_* flags and expect counts.
789
- let stepSelectCount = stepAggBody.select_count ?? false;
790
- const stepSelectMaxDurationMs = stepAggBody.select_max_duration_ms ?? false;
791
- if (!(stepSelectCount || stepSelectMaxDurationMs)) {
792
- stepSelectCount = true;
793
- }
794
- try {
795
- const rows = await (0, observability_1.getStepAggregates)(this.dbosExec.systemDatabase, {
796
- groupByFunctionName: stepAggBody.group_by_function_name ?? false,
797
- groupByStatus: stepAggBody.group_by_status ?? false,
798
- selectCount: stepSelectCount,
799
- selectMaxDurationMs: stepSelectMaxDurationMs,
800
- timeBucketSizeMs: stepAggBody.time_bucket_size_ms ?? undefined,
801
- status: stepAggBody.status,
802
- functionName: stepAggBody.function_name,
803
- workflowIdPrefix: stepAggBody.workflow_id_prefix,
804
- completedAfter: stepAggBody.completed_after,
805
- completedBefore: stepAggBody.completed_before,
806
- applicationName: stepAggBody.application_name,
807
- });
808
- stepAggOutput = rows.map((r) => ({
809
- group: r.group,
810
- count: r.count,
811
- max_duration_ms: r.maxDurationMs,
812
- }));
813
- }
814
- catch (e) {
815
- errorMsg = this.reportException('Exception encountered when getting step aggregates', e);
816
- }
817
- const stepAggResp = new protocol.GetStepAggregatesResponse(baseMsg.request_id, stepAggOutput, errorMsg);
818
- currWebsocket.send(JSON.stringify(stepAggResp));
819
- break;
820
- case protocol.MessageType.LIST_QUEUES:
821
- const listQueuesMsg = baseMsg;
822
- let queueOutputs = [];
823
- try {
824
- const queues = await this.dbosExec.systemDatabase.listQueues(listQueuesMsg.body?.application_name);
825
- queueOutputs = queues.map((q) => ({
826
- name: q.name,
827
- concurrency: q.concurrency,
828
- worker_concurrency: q.workerConcurrency,
829
- rate_limit_max: q.rateLimitMax,
830
- rate_limit_period_sec: q.rateLimitPeriodSec,
831
- priority_enabled: true,
832
- partition_queue: q.partitionQueue,
833
- polling_interval_sec: q.pollingIntervalSec,
834
- application_name: q.applicationName ?? null,
835
- partition_concurrency: q.partitionConcurrency,
836
- partition_worker_concurrency: q.partitionWorkerConcurrency,
837
- partition_rate_limit_max: q.partitionRateLimitMax,
838
- partition_rate_limit_period_sec: q.partitionRateLimitPeriodSec,
839
- }));
840
- }
841
- catch (e) {
842
- errorMsg = this.reportException('Exception encountered when listing queues', e);
843
- }
844
- const listQueuesResp = new protocol.ListQueuesResponse(baseMsg.request_id, queueOutputs, errorMsg);
845
- currWebsocket.send(JSON.stringify(listQueuesResp));
846
- break;
847
- case protocol.MessageType.GET_QUEUE:
848
- const getQueueMsg = baseMsg;
849
- let getQueueOutput = null;
850
- try {
851
- const queue = await this.dbosExec.systemDatabase.getQueue(getQueueMsg.name);
852
- if (queue !== null) {
853
- getQueueOutput = {
854
- name: queue.name,
855
- concurrency: queue.concurrency,
856
- worker_concurrency: queue.workerConcurrency,
857
- rate_limit_max: queue.rateLimitMax,
858
- rate_limit_period_sec: queue.rateLimitPeriodSec,
859
- priority_enabled: true,
860
- partition_queue: queue.partitionQueue,
861
- polling_interval_sec: queue.pollingIntervalSec,
862
- application_name: queue.applicationName ?? null,
863
- partition_concurrency: queue.partitionConcurrency,
864
- partition_worker_concurrency: queue.partitionWorkerConcurrency,
865
- partition_rate_limit_max: queue.partitionRateLimitMax,
866
- partition_rate_limit_period_sec: queue.partitionRateLimitPeriodSec,
867
- };
868
- }
869
- }
870
- catch (e) {
871
- errorMsg = this.reportException(`Exception encountered when getting queue ${getQueueMsg.name}`, e);
872
- }
873
- const getQueueResp = new protocol.GetQueueResponse(baseMsg.request_id, getQueueOutput, errorMsg);
874
- currWebsocket.send(JSON.stringify(getQueueResp));
875
- break;
876
- default:
877
- this.dbosExec.logger.warn(`Unknown message type: ${baseMsg.type}`);
878
- // Still need to send a response to the conductor
879
- const unknownResp = new protocol.BaseResponse(baseMsg.type, baseMsg.request_id, 'Unknown message type');
880
- currWebsocket.send(JSON.stringify(unknownResp));
907
+ catch (e) {
908
+ if (e instanceof protocol.ShardKeyMismatchError) {
909
+ currWebsocket.send(JSON.stringify(new protocol.BaseResponse(baseMsg.type, baseMsg.request_id, e.message)));
910
+ }
911
+ else {
912
+ this.dbosExec.logger.warn(`Could not read the shard keys of a message from Conductor: ${String(e)}`);
913
+ }
914
+ return;
881
915
  }
916
+ await (0, shard_keys_1.bindShardKeys)(shardKeys, () => this.#handleMessage(currWebsocket, baseMsg, shardKeys));
882
917
  });
883
918
  currWebsocket.on('close', () => {
884
919
  if (this.isShuttingDown) {
@@ -915,10 +950,6 @@ class ConductorWebsocket {
915
950
  }
916
951
  }
917
952
  }
918
- /**
919
- * Give a round in flight a bounded chance to finish before shutdown takes the system
920
- * database away from it.
921
- */
922
953
  async awaitRetention(timeoutMs = 10000) {
923
954
  const round = this.retentionRound;
924
955
  if (round === undefined) {
@@ -940,7 +971,6 @@ class ConductorWebsocket {
940
971
  if (this.websocket) {
941
972
  this.websocket.close();
942
973
  }
943
- // Grace only: a round still running past this is cut when the system database is destroyed.
944
974
  await this.awaitRetention();
945
975
  }
946
976
  }