@hahnpro/flow-sdk 5.0.0-0 → 5.0.1
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/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/FlowApplication.d.ts +53 -14
- package/dist/FlowApplication.js +266 -85
- package/dist/FlowElement.d.ts +12 -4
- package/dist/FlowElement.js +47 -48
- package/dist/FlowEvent.d.ts +7 -1
- package/dist/FlowEvent.js +5 -5
- package/dist/FlowLogger.d.ts +9 -6
- package/dist/FlowLogger.js +17 -18
- package/dist/FlowModule.d.ts +1 -1
- package/dist/FlowModule.js +3 -3
- package/dist/RpcClient.d.ts +5 -3
- package/dist/RpcClient.js +23 -9
- package/dist/TestModule.js +6 -6
- package/dist/amqp.d.ts +14 -0
- package/dist/amqp.js +13 -0
- package/dist/extra-validators.js +15 -8
- package/dist/flow.interface.d.ts +7 -1
- package/dist/flow.interface.js +7 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/rpc_server.py +89 -77
- package/dist/unit-decorators.d.ts +39 -0
- package/dist/unit-decorators.js +157 -0
- package/dist/unit-utils.d.ts +8 -0
- package/dist/unit-utils.js +144 -0
- package/dist/units.d.ts +31 -0
- package/dist/units.js +570 -0
- package/dist/utils.d.ts +5 -1
- package/dist/utils.js +40 -7
- package/package.json +35 -33
- package/dist/api/Queue.d.ts +0 -15
- package/dist/api/Queue.js +0 -25
- package/dist/api/api.d.ts +0 -22
- package/dist/api/api.interface.d.ts +0 -14
- package/dist/api/api.interface.js +0 -2
- package/dist/api/api.js +0 -38
- package/dist/api/asset.interface.d.ts +0 -45
- package/dist/api/asset.interface.js +0 -2
- package/dist/api/asset.service.d.ts +0 -8
- package/dist/api/asset.service.js +0 -18
- package/dist/api/content.interface.d.ts +0 -33
- package/dist/api/content.interface.js +0 -2
- package/dist/api/content.service.d.ts +0 -9
- package/dist/api/content.service.js +0 -22
- package/dist/api/data.interface.d.ts +0 -29
- package/dist/api/data.interface.js +0 -2
- package/dist/api/data.service.d.ts +0 -15
- package/dist/api/data.service.js +0 -50
- package/dist/api/endpoint.interface.d.ts +0 -18
- package/dist/api/endpoint.interface.js +0 -2
- package/dist/api/endpoint.service.d.ts +0 -7
- package/dist/api/endpoint.service.js +0 -14
- package/dist/api/http.service.d.ts +0 -29
- package/dist/api/http.service.js +0 -96
- package/dist/api/index.d.ts +0 -11
- package/dist/api/index.js +0 -15
- package/dist/api/mock/api.mock.d.ts +0 -102
- package/dist/api/mock/api.mock.js +0 -81
- package/dist/api/mock/asset.mock.service.d.ts +0 -9
- package/dist/api/mock/asset.mock.service.js +0 -18
- package/dist/api/mock/content.mock.service.d.ts +0 -9
- package/dist/api/mock/content.mock.service.js +0 -21
- package/dist/api/mock/data.mock.service.d.ts +0 -11
- package/dist/api/mock/data.mock.service.js +0 -53
- package/dist/api/mock/endpoint.mock.service.d.ts +0 -10
- package/dist/api/mock/endpoint.mock.service.js +0 -15
- package/dist/api/mock/index.d.ts +0 -7
- package/dist/api/mock/index.js +0 -10
- package/dist/api/mock/secret.mock.service.d.ts +0 -5
- package/dist/api/mock/secret.mock.service.js +0 -11
- package/dist/api/mock/task.mock.service.d.ts +0 -8
- package/dist/api/mock/task.mock.service.js +0 -16
- package/dist/api/mock/timeseries.mock.service.d.ts +0 -18
- package/dist/api/mock/timeseries.mock.service.js +0 -70
- package/dist/api/mock/user.mock.service.d.ts +0 -7
- package/dist/api/mock/user.mock.service.js +0 -12
- package/dist/api/secret.interface.d.ts +0 -9
- package/dist/api/secret.interface.js +0 -2
- package/dist/api/secret.service.d.ts +0 -6
- package/dist/api/secret.service.js +0 -10
- package/dist/api/sidriveiq.interface.d.ts +0 -104
- package/dist/api/sidriveiq.interface.js +0 -2
- package/dist/api/sidriveiq.service.d.ts +0 -31
- package/dist/api/sidriveiq.service.js +0 -97
- package/dist/api/task.interface.d.ts +0 -26
- package/dist/api/task.interface.js +0 -2
- package/dist/api/task.service.d.ts +0 -7
- package/dist/api/task.service.js +0 -13
- package/dist/api/timeseries.interface.d.ts +0 -51
- package/dist/api/timeseries.interface.js +0 -2
- package/dist/api/timeseries.service.d.ts +0 -17
- package/dist/api/timeseries.service.js +0 -38
- package/dist/api/user.interface.d.ts +0 -3
- package/dist/api/user.interface.js +0 -2
- package/dist/api/user.service.d.ts +0 -7
- package/dist/api/user.service.js +0 -21
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,34 +1,73 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { API, HttpClient, MockAPI } from '@hahnpro/hpc-api';
|
|
3
|
+
import { ConsumeMessage } from 'amqplib';
|
|
4
|
+
import { AmqpConnectionManager } from 'amqp-connection-manager';
|
|
4
5
|
import { PartialObserver } from 'rxjs';
|
|
5
|
-
import {
|
|
6
|
+
import { AmqpConnection, AmqpConnectionConfig } from './amqp';
|
|
6
7
|
import { ClassType, Flow, FlowElementContext } from './flow.interface';
|
|
7
8
|
import { FlowEvent } from './FlowEvent';
|
|
8
9
|
import { Logger } from './FlowLogger';
|
|
9
10
|
import { RpcClient } from './RpcClient';
|
|
11
|
+
interface FlowAppConfig {
|
|
12
|
+
logger?: Logger;
|
|
13
|
+
amqpConfig?: AmqpConnectionConfig;
|
|
14
|
+
amqpConnection?: AmqpConnectionManager;
|
|
15
|
+
apiClient?: HttpClient;
|
|
16
|
+
skipApi?: boolean;
|
|
17
|
+
explicitInit?: boolean;
|
|
18
|
+
mockApi?: MockAPI;
|
|
19
|
+
}
|
|
10
20
|
export declare class FlowApplication {
|
|
11
|
-
private
|
|
12
|
-
|
|
21
|
+
private modules;
|
|
22
|
+
private flow;
|
|
23
|
+
private _api;
|
|
24
|
+
private _rpcClient;
|
|
25
|
+
private amqpChannel;
|
|
26
|
+
private readonly amqpConnection;
|
|
27
|
+
private readonly baseLogger;
|
|
13
28
|
private context;
|
|
14
29
|
private declarations;
|
|
15
30
|
private elements;
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
private _rpcClient;
|
|
31
|
+
private initialized;
|
|
32
|
+
private readonly logger;
|
|
19
33
|
private outputStreamMap;
|
|
20
|
-
|
|
34
|
+
private outputQueueMetrics;
|
|
35
|
+
private performanceMap;
|
|
36
|
+
private properties;
|
|
37
|
+
private readonly skipApi;
|
|
38
|
+
private readonly apiClient?;
|
|
39
|
+
constructor(modules: ClassType<any>[], flow: Flow, config?: FlowAppConfig);
|
|
40
|
+
constructor(modules: ClassType<any>[], flow: Flow, baseLogger?: Logger, amqpConnection?: AmqpConnection, skipApi?: boolean, explicitInit?: boolean);
|
|
41
|
+
get rpcClient(): RpcClient;
|
|
42
|
+
get api(): API;
|
|
43
|
+
init(): Promise<void>;
|
|
44
|
+
private publishLifecycleEvent;
|
|
45
|
+
private setQueueMetrics;
|
|
46
|
+
private updateMetrics;
|
|
21
47
|
subscribe: (streamId: string, observer: PartialObserver<FlowEvent>) => import("rxjs").Subscription;
|
|
22
48
|
emit: (event: FlowEvent) => void;
|
|
49
|
+
emitPartial: (completeEvent: FlowEvent, partialEvent: FlowEvent) => void;
|
|
23
50
|
getProperties(): Record<string, any>;
|
|
24
|
-
onMessage: (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
onMessage: (msg: ConsumeMessage) => Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Publish a flow event to the amqp flowlogs exchange.
|
|
54
|
+
* If the event size exceeds the limit it will be truncated
|
|
55
|
+
*/
|
|
56
|
+
publishEvent: (event: FlowEvent) => Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Calls onDestroy lifecycle method on all flow elements,
|
|
59
|
+
* closes amqp connection after allowing logs to be processed and published
|
|
60
|
+
* then exits process
|
|
61
|
+
*/
|
|
62
|
+
destroy(exitCode?: number): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Returns rxjs subject for the specified stream id.
|
|
65
|
+
* A new subject will be created if one doesn't exist yet.
|
|
66
|
+
*/
|
|
28
67
|
private getOutputStream;
|
|
29
|
-
private truncate;
|
|
30
68
|
}
|
|
31
69
|
export interface Context extends FlowElementContext {
|
|
32
70
|
app?: FlowApplication;
|
|
33
71
|
logger?: Logger;
|
|
34
72
|
}
|
|
73
|
+
export {};
|
package/dist/FlowApplication.js
CHANGED
|
@@ -3,93 +3,181 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FlowApplication = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
require("reflect-metadata");
|
|
6
|
-
const
|
|
6
|
+
const hpc_api_1 = require("@hahnpro/hpc-api");
|
|
7
|
+
const cloudevents_1 = require("cloudevents");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
7
9
|
const object_sizeof_1 = tslib_1.__importDefault(require("object-sizeof"));
|
|
10
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
8
11
|
const rxjs_1 = require("rxjs");
|
|
9
12
|
const operators_1 = require("rxjs/operators");
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const api_1 = require("./api");
|
|
13
|
+
const amqp_1 = require("./amqp");
|
|
14
|
+
const flow_interface_1 = require("./flow.interface");
|
|
13
15
|
const FlowLogger_1 = require("./FlowLogger");
|
|
14
16
|
const RpcClient_1 = require("./RpcClient");
|
|
15
|
-
const
|
|
17
|
+
const utils_1 = require("./utils");
|
|
18
|
+
const MAX_EVENT_SIZE_BYTES = +process.env.MAX_EVENT_SIZE_BYTES || 512 * 1024;
|
|
19
|
+
const WARN_EVENT_PROCESSING_SEC = +process.env.WARN_EVENT_PROCESSING_SEC || 60;
|
|
20
|
+
const WARN_EVENT_QUEUE_SIZE = +process.env.WARN_EVENT_QUEUE_SIZE || 100;
|
|
16
21
|
class FlowApplication {
|
|
17
|
-
constructor(modules, flow,
|
|
18
|
-
this.
|
|
22
|
+
constructor(modules, flow, baseLoggerOrConfig, amqpConnection, skipApi, explicitInit, mockApi) {
|
|
23
|
+
this.modules = modules;
|
|
24
|
+
this.flow = flow;
|
|
19
25
|
this.declarations = {};
|
|
20
26
|
this.elements = {};
|
|
21
|
-
this.
|
|
27
|
+
this.initialized = false;
|
|
28
|
+
this.outputStreamMap = new Map();
|
|
29
|
+
this.outputQueueMetrics = new Map();
|
|
30
|
+
this.performanceMap = new Map();
|
|
31
|
+
this.publishLifecycleEvent = (element, flowEventId, eventType, data = {}) => {
|
|
32
|
+
if (!this.amqpChannel) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const { flowId, deploymentId, id: elementId, functionFqn, inputStreamId } = element.getMetadata();
|
|
37
|
+
const event = new cloudevents_1.CloudEvent({
|
|
38
|
+
source: `flows/${flowId}/deployments/${deploymentId}/elements/${elementId}`,
|
|
39
|
+
type: eventType,
|
|
40
|
+
data: {
|
|
41
|
+
flowEventId,
|
|
42
|
+
functionFqn,
|
|
43
|
+
inputStreamId,
|
|
44
|
+
...data,
|
|
45
|
+
},
|
|
46
|
+
time: new Date().toISOString(),
|
|
47
|
+
});
|
|
48
|
+
const message = event.toJSON();
|
|
49
|
+
return this.amqpChannel.publish('flow', 'lifecycle', message);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
this.logger.error(err);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
this.setQueueMetrics = (id) => {
|
|
56
|
+
const metrics = this.outputQueueMetrics.get(id) || { size: 0, lastAdd: 0, lastRemove: Date.now(), warnings: 0 };
|
|
57
|
+
const secsProcessing = Math.round((metrics.lastAdd - metrics.lastRemove) / 1000);
|
|
58
|
+
metrics.size++;
|
|
59
|
+
metrics.lastAdd = Date.now();
|
|
60
|
+
if (secsProcessing >= WARN_EVENT_PROCESSING_SEC * (metrics.warnings + 1)) {
|
|
61
|
+
this.logger.warn(`Input stream "${id}" has ${metrics.size} queued events and the last event has been processing for ${secsProcessing}s`);
|
|
62
|
+
metrics.warnings++;
|
|
63
|
+
}
|
|
64
|
+
else if (metrics.size % WARN_EVENT_QUEUE_SIZE === 0) {
|
|
65
|
+
this.logger.warn(`Input stream "${id}" has ${metrics.size} queued events`);
|
|
66
|
+
}
|
|
67
|
+
this.outputQueueMetrics.set(id, metrics);
|
|
68
|
+
};
|
|
69
|
+
this.updateMetrics = (id) => {
|
|
70
|
+
const metrics = this.outputQueueMetrics.get(id);
|
|
71
|
+
if (metrics) {
|
|
72
|
+
metrics.size = metrics.size > 0 ? metrics.size - 1 : 0;
|
|
73
|
+
metrics.lastRemove = Date.now();
|
|
74
|
+
metrics.warnings = 0;
|
|
75
|
+
this.outputQueueMetrics.set(id, metrics);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
22
78
|
this.subscribe = (streamId, observer) => this.getOutputStream(streamId).subscribe(observer);
|
|
23
79
|
this.emit = (event) => {
|
|
24
|
-
var _a;
|
|
25
80
|
if (event) {
|
|
26
81
|
try {
|
|
27
82
|
this.publishEvent(event);
|
|
28
|
-
this.
|
|
83
|
+
if (this.outputStreamMap.has(event.getStreamId())) {
|
|
84
|
+
this.getOutputStream(event.getStreamId()).next(event);
|
|
85
|
+
}
|
|
29
86
|
}
|
|
30
87
|
catch (err) {
|
|
31
|
-
|
|
88
|
+
this.logger.error(err);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
this.emitPartial = (completeEvent, partialEvent) => {
|
|
93
|
+
try {
|
|
94
|
+
if (completeEvent && this.outputStreamMap.has(completeEvent.getStreamId())) {
|
|
95
|
+
this.getOutputStream(completeEvent.getStreamId()).next(completeEvent);
|
|
96
|
+
}
|
|
97
|
+
if (partialEvent) {
|
|
98
|
+
this.publishEvent(partialEvent);
|
|
32
99
|
}
|
|
33
100
|
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
this.logger.error(err);
|
|
103
|
+
}
|
|
34
104
|
};
|
|
35
|
-
this.onMessage = async (
|
|
36
|
-
|
|
105
|
+
this.onMessage = async (msg) => {
|
|
106
|
+
let event;
|
|
107
|
+
try {
|
|
108
|
+
event = JSON.parse(msg.content.toString());
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
this.logger.error(err);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
37
114
|
if (event.type === 'com.flowstudio.deployment.update') {
|
|
38
115
|
try {
|
|
39
116
|
const flow = event.data;
|
|
40
117
|
if (!flow) {
|
|
41
|
-
|
|
118
|
+
this.amqpChannel.nack(msg, false, false);
|
|
119
|
+
return;
|
|
42
120
|
}
|
|
43
121
|
let context = {};
|
|
44
122
|
if (flow.context) {
|
|
45
|
-
this.context =
|
|
123
|
+
this.context = { ...this.context, ...flow.context };
|
|
46
124
|
context = this.context;
|
|
47
125
|
}
|
|
48
126
|
if (flow.properties) {
|
|
49
127
|
this.properties = flow.properties;
|
|
50
128
|
for (const element of Object.values(this.elements)) {
|
|
51
|
-
|
|
129
|
+
element.onFlowPropertiesChanged?.(flow.properties);
|
|
52
130
|
}
|
|
53
131
|
}
|
|
54
132
|
if (Object.keys(context).length > 0) {
|
|
55
133
|
for (const element of flow.elements || []) {
|
|
56
|
-
context =
|
|
57
|
-
|
|
134
|
+
context = { ...context, name: element.name };
|
|
135
|
+
this.elements?.[element.id]?.onContextChanged(context);
|
|
58
136
|
}
|
|
59
137
|
}
|
|
60
138
|
for (const element of flow.elements || []) {
|
|
61
|
-
|
|
139
|
+
this.elements?.[element.id]?.onPropertiesChanged(element.properties);
|
|
62
140
|
}
|
|
63
141
|
const statusEvent = {
|
|
64
|
-
eventId:
|
|
142
|
+
eventId: (0, crypto_1.randomUUID)(),
|
|
65
143
|
eventTime: new Date().toISOString(),
|
|
66
144
|
eventType: 'com.hahnpro.event.health',
|
|
67
145
|
contentType: 'application/json',
|
|
68
146
|
data: { deploymentId: this.context.deploymentId, status: 'updated' },
|
|
69
147
|
};
|
|
70
|
-
|
|
148
|
+
try {
|
|
149
|
+
this.amqpChannel?.publish('deployment', 'health', statusEvent);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
this.logger.error(err);
|
|
153
|
+
}
|
|
71
154
|
}
|
|
72
155
|
catch (err) {
|
|
73
156
|
this.logger.error(err);
|
|
74
157
|
const statusEvent = {
|
|
75
|
-
eventId:
|
|
158
|
+
eventId: (0, crypto_1.randomUUID)(),
|
|
76
159
|
eventTime: new Date().toISOString(),
|
|
77
160
|
eventType: 'com.hahnpro.event.health',
|
|
78
161
|
contentType: 'application/json',
|
|
79
162
|
data: { deploymentId: this.context.deploymentId, status: 'updating failed' },
|
|
80
163
|
};
|
|
81
|
-
|
|
164
|
+
try {
|
|
165
|
+
this.amqpChannel?.publish('deployment', 'health', statusEvent);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
this.logger.error(e);
|
|
169
|
+
}
|
|
82
170
|
}
|
|
83
171
|
}
|
|
84
172
|
else if (event.type === 'com.flowstudio.deployment.message') {
|
|
85
173
|
const data = event.data;
|
|
86
|
-
const elementId = data
|
|
174
|
+
const elementId = data?.elementId;
|
|
87
175
|
if (elementId) {
|
|
88
|
-
|
|
176
|
+
this.elements?.[elementId]?.onMessage?.(data);
|
|
89
177
|
}
|
|
90
178
|
else {
|
|
91
179
|
for (const element of Object.values(this.elements)) {
|
|
92
|
-
|
|
180
|
+
element?.onMessage?.(data);
|
|
93
181
|
}
|
|
94
182
|
}
|
|
95
183
|
}
|
|
@@ -97,131 +185,224 @@ class FlowApplication {
|
|
|
97
185
|
this.destroy();
|
|
98
186
|
}
|
|
99
187
|
else {
|
|
100
|
-
|
|
188
|
+
this.amqpChannel.nack(msg, false, false);
|
|
101
189
|
}
|
|
102
|
-
return undefined;
|
|
103
190
|
};
|
|
104
191
|
this.publishEvent = (event) => {
|
|
105
|
-
if (!this.
|
|
192
|
+
if (!this.amqpChannel) {
|
|
106
193
|
return;
|
|
107
194
|
}
|
|
108
195
|
try {
|
|
109
196
|
const message = event.format();
|
|
110
|
-
if (object_sizeof_1.default(message) > MAX_EVENT_SIZE_BYTES) {
|
|
111
|
-
message.data =
|
|
197
|
+
if ((0, object_sizeof_1.default)(message) > MAX_EVENT_SIZE_BYTES) {
|
|
198
|
+
message.data = (0, utils_1.truncate)(message.data);
|
|
112
199
|
}
|
|
113
|
-
return this.
|
|
200
|
+
return this.amqpChannel.publish('flowlogs', '', message);
|
|
114
201
|
}
|
|
115
202
|
catch (err) {
|
|
116
203
|
this.logger.error(err);
|
|
117
204
|
}
|
|
118
205
|
};
|
|
206
|
+
if (baseLoggerOrConfig && !baseLoggerOrConfig.log) {
|
|
207
|
+
const config = baseLoggerOrConfig;
|
|
208
|
+
this.baseLogger = config.logger;
|
|
209
|
+
this.amqpConnection = config.amqpConnection || (0, amqp_1.createAmqpConnection)(config.amqpConfig);
|
|
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.skipApi = skipApi || false;
|
|
219
|
+
explicitInit = explicitInit || false;
|
|
220
|
+
this._api = mockApi || null;
|
|
221
|
+
}
|
|
222
|
+
this.logger = new FlowLogger_1.FlowLogger({ id: 'none', functionFqn: 'FlowApplication', ...flow?.context }, this.baseLogger || undefined, this.publishEvent);
|
|
223
|
+
process.once('uncaughtException', (err) => {
|
|
224
|
+
this.logger.error('Uncaught exception!');
|
|
225
|
+
this.logger.error(err);
|
|
226
|
+
this.destroy(1);
|
|
227
|
+
});
|
|
228
|
+
process.on('unhandledRejection', (reason) => {
|
|
229
|
+
this.logger.error('Unhandled promise rejection!');
|
|
230
|
+
this.logger.error(reason);
|
|
231
|
+
});
|
|
119
232
|
process.on('SIGTERM', () => {
|
|
120
|
-
this.logger.log('Flow
|
|
121
|
-
this.destroy(
|
|
233
|
+
this.logger.log('Flow Deployment is terminating');
|
|
234
|
+
this.destroy(0);
|
|
122
235
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
236
|
+
if (explicitInit !== true) {
|
|
237
|
+
this.init();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
get rpcClient() {
|
|
241
|
+
if (!this._rpcClient && this.amqpConnection) {
|
|
242
|
+
this._rpcClient = new RpcClient_1.RpcClient(this.amqpConnection, this.logger);
|
|
243
|
+
}
|
|
244
|
+
return this._rpcClient;
|
|
245
|
+
}
|
|
246
|
+
get api() {
|
|
247
|
+
return this._api;
|
|
248
|
+
}
|
|
249
|
+
async init() {
|
|
250
|
+
if (this.initialized)
|
|
251
|
+
return;
|
|
252
|
+
this.context = { ...this.flow.context };
|
|
253
|
+
this.properties = this.flow.properties || {};
|
|
126
254
|
try {
|
|
127
|
-
if (!skipApi) {
|
|
128
|
-
this.
|
|
255
|
+
if (!this.skipApi && !(this._api instanceof hpc_api_1.MockAPI)) {
|
|
256
|
+
this._api = new hpc_api_1.API(this.apiClient);
|
|
129
257
|
}
|
|
130
258
|
}
|
|
131
259
|
catch (err) {
|
|
132
|
-
this.logger.error(
|
|
260
|
+
this.logger.error(err?.message || err);
|
|
133
261
|
}
|
|
134
|
-
|
|
262
|
+
const logErrorAndExit = async (err) => {
|
|
263
|
+
this.logger.error(new Error(err));
|
|
264
|
+
await this.destroy(1);
|
|
265
|
+
};
|
|
266
|
+
this.amqpChannel = this.amqpConnection?.createChannel({
|
|
267
|
+
json: true,
|
|
268
|
+
setup: async (channel) => {
|
|
269
|
+
try {
|
|
270
|
+
await channel.assertExchange('deployment', 'direct', { durable: true });
|
|
271
|
+
await channel.assertExchange('flowlogs', 'fanout', { durable: true });
|
|
272
|
+
await channel.assertExchange('flow', 'direct', { durable: true });
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
await logErrorAndExit(`Could not assert exchanges: ${e}`);
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const queue = await channel.assertQueue(null, { durable: false, exclusive: true });
|
|
279
|
+
await channel.bindQueue(queue.queue, 'deployment', this.context.deploymentId);
|
|
280
|
+
await channel.consume(queue.queue, (msg) => this.onMessage(msg));
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
await logErrorAndExit(`Could not subscribe to deployment exchange: ${err}`);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
this.amqpChannel && (await this.amqpChannel.waitForConnect());
|
|
288
|
+
for (const module of this.modules) {
|
|
135
289
|
const moduleName = Reflect.getMetadata('module:name', module);
|
|
136
290
|
const moduleDeclarations = Reflect.getMetadata('module:declarations', module);
|
|
137
291
|
if (!moduleName || !moduleDeclarations || !Array.isArray(moduleDeclarations)) {
|
|
138
|
-
|
|
292
|
+
await logErrorAndExit(`FlowModule (${module.name}) metadata is missing or invalid`);
|
|
293
|
+
return;
|
|
139
294
|
}
|
|
140
295
|
for (const declaration of moduleDeclarations) {
|
|
141
296
|
const functionFqn = Reflect.getMetadata('element:functionFqn', declaration);
|
|
142
297
|
if (!functionFqn) {
|
|
143
|
-
|
|
298
|
+
await logErrorAndExit(`FlowFunction (${declaration.name}) metadata is missing or invalid`);
|
|
299
|
+
return;
|
|
144
300
|
}
|
|
145
301
|
this.declarations[`${moduleName}.${functionFqn}`] = declaration;
|
|
146
302
|
}
|
|
147
303
|
}
|
|
148
|
-
for (const element of flow.elements) {
|
|
304
|
+
for (const element of this.flow.elements) {
|
|
149
305
|
const { id, name, properties, module, functionFqn } = element;
|
|
150
306
|
try {
|
|
151
|
-
const context =
|
|
307
|
+
const context = { ...this.context, id, name, logger: this.baseLogger, app: this };
|
|
152
308
|
this.elements[id] = new this.declarations[`${module}.${functionFqn}`](context, properties);
|
|
153
309
|
}
|
|
154
310
|
catch (err) {
|
|
155
|
-
|
|
156
|
-
|
|
311
|
+
await logErrorAndExit(`Could not create FlowElement for ${module}.${functionFqn}`);
|
|
312
|
+
return;
|
|
157
313
|
}
|
|
158
314
|
}
|
|
159
|
-
for (const connection of flow.connections) {
|
|
315
|
+
for (const connection of this.flow.connections) {
|
|
160
316
|
const { source, target, sourceStream = 'default', targetStream = 'default' } = connection;
|
|
161
317
|
if (!source || !target) {
|
|
162
318
|
continue;
|
|
163
319
|
}
|
|
164
|
-
const
|
|
320
|
+
const sourceStreamId = `${source}.${sourceStream}`;
|
|
321
|
+
const targetStreamId = `${target}.${targetStream}`;
|
|
165
322
|
const element = this.elements[target];
|
|
166
323
|
if (!element || !element.constructor) {
|
|
167
|
-
|
|
324
|
+
await logErrorAndExit(`${target} has not been initialized`);
|
|
325
|
+
return;
|
|
168
326
|
}
|
|
169
327
|
const streamHandler = Reflect.getMetadata(`stream:${targetStream}`, element.constructor);
|
|
170
328
|
if (!streamHandler || !element[streamHandler]) {
|
|
171
|
-
|
|
329
|
+
await logErrorAndExit(`${target} does not implement a handler for ${targetStream}`);
|
|
330
|
+
return;
|
|
172
331
|
}
|
|
173
332
|
const streamOptions = Reflect.getMetadata(`stream:options:${targetStream}`, element.constructor) || {};
|
|
174
333
|
const concurrent = streamOptions.concurrent || 1;
|
|
175
|
-
const outputStream = this.getOutputStream(
|
|
334
|
+
const outputStream = this.getOutputStream(sourceStreamId);
|
|
176
335
|
outputStream
|
|
177
|
-
.pipe(operators_1.
|
|
178
|
-
|
|
179
|
-
|
|
336
|
+
.pipe((0, operators_1.tap)(() => this.setQueueMetrics(targetStreamId)), (0, operators_1.mergeMap)(async (event) => {
|
|
337
|
+
const eventId = event.getId();
|
|
338
|
+
this.publishLifecycleEvent(element, eventId, flow_interface_1.LifecycleEvent.ACTIVATED);
|
|
339
|
+
this.performanceMap.set(eventId, perf_hooks_1.performance.eventLoopUtilization());
|
|
340
|
+
const start = perf_hooks_1.performance.now();
|
|
341
|
+
try {
|
|
342
|
+
await element[streamHandler](event);
|
|
343
|
+
const duration = Math.ceil(perf_hooks_1.performance.now() - start);
|
|
344
|
+
this.publishLifecycleEvent(element, eventId, flow_interface_1.LifecycleEvent.COMPLETED, { duration });
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
const duration = Math.ceil(perf_hooks_1.performance.now() - start);
|
|
348
|
+
this.publishLifecycleEvent(element, eventId, flow_interface_1.LifecycleEvent.TERMINATED, { duration });
|
|
349
|
+
try {
|
|
350
|
+
element.handleApiError(err);
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
this.logger.error(err);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return event;
|
|
357
|
+
}, concurrent), (0, operators_1.tap)((event) => {
|
|
358
|
+
this.updateMetrics(targetStreamId);
|
|
359
|
+
let elu = this.performanceMap.get(event.getId());
|
|
360
|
+
if (elu) {
|
|
361
|
+
this.performanceMap.delete(event.getId());
|
|
362
|
+
elu = perf_hooks_1.performance.eventLoopUtilization(elu);
|
|
363
|
+
if (elu.utilization > 0.75 && elu.active > 2000) {
|
|
364
|
+
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.`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
180
367
|
}))
|
|
181
368
|
.subscribe();
|
|
182
369
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
exchange: 'deployment',
|
|
186
|
-
routingKey: this.context.deploymentId,
|
|
187
|
-
queueOptions: { durable: false, exclusive: true },
|
|
188
|
-
});
|
|
189
|
-
}
|
|
370
|
+
this.initialized = true;
|
|
371
|
+
this.logger.log('Flow Deployment is running');
|
|
190
372
|
}
|
|
191
373
|
getProperties() {
|
|
192
374
|
return this.properties;
|
|
193
375
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
376
|
+
async destroy(exitCode = 0) {
|
|
377
|
+
try {
|
|
378
|
+
try {
|
|
379
|
+
for (const element of Object.values(this.elements)) {
|
|
380
|
+
element?.onDestroy?.();
|
|
381
|
+
}
|
|
382
|
+
this._rpcClient && (await this._rpcClient.close());
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
this.logger.error(err);
|
|
386
|
+
}
|
|
387
|
+
await (0, utils_1.delay)(250);
|
|
388
|
+
this.amqpConnection && (await this.amqpConnection.close());
|
|
198
389
|
}
|
|
199
|
-
|
|
200
|
-
|
|
390
|
+
catch (err) {
|
|
391
|
+
console.error(err);
|
|
201
392
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for (const element of Object.values(this.elements)) {
|
|
207
|
-
(_a = element === null || element === void 0 ? void 0 : element.onDestroy) === null || _a === void 0 ? void 0 : _a.call(element);
|
|
393
|
+
finally {
|
|
394
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
395
|
+
process.exit(exitCode);
|
|
396
|
+
}
|
|
208
397
|
}
|
|
209
|
-
await ((_b = this._rpcClient) === null || _b === void 0 ? void 0 : _b.close());
|
|
210
398
|
}
|
|
211
399
|
getOutputStream(id) {
|
|
212
|
-
const stream = this.outputStreamMap
|
|
400
|
+
const stream = this.outputStreamMap.get(id);
|
|
213
401
|
if (!stream) {
|
|
214
|
-
this.outputStreamMap
|
|
215
|
-
return this.outputStreamMap
|
|
402
|
+
this.outputStreamMap.set(id, new rxjs_1.Subject());
|
|
403
|
+
return this.outputStreamMap.get(id);
|
|
216
404
|
}
|
|
217
405
|
return stream;
|
|
218
406
|
}
|
|
219
|
-
truncate(msg) {
|
|
220
|
-
let truncated = util_1.inspect(msg, { depth: 4, maxStringLength: 1000 });
|
|
221
|
-
if (truncated.startsWith("'") && truncated.endsWith("'")) {
|
|
222
|
-
truncated = truncated.substring(1, truncated.length - 1);
|
|
223
|
-
}
|
|
224
|
-
return truncated;
|
|
225
|
-
}
|
|
226
407
|
}
|
|
227
408
|
exports.FlowApplication = FlowApplication;
|
package/dist/FlowElement.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PythonShell } from 'python-shell';
|
|
2
|
-
import { API } from '
|
|
3
|
-
import { ClassType,
|
|
2
|
+
import { API } from '@hahnpro/hpc-api';
|
|
3
|
+
import { ClassType, DeploymentMessage, FlowContext, FlowElementContext } from './flow.interface';
|
|
4
4
|
import { Context } from './FlowApplication';
|
|
5
5
|
import { FlowEvent } from './FlowEvent';
|
|
6
6
|
import { FlowLogger } from './FlowLogger';
|
|
@@ -14,6 +14,7 @@ export declare abstract class FlowElement<T = any> {
|
|
|
14
14
|
protected properties: T;
|
|
15
15
|
private readonly app?;
|
|
16
16
|
private readonly rpcRoutingKey;
|
|
17
|
+
private stopPropagateStream;
|
|
17
18
|
constructor({ app, logger, ...metadata }: Context, properties?: unknown, propertiesClassType?: ClassType<T>, whitelist?: boolean);
|
|
18
19
|
get flowProperties(): Record<string, any>;
|
|
19
20
|
onDestroy?: () => void;
|
|
@@ -21,17 +22,24 @@ export declare abstract class FlowElement<T = any> {
|
|
|
21
22
|
onFlowPropertiesChanged?: (properties: Record<string, any>) => void;
|
|
22
23
|
onContextChanged: (context: Partial<FlowContext>) => void;
|
|
23
24
|
onPropertiesChanged: (properties: T) => void;
|
|
25
|
+
getMetadata: () => FlowElementContext;
|
|
24
26
|
protected setProperties: (properties: T) => void;
|
|
25
27
|
handleApiError: (error: any) => void;
|
|
28
|
+
/**
|
|
29
|
+
* @deprecated since version 4.8.0, will be removed in 5.0.0, use emitEvent(...) instead
|
|
30
|
+
*/
|
|
26
31
|
protected emitOutput(data?: any, outputId?: string, time?: Date): FlowEvent;
|
|
32
|
+
protected emitEvent(data: Record<string, any>, inputEvent: FlowEvent, outputId?: string, time?: Date): FlowEvent;
|
|
27
33
|
protected validateProperties<P>(classType: ClassType<P>, properties?: any, whitelist?: boolean): P;
|
|
34
|
+
protected logValidationErrors(error: any, parent?: string): void;
|
|
28
35
|
protected validateEventData<E>(classType: ClassType<E>, event: FlowEvent, whitelist?: boolean): E;
|
|
29
|
-
protected interpolate(
|
|
36
|
+
protected interpolate: (value: any, ...templateVariables: any) => any;
|
|
30
37
|
protected callRpcFunction(functionName: string, ...args: any[]): Promise<unknown>;
|
|
31
|
-
protected runPyRpcScript(scriptPath: string): PythonShell;
|
|
38
|
+
protected runPyRpcScript(scriptPath: string, ...args: (string | boolean | number)[]): PythonShell;
|
|
32
39
|
}
|
|
33
40
|
export declare function InputStream(id?: string, options?: {
|
|
34
41
|
concurrent?: number;
|
|
42
|
+
stopPropagation?: boolean;
|
|
35
43
|
}): MethodDecorator;
|
|
36
44
|
export declare function FlowFunction(fqn: string): ClassDecorator;
|
|
37
45
|
export declare abstract class FlowResource<T = any> extends FlowElement<T> {
|