@hahnpro/flow-sdk 2025.2.0-beta.1 → 2025.2.0-beta.3
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/package.json +4 -4
- package/src/{index.ts → index.d.ts} +0 -3
- package/src/index.js +18 -0
- package/src/lib/ContextManager.d.ts +40 -0
- package/src/lib/ContextManager.js +105 -0
- package/src/lib/FlowApplication.d.ts +85 -0
- package/src/lib/FlowApplication.js +528 -0
- package/src/lib/FlowElement.d.ts +67 -0
- package/src/lib/FlowElement.js +178 -0
- package/src/lib/FlowEvent.d.ts +25 -0
- package/src/lib/FlowEvent.js +72 -0
- package/src/lib/FlowLogger.d.ts +44 -0
- package/src/lib/FlowLogger.js +110 -0
- package/src/lib/FlowModule.d.ts +7 -0
- package/src/lib/FlowModule.js +14 -0
- package/src/lib/RpcClient.d.ts +13 -0
- package/src/lib/RpcClient.js +88 -0
- package/src/lib/TestModule.d.ts +2 -0
- package/src/lib/TestModule.js +27 -0
- package/src/lib/amqp.d.ts +14 -0
- package/src/lib/amqp.js +12 -0
- package/src/lib/extra-validators.d.ts +1 -0
- package/src/lib/extra-validators.js +53 -0
- package/src/lib/flow.interface.d.ts +48 -0
- package/src/lib/flow.interface.js +9 -0
- package/src/lib/{index.ts → index.d.ts} +0 -3
- package/src/lib/index.js +18 -0
- package/src/lib/nats.d.ts +12 -0
- package/src/lib/nats.js +115 -0
- package/src/lib/unit-decorators.d.ts +39 -0
- package/src/lib/unit-decorators.js +156 -0
- package/src/lib/unit-utils.d.ts +8 -0
- package/src/lib/unit-utils.js +144 -0
- package/src/lib/units.d.ts +31 -0
- package/src/lib/units.js +572 -0
- package/src/lib/utils.d.ts +51 -0
- package/src/lib/utils.js +178 -0
- package/jest.config.ts +0 -10
- package/project.json +0 -41
- package/src/lib/ContextManager.ts +0 -111
- package/src/lib/FlowApplication.ts +0 -659
- package/src/lib/FlowElement.ts +0 -220
- package/src/lib/FlowEvent.ts +0 -73
- package/src/lib/FlowLogger.ts +0 -131
- package/src/lib/FlowModule.ts +0 -18
- package/src/lib/RpcClient.ts +0 -99
- package/src/lib/TestModule.ts +0 -14
- package/src/lib/__pycache__/rpc_server.cpython-310.pyc +0 -0
- package/src/lib/amqp.ts +0 -32
- package/src/lib/extra-validators.ts +0 -62
- package/src/lib/flow.interface.ts +0 -56
- package/src/lib/nats.ts +0 -140
- package/src/lib/unit-decorators.ts +0 -156
- package/src/lib/unit-utils.ts +0 -163
- package/src/lib/units.ts +0 -587
- package/src/lib/utils.ts +0 -176
- package/test/context-manager-purpose.spec.ts +0 -248
- package/test/context-manager.spec.ts +0 -55
- package/test/context.spec.ts +0 -180
- package/test/event.spec.ts +0 -155
- package/test/extra-validators.spec.ts +0 -84
- package/test/flow-logger.spec.ts +0 -104
- package/test/flow.spec.ts +0 -508
- package/test/input-stream.decorator.spec.ts +0 -379
- package/test/long-rpc.test.py +0 -14
- package/test/long-running-rpc.spec.ts +0 -60
- package/test/message.spec.ts +0 -57
- package/test/mocks/logger.mock.ts +0 -7
- package/test/mocks/nats-connection.mock.ts +0 -135
- package/test/mocks/nats-prepare.reals-nats.ts +0 -15
- package/test/rpc.spec.ts +0 -198
- package/test/rpc.test.py +0 -45
- package/test/rx.spec.ts +0 -92
- package/test/unit-decorator.spec.ts +0 -57
- package/test/utils.spec.ts +0 -210
- package/test/validation.spec.ts +0 -174
- package/tsconfig.json +0 -13
- package/tsconfig.lib.json +0 -22
- package/tsconfig.spec.json +0 -8
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowApplication = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
require("reflect-metadata");
|
|
6
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
7
|
+
const hpc_api_1 = require("@hahnpro/hpc-api");
|
|
8
|
+
const jetstream_1 = require("@nats-io/jetstream");
|
|
9
|
+
const cloudevents_1 = require("cloudevents");
|
|
10
|
+
const lodash_1 = require("lodash");
|
|
11
|
+
const object_sizeof_1 = tslib_1.__importDefault(require("object-sizeof"));
|
|
12
|
+
const rxjs_1 = require("rxjs");
|
|
13
|
+
const operators_1 = require("rxjs/operators");
|
|
14
|
+
const amqp_1 = require("./amqp");
|
|
15
|
+
const ContextManager_1 = require("./ContextManager");
|
|
16
|
+
const flow_interface_1 = require("./flow.interface");
|
|
17
|
+
const FlowLogger_1 = require("./FlowLogger");
|
|
18
|
+
const nats_1 = require("./nats");
|
|
19
|
+
const RpcClient_1 = require("./RpcClient");
|
|
20
|
+
const utils_1 = require("./utils");
|
|
21
|
+
const MAX_EVENT_SIZE_BYTES = +process.env.MAX_EVENT_SIZE_BYTES || 512 * 1024; // 512kb
|
|
22
|
+
const WARN_EVENT_PROCESSING_SEC = +process.env.WARN_EVENT_PROCESSING_SEC || 60;
|
|
23
|
+
const WARN_EVENT_QUEUE_SIZE = +process.env.WARN_EVENT_QUEUE_SIZE || 100;
|
|
24
|
+
class FlowApplication {
|
|
25
|
+
constructor(modules, flow, baseLoggerOrConfig, amqpConnection, natsConnection, skipApi, explicitInit, mockApi) {
|
|
26
|
+
this.modules = modules;
|
|
27
|
+
this.flow = flow;
|
|
28
|
+
this.declarations = {};
|
|
29
|
+
this.elements = {};
|
|
30
|
+
this.initialized = false;
|
|
31
|
+
this.outputStreamMap = new Map();
|
|
32
|
+
this.outputQueueMetrics = new Map();
|
|
33
|
+
this.performanceMap = new Map();
|
|
34
|
+
this.publishLifecycleEvent = async (element, flowEventId, eventType, data = {}) => {
|
|
35
|
+
try {
|
|
36
|
+
const { flowId, deploymentId, id: elementId, functionFqn, inputStreamId } = element.getMetadata();
|
|
37
|
+
const natsEvent = {
|
|
38
|
+
source: `flows/${flowId}/deployments/${deploymentId}/elements/${elementId}`,
|
|
39
|
+
type: eventType,
|
|
40
|
+
data: {
|
|
41
|
+
flowEventId,
|
|
42
|
+
functionFqn,
|
|
43
|
+
inputStreamId,
|
|
44
|
+
...data,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
await (0, nats_1.publishNatsEvent)(this.logger, this._natsConnection, natsEvent, `${nats_1.natsFlowsPrefixFlowDeployment}.flowlifecycle.${deploymentId}`);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
this.logger.error(err);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
this.setQueueMetrics = (id) => {
|
|
54
|
+
const metrics = this.outputQueueMetrics.get(id) || { size: 0, lastAdd: 0, lastRemove: Date.now(), warnings: 0 };
|
|
55
|
+
const secsProcessing = Math.round((metrics.lastAdd - metrics.lastRemove) / 1000);
|
|
56
|
+
metrics.size++;
|
|
57
|
+
metrics.lastAdd = Date.now();
|
|
58
|
+
if (secsProcessing >= WARN_EVENT_PROCESSING_SEC * (metrics.warnings + 1)) {
|
|
59
|
+
this.logger.warn(`Input stream "${id}" has ${metrics.size} queued events and the last event has been processing for ${secsProcessing}s`);
|
|
60
|
+
metrics.warnings++;
|
|
61
|
+
}
|
|
62
|
+
else if (metrics.size % WARN_EVENT_QUEUE_SIZE === 0) {
|
|
63
|
+
this.logger.warn(`Input stream "${id}" has ${metrics.size} queued events`);
|
|
64
|
+
}
|
|
65
|
+
this.outputQueueMetrics.set(id, metrics);
|
|
66
|
+
};
|
|
67
|
+
this.updateMetrics = (id) => {
|
|
68
|
+
const metrics = this.outputQueueMetrics.get(id);
|
|
69
|
+
if (metrics) {
|
|
70
|
+
metrics.size = metrics.size > 0 ? metrics.size - 1 : 0;
|
|
71
|
+
metrics.lastRemove = Date.now();
|
|
72
|
+
metrics.warnings = 0;
|
|
73
|
+
this.outputQueueMetrics.set(id, metrics);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
this.subscribe = (streamId, observer) => this.getOutputStream(streamId).subscribe(observer);
|
|
77
|
+
this.emit = (event) => {
|
|
78
|
+
if (event) {
|
|
79
|
+
try {
|
|
80
|
+
this.publishNatsEventFlowlogs(event);
|
|
81
|
+
if (this.outputStreamMap.has(event.getStreamId())) {
|
|
82
|
+
this.getOutputStream(event.getStreamId()).next(event);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
this.logger.error(err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
this.emitPartial = (completeEvent, partialEvent) => {
|
|
91
|
+
// send complete event, log only partial event
|
|
92
|
+
try {
|
|
93
|
+
if (completeEvent && this.outputStreamMap.has(completeEvent.getStreamId())) {
|
|
94
|
+
this.getOutputStream(completeEvent.getStreamId()).next(completeEvent);
|
|
95
|
+
}
|
|
96
|
+
if (partialEvent) {
|
|
97
|
+
this.publishNatsEventFlowlogs(partialEvent);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
this.logger.error(err);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
this.onMessage = async (cloudEvent) => {
|
|
105
|
+
if (cloudEvent.subject.endsWith('.update')) {
|
|
106
|
+
try {
|
|
107
|
+
const flow = cloudEvent.data;
|
|
108
|
+
if (!flow) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
let context = {};
|
|
112
|
+
if (flow.context) {
|
|
113
|
+
this.context = { ...this.context, ...flow.context };
|
|
114
|
+
context = this.context;
|
|
115
|
+
}
|
|
116
|
+
if (flow.properties) {
|
|
117
|
+
this.contextManager.updateFlowProperties(flow.properties);
|
|
118
|
+
for (const element of Object.values(this.elements)) {
|
|
119
|
+
element.replacePlaceholderAndSetProperties();
|
|
120
|
+
element.onFlowPropertiesChanged?.(flow.properties);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (Object.keys(context).length > 0) {
|
|
124
|
+
for (const element of flow.elements || []) {
|
|
125
|
+
context = { ...context, name: element.name };
|
|
126
|
+
this.elements?.[element.id]?.onContextChanged(context);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const element of flow.elements || []) {
|
|
130
|
+
this.elements?.[element.id]?.setPropertiesWithPlaceholders((0, lodash_1.cloneDeep)(element.properties));
|
|
131
|
+
this.elements?.[element.id]?.onPropertiesChanged(this.contextManager.replaceAllPlaceholderProperties(this.elements[element.id].getPropertiesWithPlaceholders()));
|
|
132
|
+
}
|
|
133
|
+
const natsEvent = {
|
|
134
|
+
source: `hpc/flow-application`,
|
|
135
|
+
type: `${nats_1.natsFlowsPrefixFlowDeployment}.health`,
|
|
136
|
+
subject: `${this.context.deploymentId}`,
|
|
137
|
+
data: {
|
|
138
|
+
deploymentId: this.context.deploymentId,
|
|
139
|
+
status: 'updated',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
await (0, nats_1.publishNatsEvent)(this.logger, this._natsConnection, natsEvent);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
this.logger.error(err);
|
|
146
|
+
const natsEvent = {
|
|
147
|
+
source: `hpc/flow-application`,
|
|
148
|
+
type: `${nats_1.natsFlowsPrefixFlowDeployment}.health`,
|
|
149
|
+
subject: `${this.context.deploymentId}`,
|
|
150
|
+
data: {
|
|
151
|
+
deploymentId: this.context.deploymentId,
|
|
152
|
+
status: 'updating failed',
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
await (0, nats_1.publishNatsEvent)(this.logger, this._natsConnection, natsEvent);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (cloudEvent.subject.endsWith('.message')) {
|
|
159
|
+
const data = cloudEvent.data;
|
|
160
|
+
const elementId = data?.elementId;
|
|
161
|
+
if (elementId) {
|
|
162
|
+
this.elements?.[elementId]?.onMessage?.(data);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
for (const element of Object.values(this.elements)) {
|
|
166
|
+
element?.onMessage?.(data);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (cloudEvent.subject.endsWith('.destroy')) {
|
|
171
|
+
// TODO war com.flowstudio.deployment.destroy in RabbitMq: wo wird das jetzt wieder gesendet?
|
|
172
|
+
this.destroy();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Publish a flow event to the amqp flowlogs exchange.
|
|
177
|
+
* If the event size exceeds the limit it will be truncated
|
|
178
|
+
*
|
|
179
|
+
* TODO warum darf hier nicht false zurückgegeben werden? -> erzeugt loop
|
|
180
|
+
*/
|
|
181
|
+
this.publishNatsEventFlowlogs = async (event) => {
|
|
182
|
+
if (!this._natsConnection || this._natsConnection.isClosed()) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const formatedEvent = event.format();
|
|
187
|
+
if ((0, object_sizeof_1.default)(formatedEvent) > MAX_EVENT_SIZE_BYTES) {
|
|
188
|
+
formatedEvent.data = (0, utils_1.truncate)(formatedEvent.data);
|
|
189
|
+
}
|
|
190
|
+
const natsEvent = {
|
|
191
|
+
source: `hpc/flow-application`,
|
|
192
|
+
type: `${nats_1.natsFlowsPrefixFlowDeployment}.flowlogs`,
|
|
193
|
+
subject: `${this.context.deploymentId}`,
|
|
194
|
+
data: formatedEvent,
|
|
195
|
+
};
|
|
196
|
+
await (0, nats_1.publishNatsEvent)(this.logger, this._natsConnection, natsEvent);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
this.logger.error(err);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
if (baseLoggerOrConfig && !baseLoggerOrConfig.log) {
|
|
205
|
+
const config = baseLoggerOrConfig;
|
|
206
|
+
this.baseLogger = config.logger;
|
|
207
|
+
this.amqpConnection = config.amqpConnection || (0, amqp_1.createAmqpConnection)(config.amqpConfig);
|
|
208
|
+
this.natsConnectionConfig = config.natsConfig;
|
|
209
|
+
this._natsConnection = config.natsConnection;
|
|
210
|
+
this.skipApi = config.skipApi || false;
|
|
211
|
+
explicitInit = config.explicitInit || false;
|
|
212
|
+
this._api = config.mockApi || null;
|
|
213
|
+
this.apiClient = config.apiClient;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
this.baseLogger = baseLoggerOrConfig;
|
|
217
|
+
this.amqpConnection = amqpConnection?.managedConnection;
|
|
218
|
+
this._natsConnection = natsConnection;
|
|
219
|
+
this.skipApi = skipApi || false;
|
|
220
|
+
explicitInit = explicitInit || false;
|
|
221
|
+
this._api = mockApi || null;
|
|
222
|
+
}
|
|
223
|
+
this.logger = new FlowLogger_1.FlowLogger({ id: 'none', functionFqn: 'FlowApplication', ...flow?.context }, this.baseLogger || undefined, this.publishNatsEventFlowlogs);
|
|
224
|
+
this.contextManager = new ContextManager_1.ContextManager(this.logger, this.flow?.properties);
|
|
225
|
+
process.once('uncaughtException', (err) => {
|
|
226
|
+
this.logger.error('Uncaught exception!');
|
|
227
|
+
this.logger.error(err);
|
|
228
|
+
this.destroy(1);
|
|
229
|
+
});
|
|
230
|
+
process.on('unhandledRejection', (reason) => {
|
|
231
|
+
this.logger.error('Unhandled promise rejection!');
|
|
232
|
+
this.logger.error(reason);
|
|
233
|
+
});
|
|
234
|
+
process.on('SIGTERM', () => {
|
|
235
|
+
this.logger.log('Flow Deployment is terminating');
|
|
236
|
+
this.destroy(0);
|
|
237
|
+
});
|
|
238
|
+
if (explicitInit !== true) {
|
|
239
|
+
this.init();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
get rpcClient() {
|
|
243
|
+
if (!this._rpcClient && this.amqpConnection) {
|
|
244
|
+
this._rpcClient = new RpcClient_1.RpcClient(this.amqpConnection, this.logger);
|
|
245
|
+
}
|
|
246
|
+
return this._rpcClient;
|
|
247
|
+
}
|
|
248
|
+
get api() {
|
|
249
|
+
return this._api;
|
|
250
|
+
}
|
|
251
|
+
get natsConnection() {
|
|
252
|
+
return this._natsConnection;
|
|
253
|
+
}
|
|
254
|
+
getContextManager() {
|
|
255
|
+
return this.contextManager;
|
|
256
|
+
}
|
|
257
|
+
getProperties() {
|
|
258
|
+
return this.contextManager.getProperties();
|
|
259
|
+
}
|
|
260
|
+
async consumeNatsMessagesOfConsumer(consumer, consumeOptions) {
|
|
261
|
+
if (this.natsMessageIterator) {
|
|
262
|
+
await this.natsMessageIterator.close();
|
|
263
|
+
}
|
|
264
|
+
this.natsMessageIterator = await consumer.consume(consumeOptions);
|
|
265
|
+
for await (const msg of this.natsMessageIterator) {
|
|
266
|
+
try {
|
|
267
|
+
let event;
|
|
268
|
+
try {
|
|
269
|
+
event = new cloudevents_1.CloudEvent(msg.json());
|
|
270
|
+
event.validate();
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
this.logger.error('Message is not a valid CloudEvent and will be discarded');
|
|
274
|
+
msg.ack(); // Acknowledge the message to remove it from the queue
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
await this.onMessage(event);
|
|
278
|
+
msg.ack();
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
this.logger.error('Error processing message');
|
|
282
|
+
this.logger.error(error);
|
|
283
|
+
msg.nak(1000);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async init() {
|
|
288
|
+
if (this.initialized)
|
|
289
|
+
return;
|
|
290
|
+
this.context = { ...this.flow.context };
|
|
291
|
+
this.contextManager.overwriteAllProperties(this.flow.properties ?? {});
|
|
292
|
+
try {
|
|
293
|
+
if (!this.skipApi && !(this._api instanceof hpc_api_1.MockAPI)) {
|
|
294
|
+
const { owner } = this.context;
|
|
295
|
+
// only create real API if it should not be skipped and is not already a mock
|
|
296
|
+
this._api = new hpc_api_1.API(this.apiClient, { activeOrg: owner?.id });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
this.logger.error(err?.message || err);
|
|
301
|
+
}
|
|
302
|
+
const logErrorAndExit = async (err) => {
|
|
303
|
+
this.logger.error(new Error(err));
|
|
304
|
+
await this.destroy(1);
|
|
305
|
+
};
|
|
306
|
+
if (!this._natsConnection && this.natsConnectionConfig) {
|
|
307
|
+
try {
|
|
308
|
+
this._natsConnection = await (0, nats_1.createNatsConnection)(this.natsConnectionConfig);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
await logErrorAndExit(`Could not connect to the NATS-Servers: ${err}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (this._natsConnection && !this._natsConnection.isClosed() && this.context?.deploymentId !== undefined) {
|
|
315
|
+
try {
|
|
316
|
+
const consumerOptions = {
|
|
317
|
+
...nats_1.defaultConsumerConfig,
|
|
318
|
+
name: `flow-deployment-${this.context.deploymentId}`,
|
|
319
|
+
filter_subject: `${nats_1.natsFlowsPrefixFlowDeployment}.${this.context.deploymentId}.*`,
|
|
320
|
+
inactive_threshold: 10 * 60 * 1000000000, // 10 mins
|
|
321
|
+
deliver_policy: jetstream_1.DeliverPolicy.New,
|
|
322
|
+
};
|
|
323
|
+
const consumer = await (0, nats_1.getOrCreateConsumer)(this.logger, this._natsConnection, nats_1.FLOWS_STREAM_NAME, consumerOptions.name, consumerOptions);
|
|
324
|
+
// Recreate consumers on reconnects: NO AWAIT, listen asynchronously
|
|
325
|
+
const handleNatsStatus = async () => {
|
|
326
|
+
try {
|
|
327
|
+
this.logger.debug('ConsumerService: Reconnected to Nats and re-creating non-durable consumers');
|
|
328
|
+
await (0, nats_1.getOrCreateConsumer)(this.logger, this._natsConnection, nats_1.FLOWS_STREAM_NAME, consumerOptions.name, consumerOptions);
|
|
329
|
+
this.consumeNatsMessagesOfConsumer(consumer, { expires: 10 * 1000000000 /* 10 seconds */ });
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
this.logger.error('NATS Status-AsyncIterator is not available, cannot listen. Due to error:');
|
|
333
|
+
this.logger.error(e);
|
|
334
|
+
(0, nats_1.natsEventListener)(this._natsConnection, this.logger, handleNatsStatus);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
(0, nats_1.natsEventListener)(this._natsConnection, this.logger, handleNatsStatus);
|
|
338
|
+
// NO AWAIT, listen for messages of the consumer asynchronously
|
|
339
|
+
this.consumeNatsMessagesOfConsumer(consumer, { expires: 10 * 1000000000 /* 10 seconds */ });
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
await logErrorAndExit(`Could not set up consumer for deployment messages exchanges: ${e}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
this.amqpChannel = this.amqpConnection?.createChannel({
|
|
346
|
+
json: true,
|
|
347
|
+
setup: async (channel) => {
|
|
348
|
+
try {
|
|
349
|
+
await channel.assertExchange('flow', 'direct', { durable: true }); // TODO wieso weshalb warum: wo wird das gebraucht?
|
|
350
|
+
}
|
|
351
|
+
catch (e) {
|
|
352
|
+
await logErrorAndExit(`Could not assert exchanges: ${e}`);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
if (this.amqpChannel) {
|
|
357
|
+
await this.amqpChannel.waitForConnect();
|
|
358
|
+
}
|
|
359
|
+
for (const module of this.modules) {
|
|
360
|
+
const moduleName = Reflect.getMetadata('module:name', module);
|
|
361
|
+
const moduleDeclarations = Reflect.getMetadata('module:declarations', module);
|
|
362
|
+
if (!moduleName || !moduleDeclarations || !Array.isArray(moduleDeclarations)) {
|
|
363
|
+
await logErrorAndExit(`FlowModule (${module.name}) metadata is missing or invalid`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
for (const declaration of moduleDeclarations) {
|
|
367
|
+
const functionFqn = Reflect.getMetadata('element:functionFqn', declaration);
|
|
368
|
+
if (!functionFqn) {
|
|
369
|
+
await logErrorAndExit(`FlowFunction (${declaration.name}) metadata is missing or invalid`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
this.declarations[`${moduleName}.${functionFqn}`] = declaration;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const element of this.flow.elements) {
|
|
376
|
+
const { id, name, properties, module, functionFqn } = element;
|
|
377
|
+
try {
|
|
378
|
+
const context = { ...this.context, id, name, logger: this.baseLogger, app: this };
|
|
379
|
+
this.elements[id] = new this.declarations[`${module}.${functionFqn}`](context,
|
|
380
|
+
// run recursively through all properties and interpolate them / replace them with their explicit value
|
|
381
|
+
this.contextManager.replaceAllPlaceholderProperties(properties));
|
|
382
|
+
this.elements[id].setPropertiesWithPlaceholders((0, lodash_1.cloneDeep)(properties));
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
await logErrorAndExit(`Could not create FlowElement for ${module}.${functionFqn}`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
for (const connection of this.flow.connections) {
|
|
390
|
+
const { source, target, sourceStream = 'default', targetStream = 'default' } = connection;
|
|
391
|
+
if (!source || !target) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const sourceStreamId = `${source}.${sourceStream}`;
|
|
395
|
+
const targetStreamId = `${target}.${targetStream}`;
|
|
396
|
+
const element = this.elements[target];
|
|
397
|
+
if (!element || !element.constructor) {
|
|
398
|
+
await logErrorAndExit(`${target} has not been initialized`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const streamHandler = Reflect.getMetadata(`stream:${targetStream}`, element.constructor);
|
|
402
|
+
if (!streamHandler || !element[streamHandler]) {
|
|
403
|
+
await logErrorAndExit(`${target} does not implement a handler for ${targetStream}`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const streamOptions = Reflect.getMetadata(`stream:options:${targetStream}`, element.constructor) || {};
|
|
407
|
+
const concurrent = streamOptions.concurrent || 1;
|
|
408
|
+
const outputStream = this.getOutputStream(sourceStreamId);
|
|
409
|
+
outputStream
|
|
410
|
+
.pipe((0, operators_1.tap)(() => this.setQueueMetrics(targetStreamId)), (0, operators_1.mergeMap)(async (event) => {
|
|
411
|
+
const eventId = event.getId();
|
|
412
|
+
this.publishLifecycleEvent(element, eventId, flow_interface_1.LifecycleEvent.ACTIVATED);
|
|
413
|
+
this.performanceMap.set(eventId, perf_hooks_1.performance.eventLoopUtilization());
|
|
414
|
+
const start = perf_hooks_1.performance.now();
|
|
415
|
+
try {
|
|
416
|
+
await element[streamHandler](event);
|
|
417
|
+
const duration = Math.ceil(perf_hooks_1.performance.now() - start);
|
|
418
|
+
this.publishLifecycleEvent(element, eventId, flow_interface_1.LifecycleEvent.COMPLETED, { duration });
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
const duration = Math.ceil(perf_hooks_1.performance.now() - start);
|
|
422
|
+
this.publishLifecycleEvent(element, eventId, flow_interface_1.LifecycleEvent.TERMINATED, { duration });
|
|
423
|
+
try {
|
|
424
|
+
element.handleApiError(err);
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
this.logger.error(err);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return event;
|
|
431
|
+
}, concurrent), (0, operators_1.tap)((event) => {
|
|
432
|
+
this.updateMetrics(targetStreamId);
|
|
433
|
+
let elu = this.performanceMap.get(event.getId());
|
|
434
|
+
if (elu) {
|
|
435
|
+
this.performanceMap.delete(event.getId());
|
|
436
|
+
elu = perf_hooks_1.performance.eventLoopUtilization(elu);
|
|
437
|
+
if (elu.utilization > 0.75 && elu.active > 2000) {
|
|
438
|
+
this.logger.warn(`High event loop utilization detected for ${targetStreamId} with event ${event.getId()}! Handler was active for ${Number(elu.active).toFixed(2)}ms with a utilization of ${Number(elu.utilization * 100).toFixed(2)}%. Consider refactoring or move tasks to a worker thread.`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}))
|
|
442
|
+
.subscribe();
|
|
443
|
+
}
|
|
444
|
+
this.initialized = true;
|
|
445
|
+
this.logger.log('Flow Deployment is running');
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Calls onDestroy lifecycle method on all flow elements,
|
|
449
|
+
* closes amqp connection after allowing logs to be processed and published
|
|
450
|
+
* then exits process
|
|
451
|
+
*/
|
|
452
|
+
async destroy(exitCode = 0) {
|
|
453
|
+
try {
|
|
454
|
+
try {
|
|
455
|
+
for (const element of Object.values(this.elements)) {
|
|
456
|
+
element?.onDestroy?.();
|
|
457
|
+
}
|
|
458
|
+
if (this._rpcClient) {
|
|
459
|
+
await this._rpcClient.close();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
this.logger.error(err);
|
|
464
|
+
}
|
|
465
|
+
// allow time for logs to be processed
|
|
466
|
+
await (0, utils_1.delay)(250);
|
|
467
|
+
if (this.amqpConnection) {
|
|
468
|
+
await this.amqpConnection.close();
|
|
469
|
+
}
|
|
470
|
+
// Close all output streams
|
|
471
|
+
for (const [id, stream] of this.outputStreamMap.entries()) {
|
|
472
|
+
try {
|
|
473
|
+
stream?.complete();
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
this.logger.error(`Error completing output stream ${id}: ${err.message}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Nats: Delete consumer for flow deployment, stop message listening and close connection
|
|
480
|
+
try {
|
|
481
|
+
await this.natsMessageIterator?.close();
|
|
482
|
+
await this._natsConnection?.drain();
|
|
483
|
+
await this._natsConnection?.close();
|
|
484
|
+
if (this._natsConnection && !this._natsConnection.isClosed()) {
|
|
485
|
+
await (0, jetstream_1.jetstreamManager)(this._natsConnection).then((jsm) => {
|
|
486
|
+
jsm.consumers
|
|
487
|
+
.delete(nats_1.FLOWS_STREAM_NAME, `flow-deployment-${this.context?.deploymentId}`)
|
|
488
|
+
.then(() => {
|
|
489
|
+
this.logger.debug(`Deleted consumer for flow deployment ${this.context?.deploymentId}`);
|
|
490
|
+
})
|
|
491
|
+
.catch((err) => {
|
|
492
|
+
this.logger.error(`Could not delete consumer for flow deployment ${this.context?.deploymentId}: ${err.message}`);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
this.logger.error(err);
|
|
499
|
+
}
|
|
500
|
+
// remove process listeners
|
|
501
|
+
process.removeAllListeners('SIGTERM');
|
|
502
|
+
process.removeAllListeners('uncaughtException');
|
|
503
|
+
process.removeAllListeners('unhandledRejection');
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
/* eslint-disable-next-line no-console */
|
|
507
|
+
console.error(err);
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
511
|
+
process.exit(exitCode);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Returns rxjs subject for the specified stream id.
|
|
517
|
+
* A new subject will be created if one doesn't exist yet.
|
|
518
|
+
*/
|
|
519
|
+
getOutputStream(id) {
|
|
520
|
+
const stream = this.outputStreamMap.get(id);
|
|
521
|
+
if (!stream) {
|
|
522
|
+
this.outputStreamMap.set(id, new rxjs_1.Subject());
|
|
523
|
+
return this.outputStreamMap.get(id);
|
|
524
|
+
}
|
|
525
|
+
return stream;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
exports.FlowApplication = FlowApplication;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { API } from '@hahnpro/hpc-api';
|
|
2
|
+
import { PythonShell } from 'python-shell';
|
|
3
|
+
import { ClassType, DeploymentMessage, FlowContext, FlowElementContext } from './flow.interface';
|
|
4
|
+
import { Context } from './FlowApplication';
|
|
5
|
+
import { FlowEvent } from './FlowEvent';
|
|
6
|
+
import { FlowLogger } from './FlowLogger';
|
|
7
|
+
export declare abstract class FlowElement<T = any> {
|
|
8
|
+
private readonly propertiesClassType?;
|
|
9
|
+
private readonly whitelist;
|
|
10
|
+
readonly functionFqn: string;
|
|
11
|
+
protected readonly api?: API;
|
|
12
|
+
protected readonly logger: FlowLogger;
|
|
13
|
+
protected metadata: FlowElementContext;
|
|
14
|
+
protected properties: T;
|
|
15
|
+
private propertiesWithPlaceholders;
|
|
16
|
+
private readonly app?;
|
|
17
|
+
private readonly rpcRoutingKey;
|
|
18
|
+
private stopPropagateStream;
|
|
19
|
+
constructor({ app, logger, ...metadata }: Context, properties?: unknown, propertiesClassType?: ClassType<T>, whitelist?: boolean);
|
|
20
|
+
/**
|
|
21
|
+
* Sets the placeholder properties for this flow element
|
|
22
|
+
* @param propertiesWithPlaceholders
|
|
23
|
+
*/
|
|
24
|
+
setPropertiesWithPlaceholders(propertiesWithPlaceholders: T): void;
|
|
25
|
+
/**
|
|
26
|
+
* Returns the placeholder properties for this flow element
|
|
27
|
+
*/
|
|
28
|
+
getPropertiesWithPlaceholders(): T;
|
|
29
|
+
get flowProperties(): Record<string, any>;
|
|
30
|
+
get natsConnection(): import("@nats-io/nats-core").NatsConnection;
|
|
31
|
+
onDestroy?: () => void;
|
|
32
|
+
onMessage?: (message: DeploymentMessage) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Replace all placeholder properties with their explicit updated value and set them as the properties of the element
|
|
35
|
+
*/
|
|
36
|
+
replacePlaceholderAndSetProperties(): void;
|
|
37
|
+
onFlowPropertiesChanged?: (properties: Record<string, any>) => void;
|
|
38
|
+
onContextChanged: (context: Partial<FlowContext>) => void;
|
|
39
|
+
onPropertiesChanged: (properties: T) => void;
|
|
40
|
+
getMetadata: () => FlowElementContext;
|
|
41
|
+
protected setProperties: (properties: T) => void;
|
|
42
|
+
handleApiError: (error: any) => void;
|
|
43
|
+
/**
|
|
44
|
+
* @deprecated since version 4.8.0, will be removed in 5.0.0, use emitEvent(...) instead
|
|
45
|
+
*/
|
|
46
|
+
protected emitOutput(data?: any, outputId?: string, time?: Date): FlowEvent;
|
|
47
|
+
protected emitEvent(data: Record<string, any>, inputEvent: FlowEvent, outputId?: string, time?: Date): FlowEvent;
|
|
48
|
+
protected validateProperties<P>(classType: ClassType<P>, properties?: any, whitelist?: boolean): P;
|
|
49
|
+
protected logValidationErrors(error: any, parent?: string): void;
|
|
50
|
+
protected validateEventData<E>(classType: ClassType<E>, event: FlowEvent, whitelist?: boolean): E;
|
|
51
|
+
protected interpolate: (value: any, ...templateVariables: any) => any;
|
|
52
|
+
protected callRpcFunction(functionName: string, ...args: any[]): Promise<unknown>;
|
|
53
|
+
protected runPyRpcScript(scriptPath: string, ...args: (string | boolean | number)[]): PythonShell;
|
|
54
|
+
}
|
|
55
|
+
export declare function InputStream(id?: string, options?: {
|
|
56
|
+
concurrent?: number;
|
|
57
|
+
stopPropagation?: boolean;
|
|
58
|
+
}): MethodDecorator;
|
|
59
|
+
export declare function FlowFunction(fqn: string): ClassDecorator;
|
|
60
|
+
export declare abstract class FlowResource<T = any> extends FlowElement<T> {
|
|
61
|
+
}
|
|
62
|
+
export declare abstract class FlowTask<T = any> extends FlowElement<T> {
|
|
63
|
+
}
|
|
64
|
+
export declare abstract class FlowTrigger<T = any> extends FlowElement<T> {
|
|
65
|
+
}
|
|
66
|
+
export declare abstract class FlowDashboard<T = any> extends FlowResource<T> {
|
|
67
|
+
}
|