@itentialopensource/adapter-kafkav2 0.3.12

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/adapter.js ADDED
@@ -0,0 +1,814 @@
1
+ /* @copyright Itential, LLC 2019 (pre-modifications) */
2
+
3
+ // Set globals
4
+ /* global eventSystem log */
5
+ /* eslint no-underscore-dangle: warn */
6
+ /* eslint no-loop-func: warn */
7
+ /* eslint no-cond-assign: warn */
8
+ /* eslint no-unused-vars: warn */
9
+ /* eslint consistent-return: warn */
10
+ /* eslint import/no-dynamic-require: warn */
11
+ /* eslint global-require: warn */
12
+ /* eslint no-await-in-loop: warn */
13
+
14
+ /* Required libraries. */
15
+ const fs = require('fs-extra');
16
+ const path = require('path');
17
+ const util = require('util');
18
+ /* Fetch in the other needed components for the this Adaptor */
19
+ const EventEmitterCl = require('events').EventEmitter;
20
+ const request = require('request');
21
+
22
+ function consoleLoggerProvider(name) {
23
+ return {
24
+ debug: log.debug,
25
+ info: log.info,
26
+ warn: log.warn,
27
+ error: log.error
28
+ };
29
+ }
30
+
31
+ const { Kafka, logLevel } = require('kafkajs');
32
+
33
+ const pjson = require(path.resolve(__dirname, 'pronghorn.json'));
34
+
35
+ let myid = null;
36
+ let errors = [];
37
+ let firstRun = false;
38
+ let firstDone = false;
39
+ const mytopics = Object.keys(pjson.topics);
40
+ const pronghornFile = path.join(__dirname, '/pronghorn.json');
41
+
42
+ async function fetchSchema(registryUrl, topic, version) {
43
+ return new Promise((resolve, reject) => {
44
+ log.debug(`${registryUrl}/subjects/${topic}/versions/${version}`);
45
+ try {
46
+ request(
47
+ `${registryUrl}/subjects/${topic}/versions/${version}`,
48
+ (err, res, body) => {
49
+ if (err) {
50
+ log.debug('schema fetch - evaluating error');
51
+ try {
52
+ const error = JSON.parse(err);
53
+ return reject(
54
+ new Error(
55
+ `Schema registry error: ${error.error_code} - ${error.message}`
56
+ )
57
+ );
58
+ } catch (ex) {
59
+ return reject(
60
+ new Error(
61
+ `Schema registry error: ${err}`
62
+ )
63
+ );
64
+ }
65
+ }
66
+ if (!res || !res.statusCode || res.statusCode !== 200) {
67
+ log.debug('schema fetch - Evaluating error response');
68
+ return reject(
69
+ new Error(
70
+ 'Schema registry error: Did not receive a valid status code'
71
+ )
72
+ );
73
+ }
74
+ // this is the schema response
75
+ log.debug('returning schema response');
76
+ return resolve(JSON.parse(body));
77
+ }
78
+ );
79
+ } catch (e) {
80
+ log.error(`error sending request: ${e} \n${e.stack}`);
81
+ return reject(e);
82
+ }
83
+ });
84
+ }
85
+
86
+ /**
87
+ * @summary Build a standard error object from the data provided
88
+ *
89
+ * @function formatErrorObject
90
+ * @param {String} origin - the originator of the error (optional).
91
+ * @param {String} type - the internal error type (optional).
92
+ * @param {String} variables - the variables to put into the error message (optional).
93
+ * @param {Integer} sysCode - the error code from the other system (optional).
94
+ * @param {Object} sysRes - the raw response from the other system (optional).
95
+ * @param {Exception} stack - any available stack trace from the issue (optional).
96
+ *
97
+ * @return {Object} - the error object, null if missing pertinent information
98
+ */
99
+ function formatErrorObject(origin, type, variables, sysCode, sysRes, stack) {
100
+ log.trace(`${myid}-adapter-formatErrorObject`);
101
+
102
+ // add the required fields
103
+ const errorObject = {
104
+ icode: 'AD.999',
105
+ IAPerror: {
106
+ origin: `${myid}-unidentified`,
107
+ displayString: 'error not provided',
108
+ recommendation: 'report this issue to the adapter team!'
109
+ }
110
+ };
111
+
112
+ if (origin) {
113
+ errorObject.IAPerror.origin = origin;
114
+ }
115
+ if (type) {
116
+ errorObject.IAPerror.displayString = type;
117
+ }
118
+
119
+ // add the messages from the error.json
120
+ for (let e = 0; e < errors.length; e += 1) {
121
+ if (errors[e].key === type) {
122
+ errorObject.icode = errors[e].icode;
123
+ errorObject.IAPerror.displayString = errors[e].displayString;
124
+ errorObject.IAPerror.recommendation = errors[e].recommendation;
125
+ } else if (errors[e].icode === type) {
126
+ errorObject.icode = errors[e].icode;
127
+ errorObject.IAPerror.displayString = errors[e].displayString;
128
+ errorObject.IAPerror.recommendation = errors[e].recommendation;
129
+ }
130
+ }
131
+
132
+ // replace the variables
133
+ let varCnt = 0;
134
+ while (errorObject.IAPerror.displayString.indexOf('$VARIABLE$') >= 0) {
135
+ let curVar = '';
136
+
137
+ // get the current variable
138
+ if (variables && Array.isArray(variables) && variables.length >= varCnt + 1) {
139
+ curVar = variables[varCnt];
140
+ }
141
+ varCnt += 1;
142
+ errorObject.IAPerror.displayString = errorObject.IAPerror.displayString.replace('$VARIABLE$', curVar);
143
+ }
144
+
145
+ // add all of the optional fields
146
+ if (sysCode) {
147
+ errorObject.IAPerror.code = sysCode;
148
+ }
149
+ if (sysRes) {
150
+ errorObject.IAPerror.raw_response = sysRes;
151
+ }
152
+ if (stack) {
153
+ errorObject.IAPerror.stack = stack;
154
+ }
155
+
156
+ // return the object
157
+ return errorObject;
158
+ }
159
+
160
+ /**
161
+ * This is the adapter/interface into Kafka
162
+ */
163
+ class Kafkav2 extends EventEmitterCl {
164
+ /**
165
+ * Kafka Adapter
166
+ * @constructor
167
+ */
168
+ constructor(prongid, properties) {
169
+ super();
170
+
171
+ this.props = properties;
172
+ this.alive = false;
173
+ this.healthy = false;
174
+ this.id = prongid;
175
+ myid = prongid;
176
+
177
+ // put topics from file into memory
178
+ this.topicsEvents = [];
179
+ const topicsFile = path.join(__dirname, '/.topics.json');
180
+
181
+ // if the file does not exist - error
182
+ if (fs.existsSync(topicsFile)) {
183
+ this.topicsEvents = JSON.parse(fs.readFileSync(topicsFile, 'utf-8'));
184
+ }
185
+
186
+ // get the topics into the right format --- fix older .topics.json files so they have the right format
187
+ if (!Array.isArray(this.topicsEvents)) {
188
+ // original format
189
+ const keys = Object.keys(this.topicsEvents);
190
+ const tempTops = [];
191
+ keys.forEach((item) => {
192
+ tempTops.push({
193
+ topic: item,
194
+ partition: this.topicsEvents[item].partitions || this.topicsEvents[item].partition || 0,
195
+ offset: this.topicsEvents[item].offset || 0,
196
+ processed: this.topicsEvents[item].offset || 0,
197
+ subscribers: this.topicsEvents[item].subscribers || 0,
198
+ avro: this.topicsEvents[item].avro || 'NO',
199
+ subInfo: [
200
+ {
201
+ subname: 'default',
202
+ filters: [],
203
+ rabbit: 'kafka',
204
+ throttle: {}
205
+ }
206
+ ]
207
+ });
208
+ });
209
+ this.topicsEvents = tempTops;
210
+ } else {
211
+ for (let t = 0; t < this.topicsEvents.length; t += 1) {
212
+ if (!Object.hasOwnProperty.call(this.topicsEvents[t], 'processed')) {
213
+ this.topicsEvents[t].processed = this.topicsEvents[t].offset || 0;
214
+ }
215
+ if (!Object.hasOwnProperty.call(this.topicsEvents[t], 'subInfo')) {
216
+ this.topicsEvents[t].subInfo = [
217
+ {
218
+ subname: 'default',
219
+ filters: [],
220
+ rabbit: 'kafka',
221
+ throttle: {}
222
+ }
223
+ ];
224
+ }
225
+ }
226
+ }
227
+
228
+ // If we have a rabbit queue - need to add that to topics.json
229
+ // if there are topics in the properties
230
+ if (this.props && this.props.topics) {
231
+ let needRestart = false;
232
+ for (let t = 0; t < this.props.topics.length; t += 1) {
233
+ if (typeof this.props.topics[t] === 'string') {
234
+ const topName = this.props.topics[t];
235
+ if (!mytopics.includes(topName)) {
236
+ pjson.topics[this.props.topics[t]] = {};
237
+ needRestart = true;
238
+ }
239
+ for (let te = 0; te < this.topicsEvents.length; te += 1) {
240
+ // only set the rabbit in the topic if it is the default (kafka)
241
+ if (this.topicsEvents[te].topic === topName && this.topicsEvents[te].partition === 0
242
+ && this.topicsEvents[te].subInfo.rabbit === 'kafka') {
243
+ this.topicsEvents[te].subInfo.rabbit = topName;
244
+ break;
245
+ }
246
+ }
247
+ } else if (typeof this.props.topics[t] === 'object') {
248
+ const topName = this.props.topics[t].name;
249
+ // if the topic is not in the pronghorn.json
250
+ if (!mytopics.includes(topName)) {
251
+ pjson.topics[topName] = {};
252
+ needRestart = true;
253
+ }
254
+ const topPart = this.props.topics[t].partition || 0;
255
+ let useAvro = 'NO';
256
+ if (this.props.topics[t].avro !== undefined && this.props.topics[t].avro !== null && this.props.topics[t].avro === true) {
257
+ useAvro = 'YES';
258
+ }
259
+ let subInfo = [
260
+ {
261
+ subname: 'default',
262
+ filters: [],
263
+ rabbit: 'kafka',
264
+ throttle: {}
265
+ }
266
+ ];
267
+ if (this.props.topics[t].subscriberInfo) {
268
+ subInfo = this.props.topics[t].subscriberInfo;
269
+ for (let r = 0; r < subInfo.length; r += 1) {
270
+ // if the rabbit topic is not in the pronghorn.json
271
+ if (!mytopics.includes(subInfo[r].rabbit)) {
272
+ pjson.topics[subInfo[r].rabbit] = {};
273
+ needRestart = true;
274
+ }
275
+ }
276
+ }
277
+ let found = false;
278
+ for (let te = 0; te < this.topicsEvents.length; te += 1) {
279
+ // see if we find the topic and partition
280
+ if (this.topicsEvents[te].topic === topName && this.topicsEvents[te].partition === topPart) {
281
+ found = true;
282
+ // only set the rabbit in the topic if it is the default (kafka)
283
+ if (this.topicsEvents[te].subInfo.rabbit === 'kafka') {
284
+ this.topicsEvents[te].subInfo.rabbit = topName;
285
+ }
286
+ // set the subscribers if always
287
+ if (this.props.topics[t].always) {
288
+ this.topicsEvents[te].subscribers = 99999;
289
+ }
290
+ // set the avro accordingly
291
+ this.topicsEvents[te].avro = useAvro;
292
+
293
+ // check to see if we need to add a subscriber
294
+ for (let sub = 0; sub < subInfo.length; sub += 1) {
295
+ let subFound = false;
296
+ for (let s = 0; s <= this.topicsEvents[te].subInfo.length; s += 1) {
297
+ // if the subscriber is found, no need to add anything
298
+ if (subInfo[sub].subname === this.topicsEvents[te].subInfo[s].subname) {
299
+ subFound = true;
300
+ // set the filters
301
+ if (subInfo[sub].filters) {
302
+ this.topicsEvents[te].subInfo[s].filters = subInfo[sub].filters;
303
+ }
304
+ // set the rabbit
305
+ if (subInfo[sub].rabbit) {
306
+ this.topicsEvents[te].subInfo[s].rabbit = subInfo[sub].rabbit;
307
+ }
308
+ // set the throttle
309
+ if (subInfo[sub].throttle) {
310
+ this.topicsEvents[te].subInfo[s].throttle = subInfo[sub].throttle;
311
+ }
312
+ break;
313
+ }
314
+ }
315
+ if (!subFound) {
316
+ // add the subscriber if it was not found
317
+ this.topicsEvents[te].subInfo.push(subInfo[sub]);
318
+ }
319
+ }
320
+ break;
321
+ }
322
+ }
323
+
324
+ // if the topic was not found but should always be subscribed to, add it
325
+ if (!found && this.props.topics[t].always) {
326
+ this.topicsEvents.push({
327
+ topic: topName,
328
+ partition: topPart,
329
+ offset: 0,
330
+ processed: 0,
331
+ subscribers: 99999,
332
+ avro: useAvro,
333
+ subInfo
334
+ });
335
+ }
336
+ }
337
+ }
338
+
339
+ // if we need to restart the adapter due to changes to pronghorn.json
340
+ if (needRestart) {
341
+ fs.writeFileSync(pronghornFile, JSON.stringify(pjson, null, 2));
342
+ log.error('NEED TO RESTART ADAPTER - EXITING');
343
+ const errorObj = {
344
+ origin: `${this.id}-adapter-constructor`,
345
+ type: 'Restarting to add new topics',
346
+ vars: []
347
+ };
348
+ // log and throw the error
349
+ log.error(`${errorObj.origin}: ${errorObj.displayString}`);
350
+ setTimeout(() => {
351
+ throw new Error(JSON.stringify(errorObj));
352
+ }, 1000);
353
+ }
354
+ }
355
+
356
+ this.KafkaClient = null;
357
+ this.producer = null;
358
+ this.consumer = null;
359
+ this.registryUrl = null;
360
+ this.registry = 'abc';
361
+ if (this.props && this.props.registry_url) {
362
+ this.registryUrl = this.props.registry_url;
363
+ this.registry = require('avro-schema-registry')(this.props.registry_url);
364
+ }
365
+
366
+ // get the path for the specific error file
367
+ const errorFile = path.join(__dirname, '/error.json');
368
+
369
+ // if the file does not exist - error
370
+ if (!fs.existsSync(errorFile)) {
371
+ const origin = `${this.id}-adapter-constructor`;
372
+ log.warn(`${origin}: Could not locate ${errorFile} - errors will be missing details`);
373
+ }
374
+
375
+ // Read the action from the file system
376
+ const errorData = JSON.parse(fs.readFileSync(errorFile, 'utf-8'));
377
+ ({ errors } = errorData);
378
+
379
+ // rewrite the topics file for persistence
380
+ if (this.props && this.props.stub === false) {
381
+ const intTime = this.props.interval_time || 30000;
382
+ setInterval(() => {
383
+ try {
384
+ log.debug('Update .topics.json file');
385
+ fs.writeFileSync(topicsFile, JSON.stringify(this.topicsEvents, null, 2));
386
+ } catch (err) {
387
+ log.error(err);
388
+ }
389
+ }, intTime);
390
+ }
391
+ }
392
+
393
+ /**
394
+ * @summary Connect function is used during Pronghorn startup to provide instantiation feedback.
395
+ *
396
+ * @function connect
397
+ */
398
+ connect() {
399
+ const meth = 'adapter-connect';
400
+ const origin = `${this.id}-${meth}`;
401
+ log.trace(origin);
402
+
403
+ // initially set as off
404
+ this.emit('OFFLINE', { id: this.id });
405
+ this.alive = true;
406
+
407
+ try {
408
+ // Create a kafka client
409
+ const combinedProps = this.props.client || {};
410
+ if (this.props.client.ssl.ca) {
411
+ combinedProps.ssl.ca = [fs.readFileSync(this.props.client.ssl.ca, 'utf-8')];
412
+ }
413
+ if (this.props.client.ssl.cert) {
414
+ combinedProps.ssl.cert = fs.readFileSync(this.props.client.ssl.cert, 'utf-8');
415
+ }
416
+ if (this.props.client.ssl.key) {
417
+ combinedProps.ssl.key = fs.readFileSync(this.props.client.ssl.key, 'utf-8');
418
+ }
419
+
420
+ this.KafkaClient = new Kafka(combinedProps);
421
+
422
+ this.producer = this.KafkaClient.producer(this.props.producer || {});
423
+
424
+ this.consumer = this.KafkaClient.consumer(this.props.consumer || {});
425
+
426
+ const isPassingFiltering = (topic, message) => {
427
+ const topicConfig = this.topicsEvents.find((e) => e.topic === topic);
428
+
429
+ if (!topicConfig) return true;
430
+
431
+ if (!topicConfig.subInfo || !Array.isArray(topicConfig.subInfo)) return true;
432
+
433
+ const allFilters = topicConfig.subInfo.filter((e) => Array.isArray(e.filters) && e.filters.length > 0);
434
+ if (allFilters.length === 0) return true;
435
+ const matched = allFilters.find((s) => {
436
+ for (let f = 0; f < s.filters.length; f += 1) {
437
+ const re = new RegExp(s.filters[f]);
438
+ if (message.value.toString().match(re)) {
439
+ log.debug('Matched filter: ', re);
440
+ return true;
441
+ }
442
+ }
443
+ return false;
444
+ });
445
+ return matched;
446
+ };
447
+
448
+ const run = async () => {
449
+ this.emit('ONLINE', {
450
+ id: this.id
451
+ });
452
+ log.info('EMITTED ONLINE');
453
+ // Start consuming
454
+ // Iterate through all topics in topicsEvents
455
+ const topics = [];
456
+ for (let t = 0; t < this.topicsEvents.length; t += 1) {
457
+ topics.push(this.topicsEvents[t].topic);
458
+ }
459
+ await this.consumer.connect();
460
+ await this.consumer.subscribe({ topics, fromBeginning: true });
461
+ await this.consumer.run({
462
+ eachMessage: async ({ topic, partition, message }) => {
463
+ log.info(`PROCESSING NEW MESSAGE ON TOPIC: ${topic} PARTITION: ${partition} WITH OFFSET: ${message.offset}`);
464
+ const newMsg = message;
465
+ let avroUsed = false;
466
+ let desiredTopic = 'kafka';
467
+
468
+ // find the topic here to change the offset
469
+ for (let t = 0; t < this.topicsEvents.length; t += 1) {
470
+ if (this.topicsEvents[t].topic === topic && this.topicsEvents[t].partition === partition) {
471
+ if (this.topicsEvents[t].offset < message.offset) {
472
+ this.topicsEvents[t].offset = message.offset;
473
+ }
474
+ // determine if we are using AVRO and set flag
475
+ if (this.topicsEvents[t].avro && this.topicsEvents[t].avro.toUpperCase() === 'YES') {
476
+ avroUsed = true;
477
+ }
478
+ // set desired topic
479
+ if (this.topicsEvents[t].subInfo && this.topicsEvents[t].subInfo.rabbit) {
480
+ desiredTopic = this.topicsEvents[t].subInfo.rabbit;
481
+ } else {
482
+ desiredTopic = this.topicsEvents[t].topic;
483
+ }
484
+ break;
485
+ }
486
+ }
487
+
488
+ // if using avro
489
+ if (this.registry && avroUsed) {
490
+ // will give us an interval between 5001 and 60000 milliseconds (5 seconds to 1 minute)
491
+ const fastInt = Math.floor(Math.random() * 55000) + 5001;
492
+
493
+ // This interval is to help prevent the adapter from bombarding the registry by sending requests at random intervals
494
+ const intervalObject = setInterval(async () => {
495
+ // Prevents running the request while the first one is running
496
+ if (!firstRun) {
497
+ if (!firstDone) {
498
+ firstRun = true;
499
+ }
500
+
501
+ try {
502
+ log.debug('GET AVRO KEY');
503
+ newMsg.key = await this.registry.decode(newMsg.key);
504
+ log.debug(`AVRO KEY: ${newMsg.key}`);
505
+ newMsg.value = await this.registry.decode(newMsg.value);
506
+ log.debug(`AVRO MSG: ${newMsg.value}`);
507
+ firstDone = true;
508
+ firstRun = false;
509
+ clearInterval(intervalObject);
510
+ } catch (ex) {
511
+ log.warn(`Had issue getting registry will try again in ${fastInt} milliseconds`);
512
+ firstRun = false;
513
+ }
514
+ }
515
+ }, fastInt);
516
+ }
517
+
518
+ const goodMsg = isPassingFiltering(topic, newMsg);
519
+ if (goodMsg) {
520
+ log.info(`Message: '${message.value.toString()}' being CONSUMED as it does meet the filter condition`);
521
+ if (mytopics.includes(desiredTopic)) {
522
+ log.info(`Emitting: ${message.value.toString()} TO ${desiredTopic}`);
523
+ } else {
524
+ log.info(`The desired topic ${desiredTopic} does not exist. If it should, make sure it is in pronghorn.json`);
525
+ log.info(`Emitting: ${newMsg.value.toString()} TO kafka`);
526
+ desiredTopic = 'kafka';
527
+ }
528
+ if (this.props.parseMessage) {
529
+ let messageObj;
530
+ const wrapper = this.props.wrapMessage || 'payload';
531
+ try {
532
+ messageObj = { [wrapper]: JSON.parse(newMsg.value) };
533
+ } catch (ex) {
534
+ messageObj = { [wrapper]: newMsg.value.toString() };
535
+ }
536
+ eventSystem.publish(desiredTopic, messageObj);
537
+ } else {
538
+ eventSystem.publish(desiredTopic, newMsg.value.toString());
539
+ }
540
+ } else {
541
+ log.info(`Message: '${newMsg.value.toString()}' being DROPPED as it fails to meet any filter condition`);
542
+ }
543
+ }
544
+ });
545
+ };
546
+ run().catch((err) => console.error(`[consumer] ${err.message}`, err));
547
+
548
+ const errorTypes = ['unhandledRejection', 'uncaughtException'];
549
+ const signalTraps = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
550
+
551
+ errorTypes.forEach((type) => {
552
+ process.on(type, async (e) => {
553
+ try {
554
+ console.log(`process.on ${type}`);
555
+ console.error(e);
556
+ await this.consumer.disconnect();
557
+ process.exit(0);
558
+ } catch (_) {
559
+ process.exit(1);
560
+ }
561
+ });
562
+ });
563
+
564
+ signalTraps.forEach((type) => {
565
+ process.once(type, async () => {
566
+ try {
567
+ await this.consumer.disconnect();
568
+ } finally {
569
+ process.kill(process.pid, type);
570
+ }
571
+ });
572
+ });
573
+ } catch (ex) {
574
+ const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
575
+ log.error(JSON.stringify(ex));
576
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
577
+ return (null, errorObj);
578
+ }
579
+ }
580
+
581
+ /**
582
+ * @summary HealthCheck function is used to provide Pronghorn the status of this adapter.
583
+ *
584
+ * @function healthCheck
585
+ * @param callback - a callback function to return the result id and status
586
+ */
587
+ healthCheck(callback) {
588
+ // need to work on this later
589
+ const retstatus = { id: this.id, status: 'success' };
590
+ return callback(retstatus);
591
+ }
592
+
593
+ /**
594
+ * getAllFunctions is used to get all of the exposed function in the adapter
595
+ *
596
+ * @function getAllFunctions
597
+ */
598
+ getAllFunctions() {
599
+ let myfunctions = [];
600
+ let obj = this;
601
+
602
+ // find the functions in this class
603
+ do {
604
+ const l = Object.getOwnPropertyNames(obj)
605
+ .concat(Object.getOwnPropertySymbols(obj).map((s) => s.toString()))
606
+ .sort()
607
+ .filter((p, i, arr) => typeof obj[p] === 'function' && p !== 'constructor' && (i === 0 || p !== arr[i - 1]) && myfunctions.indexOf(p) === -1);
608
+ myfunctions = myfunctions.concat(l);
609
+ }
610
+ while (
611
+ (obj = Object.getPrototypeOf(obj)) && Object.getPrototypeOf(obj)
612
+ );
613
+
614
+ return myfunctions;
615
+ }
616
+
617
+ /**
618
+ * getWorkflowFunctions is used to get all of the workflow function in the adapter
619
+ *
620
+ * @function getWorkflowFunctions
621
+ */
622
+ getWorkflowFunctions() {
623
+ const myIgnore = [
624
+ 'subscribeAvroWithSubscriber',
625
+ 'subscribeWithSubscriber'
626
+ ];
627
+
628
+ const myfunctions = this.getAllFunctions();
629
+ const wffunctions = [];
630
+
631
+ // remove the functions that should not be in a Workflow
632
+ for (let m = 0; m < myfunctions.length; m += 1) {
633
+ if (myfunctions[m] === 'addListener') {
634
+ // got to the second tier (adapterBase)
635
+ break;
636
+ }
637
+ if (myfunctions[m] !== 'connect' && myfunctions[m] !== 'healthCheck'
638
+ && myfunctions[m] !== 'getAllFunctions' && myfunctions[m] !== 'getWorkflowFunctions') {
639
+ let found = false;
640
+ if (myIgnore && Array.isArray(myIgnore)) {
641
+ for (let i = 0; i < myIgnore.length; i += 1) {
642
+ if (myfunctions[m].toUpperCase() === myIgnore[i].toUpperCase()) {
643
+ found = true;
644
+ }
645
+ }
646
+ }
647
+ if (!found) {
648
+ wffunctions.push(myfunctions[m]);
649
+ }
650
+ }
651
+ }
652
+
653
+ return wffunctions;
654
+ }
655
+
656
+ /**
657
+ * Call to send message.
658
+ * @function send
659
+ * @param payloads - array of ProduceRequest, ProduceRequest is a JSON object (required)
660
+ * @param callback - a callback function to return a result
661
+ */
662
+ sendMessage(payloads, callback) {
663
+ const meth = 'adapter-sendMessage';
664
+ const origin = `${this.id}-${meth}`;
665
+ log.trace(origin);
666
+
667
+ try {
668
+ if (!payloads) {
669
+ const errorObj = formatErrorObject(origin, 'Missing Data', ['payloads'], null, null, null);
670
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
671
+ return callback(null, errorObj);
672
+ }
673
+ if (!Array.isArray(payloads)) {
674
+ const errorObj = formatErrorObject(origin, 'Invalid payloads format - payloads must be an array', null, null, null, null);
675
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
676
+ return callback(null, errorObj);
677
+ }
678
+ if (!this.producer) {
679
+ const errorObj = formatErrorObject(origin, 'Producer not created', null, null, null, null);
680
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
681
+ return callback(null, errorObj);
682
+ }
683
+
684
+ const run = async () => {
685
+ try {
686
+ const topicMessages = payloads;
687
+ // need to go through the payloads that were provided
688
+ for (let p = 0; p < payloads.length; p += 1) {
689
+ if (!payloads[p].topic) {
690
+ const errorObj = formatErrorObject(origin, 'Missing Data', ['payloads[p].topic'], null, null, null);
691
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
692
+ return callback(null, errorObj);
693
+ }
694
+ if (!payloads[p].messages) {
695
+ const errorObj = formatErrorObject(origin, 'Missing Data', ['payloads[p].messages'], null, null, null);
696
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
697
+ return callback(null, errorObj);
698
+ }
699
+ topicMessages[p].messages = payloads[p].messages.map((message) => {
700
+ if (typeof message.value === 'object') {
701
+ return { ...message, value: JSON.stringify(message.value) };
702
+ }
703
+ return message;
704
+ });
705
+ if (this.registry && topicMessages[p].simple && topicMessages[p].simple.toUpperCase() === 'NO') {
706
+ log.debug(`Handling AVRO Message: ${JSON.stringify(topicMessages[p])}`);
707
+ let keyVer = 1;
708
+ let valVer = 1;
709
+ if (topicMessages[p].key_version) {
710
+ keyVer = topicMessages[p].key_version;
711
+ }
712
+ if (topicMessages[p].value_version) {
713
+ valVer = topicMessages[p].value_version;
714
+ }
715
+
716
+ // will give us an interval between 2501 and 10000 milliseconds (2.5 to 10 seconds)
717
+ const fastInt = Math.floor(Math.random() * 7500) + 2501;
718
+ let currValue = '';
719
+ let encodeKey = '';
720
+ const encodeKeys = [];
721
+
722
+ // This interval is to help prevent the adapter from bombarding the registry by sending requests at random intervals
723
+ const intervalObject = setInterval(async () => {
724
+ try {
725
+ // get the key and value for the topic of this message
726
+ log.spam('fetching schema values');
727
+ currValue = await fetchSchema(this.registryUrl, `${topicMessages[p].topic}-value`, valVer);
728
+ for (let m = 0; m < topicMessages[p].messages.length; m += 1) {
729
+ if (topicMessages[p].messages[m].key) {
730
+ let schKey = '';
731
+ log.spam('fetching schema keys');
732
+ schKey = await fetchSchema(this.registryUrl, `${topicMessages[p].topic}-key`, keyVer);
733
+ // encode the key
734
+ encodeKey = await this.registry.encodeKey(topicMessages[p].topic, schKey.schema, topicMessages[p].messages[m].key);
735
+ encodeKeys.push(encodeKey);
736
+ } else {
737
+ encodeKeys.push(null);
738
+ }
739
+ }
740
+
741
+ clearInterval(intervalObject);
742
+ } catch (ex) {
743
+ log.warn(`Had issue getting registry will try again in ${fastInt} milliseconds`);
744
+ firstRun = false;
745
+ }
746
+ }, fastInt);
747
+
748
+ log.debug('Encoding message array');
749
+ for (let m = 0; m < topicMessages[p].messages.length; m += 1) {
750
+ let msg = '';
751
+ log.debug(`Attemping to encode message ${m}`);
752
+ try {
753
+ msg = await this.registry.encodeMessage(topicMessages[p].topic, currValue.schema, topicMessages[p].messages[m].value);
754
+ } catch (encodeErr) {
755
+ log.error(encodeErr.stack);
756
+ return callback(null, encodeErr);
757
+ }
758
+ if (topicMessages[p].messages[m].key) {
759
+ log.debug('Replacing key with encoded key');
760
+ topicMessages[p].messages[m].key = encodeKeys[m];
761
+ }
762
+ log.debug('Replacing message with encoded message');
763
+ topicMessages[p].messages[m].value = msg;
764
+ }
765
+ }
766
+ }
767
+ await this.producer.connect();
768
+ await this.producer.sendBatch({ topicMessages });
769
+ } catch (err) {
770
+ const errorObj = formatErrorObject(origin, 'Producer sending message failed', null, null, null, err);
771
+ log.error('error: '.concat(err));
772
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
773
+ return callback(null, errorObj);
774
+ }
775
+ return callback({
776
+ status: 'success',
777
+ code: 200
778
+ });
779
+ };
780
+ run();
781
+
782
+ const errorTypes = ['unhandledRejection', 'uncaughtException'];
783
+ const signalTraps = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
784
+
785
+ errorTypes.forEach((type) => {
786
+ process.on(type, async () => {
787
+ try {
788
+ console.log(`process.on ${type}`);
789
+ await this.producer.disconnect();
790
+ process.exit(0);
791
+ } catch (_) {
792
+ process.exit(1);
793
+ }
794
+ });
795
+ });
796
+
797
+ signalTraps.forEach((type) => {
798
+ process.once(type, async () => {
799
+ try {
800
+ await this.producer.disconnect();
801
+ } finally {
802
+ process.kill(process.pid, type);
803
+ }
804
+ });
805
+ });
806
+ } catch (ex) {
807
+ const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
808
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
809
+ return callback(null, errorObj);
810
+ }
811
+ }
812
+ }
813
+
814
+ module.exports = Kafkav2;