@dbos-inc/dbos-enterprise 5.3.1-preview → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/src/auto_upgrade.d.ts +39 -0
- package/dist/src/auto_upgrade.d.ts.map +1 -0
- package/dist/src/auto_upgrade.js +229 -0
- package/dist/src/auto_upgrade.js.map +1 -0
- package/dist/src/cockroach.d.ts +10 -0
- package/dist/src/cockroach.d.ts.map +1 -0
- package/dist/src/cockroach.js +28 -0
- package/dist/src/cockroach.js.map +1 -0
- package/dist/src/conductor/conductor.d.ts +42 -0
- package/dist/src/conductor/conductor.d.ts.map +1 -0
- package/dist/src/conductor/conductor.js +978 -0
- package/dist/src/conductor/conductor.js.map +1 -0
- package/dist/src/conductor/protocol.d.ts +618 -0
- package/dist/src/conductor/protocol.d.ts.map +1 -0
- package/dist/src/conductor/protocol.js +514 -0
- package/dist/src/conductor/protocol.js.map +1 -0
- package/dist/src/export.d.ts +19 -0
- package/dist/src/export.d.ts.map +1 -0
- package/dist/src/export.js +184 -0
- package/dist/src/export.js.map +1 -0
- package/dist/src/fork.d.ts +18 -0
- package/dist/src/fork.d.ts.map +1 -0
- package/dist/src/fork.js +71 -0
- package/dist/src/fork.js.map +1 -0
- package/dist/src/index.d.ts +10 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +14 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/locks.d.ts +20 -0
- package/dist/src/locks.d.ts.map +1 -0
- package/dist/src/locks.js +47 -0
- package/dist/src/locks.js.map +1 -0
- package/dist/src/observability.d.ts +90 -0
- package/dist/src/observability.d.ts.map +1 -0
- package/dist/src/observability.js +398 -0
- package/dist/src/observability.js.map +1 -0
- package/dist/src/retention.d.ts +31 -0
- package/dist/src/retention.d.ts.map +1 -0
- package/dist/src/retention.js +408 -0
- package/dist/src/retention.js.map +1 -0
- package/dist/src/sharding/index.d.ts +9 -0
- package/dist/src/sharding/index.d.ts.map +1 -0
- package/dist/src/sharding/index.js +13 -0
- package/dist/src/sharding/index.js.map +1 -0
- package/dist/src/sharding/migrations.d.ts +20 -0
- package/dist/src/sharding/migrations.d.ts.map +1 -0
- package/dist/src/sharding/migrations.js +486 -0
- package/dist/src/sharding/migrations.js.map +1 -0
- package/dist/src/sharding/schema.d.ts +9 -0
- package/dist/src/sharding/schema.d.ts.map +1 -0
- package/dist/src/sharding/schema.js +12 -0
- package/dist/src/sharding/schema.js.map +1 -0
- package/dist/src/sharding/sys_db.d.ts +101 -0
- package/dist/src/sharding/sys_db.d.ts.map +1 -0
- package/dist/src/sharding/sys_db.js +717 -0
- package/dist/src/sharding/sys_db.js.map +1 -0
- package/package.json +14 -6
- package/dist/src/scratch.d.ts +0 -9
- package/dist/src/scratch.d.ts.map +0 -1
- package/dist/src/scratch.js +0 -15
- package/dist/src/scratch.js.map +0 -1
|
@@ -0,0 +1,978 @@
|
|
|
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.
|
|
8
|
+
"use strict";
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
26
|
+
if (mod && mod.__esModule) return mod;
|
|
27
|
+
var result = {};
|
|
28
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
29
|
+
__setModuleDefault(result, mod);
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ConductorWebsocket = void 0;
|
|
37
|
+
const decorators_1 = require("@dbos-inc/dbos-sdk/internal/decorators");
|
|
38
|
+
const utils_1 = require("@dbos-inc/dbos-sdk/internal/utils");
|
|
39
|
+
const ws_1 = __importDefault(require("ws"));
|
|
40
|
+
const protocol = __importStar(require("./protocol"));
|
|
41
|
+
const node_os_1 = require("node:os");
|
|
42
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
43
|
+
const zlib = __importStar(require("node:zlib"));
|
|
44
|
+
const node_util_1 = require("node:util");
|
|
45
|
+
const export_1 = require("../export");
|
|
46
|
+
const fork_1 = require("../fork");
|
|
47
|
+
const observability_1 = require("../observability");
|
|
48
|
+
const retention_1 = require("../retention");
|
|
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");
|
|
51
|
+
const gzip = (0, node_util_1.promisify)(zlib.gzip);
|
|
52
|
+
const gunzip = (0, node_util_1.promisify)(zlib.gunzip);
|
|
53
|
+
const DATA_MESSAGE_TYPES = new Set([
|
|
54
|
+
protocol.MessageType.GET_WORKFLOW_EVENTS,
|
|
55
|
+
protocol.MessageType.GET_WORKFLOW_NOTIFICATIONS,
|
|
56
|
+
protocol.MessageType.GET_WORKFLOW_STREAMS,
|
|
57
|
+
protocol.MessageType.EXPORT_WORKFLOW,
|
|
58
|
+
protocol.MessageType.IMPORT_WORKFLOW,
|
|
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
|
+
}
|
|
70
|
+
class ConductorWebsocket {
|
|
71
|
+
dbosExec;
|
|
72
|
+
url;
|
|
73
|
+
websocket = undefined;
|
|
74
|
+
isShuttingDown = false;
|
|
75
|
+
pingPeriodMs = 20000;
|
|
76
|
+
pingTimeoutMs = 15000;
|
|
77
|
+
pingIntervalTimeout = undefined;
|
|
78
|
+
reconnectDelayMs = 1000;
|
|
79
|
+
reconnectTimeout = undefined;
|
|
80
|
+
retentionRound = undefined;
|
|
81
|
+
appName;
|
|
82
|
+
executorMetadata;
|
|
83
|
+
metadataOnlyMode;
|
|
84
|
+
constructor(dbosExec, options) {
|
|
85
|
+
this.dbosExec = dbosExec;
|
|
86
|
+
this.appName = options.appName;
|
|
87
|
+
this.executorMetadata = options.executorMetadata;
|
|
88
|
+
this.metadataOnlyMode = options.metadataOnlyMode ?? false;
|
|
89
|
+
(0, node_assert_1.default)(this.appName, 'Application name must be set in configuration in order to use DBOS Conductor');
|
|
90
|
+
const cleanConductorURL = options.conductorURL.replace(/\/+$/, '');
|
|
91
|
+
this.url = `${cleanConductorURL}/websocket/${this.appName}/${options.conductorKey}`;
|
|
92
|
+
}
|
|
93
|
+
start() {
|
|
94
|
+
this.dispatchLoop();
|
|
95
|
+
}
|
|
96
|
+
reportException(context, e) {
|
|
97
|
+
const errorMsg = `${context}: ${e.message}`;
|
|
98
|
+
this.dbosExec.logger.error(errorMsg);
|
|
99
|
+
if (this.metadataOnlyMode) {
|
|
100
|
+
const exceptionName = e instanceof Error ? e.constructor.name : typeof e;
|
|
101
|
+
return `${context}: ${exceptionName} (details withheld in metadata-only mode)`;
|
|
102
|
+
}
|
|
103
|
+
return errorMsg;
|
|
104
|
+
}
|
|
105
|
+
resetWebsocket(currWebsocket, currPing) {
|
|
106
|
+
clearInterval(currPing?.interval);
|
|
107
|
+
clearTimeout(currPing?.timeout);
|
|
108
|
+
if (currWebsocket) {
|
|
109
|
+
currWebsocket.terminate();
|
|
110
|
+
if (this.websocket === currWebsocket) {
|
|
111
|
+
this.websocket = undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (this.reconnectTimeout || this.isShuttingDown) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.dbosExec.logger.debug(`Reconnecting in ${this.reconnectDelayMs / 1000} second`);
|
|
118
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
119
|
+
this.dispatchLoop();
|
|
120
|
+
}, this.reconnectDelayMs);
|
|
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
|
+
}
|
|
850
|
+
setPingInterval(currWebsocket, currPing) {
|
|
851
|
+
clearInterval(currPing.interval);
|
|
852
|
+
clearTimeout(currPing.timeout);
|
|
853
|
+
currPing.interval = setInterval(() => {
|
|
854
|
+
currPing.timeout = setTimeout(() => {
|
|
855
|
+
if (this.isShuttingDown) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
else if (this.reconnectTimeout === undefined) {
|
|
859
|
+
this.dbosExec.logger.warn('Connection to conductor lost. Reconnecting...');
|
|
860
|
+
this.resetWebsocket(currWebsocket, currPing);
|
|
861
|
+
}
|
|
862
|
+
}, this.pingTimeoutMs);
|
|
863
|
+
if (currWebsocket.readyState === ws_1.default.OPEN) {
|
|
864
|
+
this.dbosExec.logger.debug('Sending ping to conductor');
|
|
865
|
+
currWebsocket.ping();
|
|
866
|
+
}
|
|
867
|
+
}, this.pingPeriodMs);
|
|
868
|
+
}
|
|
869
|
+
dispatchLoop() {
|
|
870
|
+
if (this.websocket &&
|
|
871
|
+
(this.websocket.readyState === ws_1.default.OPEN || this.websocket.readyState === ws_1.default.CONNECTING)) {
|
|
872
|
+
this.dbosExec.logger.warn('Conductor websocket already exists');
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
clearTimeout(this.pingIntervalTimeout?.timeout);
|
|
876
|
+
clearInterval(this.pingIntervalTimeout?.interval);
|
|
877
|
+
clearTimeout(this.reconnectTimeout);
|
|
878
|
+
this.reconnectTimeout = undefined;
|
|
879
|
+
if (this.isShuttingDown) {
|
|
880
|
+
this.dbosExec.logger.debug('Not starting dispatch loop as conductor is shutting down');
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
this.dbosExec.logger.debug(`Connecting to conductor at ${this.url}`);
|
|
885
|
+
const currWebsocket = new ws_1.default(this.url, { handshakeTimeout: 5000 });
|
|
886
|
+
this.websocket = currWebsocket;
|
|
887
|
+
const currPing = { interval: undefined, timeout: undefined };
|
|
888
|
+
this.setPingInterval(currWebsocket, currPing);
|
|
889
|
+
this.pingIntervalTimeout = currPing;
|
|
890
|
+
currWebsocket.on('open', () => {
|
|
891
|
+
this.dbosExec.logger.debug('Opened connection to DBOS conductor');
|
|
892
|
+
clearTimeout(currPing.timeout);
|
|
893
|
+
});
|
|
894
|
+
currWebsocket.on('pong', () => {
|
|
895
|
+
this.dbosExec.logger.debug('Received pong from conductor');
|
|
896
|
+
clearTimeout(currPing.timeout);
|
|
897
|
+
});
|
|
898
|
+
currWebsocket.on('message', async (data) => {
|
|
899
|
+
this.dbosExec.logger.debug(`Received message from conductor: ${data}`);
|
|
900
|
+
const parsed = JSON.parse(data);
|
|
901
|
+
const baseMsg = parsed;
|
|
902
|
+
clearTimeout(currPing.timeout);
|
|
903
|
+
let shardKeys;
|
|
904
|
+
try {
|
|
905
|
+
shardKeys = protocol.messageShardKeys(parsed);
|
|
906
|
+
}
|
|
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;
|
|
915
|
+
}
|
|
916
|
+
await (0, shard_keys_1.bindShardKeys)(shardKeys, () => this.#handleMessage(currWebsocket, baseMsg, shardKeys));
|
|
917
|
+
});
|
|
918
|
+
currWebsocket.on('close', () => {
|
|
919
|
+
if (this.isShuttingDown) {
|
|
920
|
+
this.dbosExec.logger.info('Shutdown Conductor connection');
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
else if (this.reconnectTimeout === undefined) {
|
|
924
|
+
this.dbosExec.logger.warn('Connection to conductor lost. Reconnecting.');
|
|
925
|
+
this.resetWebsocket(currWebsocket, currPing);
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
currWebsocket.on('unexpected-response', (_, res) => {
|
|
929
|
+
const chunks = [];
|
|
930
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
931
|
+
res.on('end', () => {
|
|
932
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
933
|
+
this.dbosExec.logger.warn(`Unexpected response from conductor: ${res.statusCode} ${res.statusMessage}. Details: ${body}`);
|
|
934
|
+
if (this.reconnectTimeout === undefined) {
|
|
935
|
+
this.resetWebsocket(currWebsocket, currPing);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
currWebsocket.on('error', (err) => {
|
|
940
|
+
this.dbosExec.logger.warn(`Unexpected exception in connection to conductor. Reconnecting: ${err.message}`);
|
|
941
|
+
if (this.reconnectTimeout === undefined) {
|
|
942
|
+
this.resetWebsocket(currWebsocket, currPing);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
catch (e) {
|
|
947
|
+
this.dbosExec.logger.warn(`Error in conductor loop. Reconnecting: ${e.message}`);
|
|
948
|
+
if (this.reconnectTimeout === undefined) {
|
|
949
|
+
this.resetWebsocket(this.websocket, this.pingIntervalTimeout);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async awaitRetention(timeoutMs = 10000) {
|
|
954
|
+
const round = this.retentionRound;
|
|
955
|
+
if (round === undefined) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
let timer = undefined;
|
|
959
|
+
try {
|
|
960
|
+
await Promise.race([round, new Promise((resolve) => (timer = setTimeout(resolve, timeoutMs)))]);
|
|
961
|
+
}
|
|
962
|
+
finally {
|
|
963
|
+
clearTimeout(timer);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
async stop() {
|
|
967
|
+
this.isShuttingDown = true;
|
|
968
|
+
clearInterval(this.pingIntervalTimeout?.interval);
|
|
969
|
+
clearTimeout(this.pingIntervalTimeout?.timeout);
|
|
970
|
+
clearTimeout(this.reconnectTimeout);
|
|
971
|
+
if (this.websocket) {
|
|
972
|
+
this.websocket.close();
|
|
973
|
+
}
|
|
974
|
+
await this.awaitRetention();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
exports.ConductorWebsocket = ConductorWebsocket;
|
|
978
|
+
//# sourceMappingURL=conductor.js.map
|