@itentialopensource/adapter-kafkav2 0.19.0 → 0.21.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/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
1
 
2
+ ## 0.21.0 [01-22-2024]
3
+
4
+ * Minor/topic db dedupe
5
+
6
+ See merge request itentialopensource/adapters/notification-messaging/adapter-kafkav2!16
7
+
8
+ ---
9
+
10
+ ## 0.20.0 [01-22-2024]
11
+
12
+ * Minor/topic db dedupe
13
+
14
+ See merge request itentialopensource/adapters/notification-messaging/adapter-kafkav2!16
15
+
16
+ ---
17
+
2
18
  ## 0.19.0 [01-08-2024]
3
19
 
4
20
  * Migration changes
package/adapter.js CHANGED
@@ -20,6 +20,8 @@ const util = require('util');
20
20
  /* Fetch in the other needed components for the this Adaptor */
21
21
  // const EventEmitterCl = require('events').EventEmitter;
22
22
  const AdapterBaseCl = require(path.join(__dirname, 'adapterBase.js'));
23
+ const DBUtil = require(path.join(__dirname, 'utils', 'dbUtil.js'));
24
+
23
25
  const request = require('request');
24
26
 
25
27
  let needRestart = false;
@@ -195,6 +197,12 @@ class Kafkav2 extends AdapterBaseCl {
195
197
  this.id = prongid;
196
198
  myid = prongid;
197
199
 
200
+ this.dbUtil = new DBUtil(prongid, properties, __dirname);
201
+ this.dbUtil.dboptions = {};
202
+ // const databaseName = this.props.mongo ? this.props.mongo.database : undefined;
203
+ // const dbInfo = { dburl: this.dbUtil.dburl, dboptions: this.dbUtil.dboptions, database: databaseName };
204
+ this.allow_connect = false;
205
+
198
206
  // put topics from file into memory
199
207
  this.topicsEvents = [];
200
208
  let topicsFile = path.join(__dirname, `/.topics-${this.id}.json`);
@@ -465,10 +473,61 @@ class Kafkav2 extends AdapterBaseCl {
465
473
  ({ errors } = errorData);
466
474
 
467
475
  // rewrite the topics file for persistence
476
+ let databaseIsAlive = false;
477
+ const dedupeCollectionName = `dedupe-${myid}`;
468
478
  if (this.props && this.props.stub === false) {
469
479
  const intTime = this.props.interval_time || 30000;
470
- setInterval(() => {
480
+ setInterval(async () => {
471
481
  try {
482
+ let documents;
483
+
484
+ // Write to Database if connected
485
+ if (databaseIsAlive) {
486
+ const topicNames = this.topicsEvents.map((topicEvent) => topicEvent.topic);
487
+ const promises = [];
488
+ this.topicsEvents.forEach((t, ind) => {
489
+ const eachTopic = this.topicsEvents[ind];
490
+ eachTopic.offset = parseInt(eachTopic.offset, 10);
491
+ const filter = { topic: eachTopic.topic, offset: { $lt: eachTopic.offset } };
492
+ const update = { $set: { offset: eachTopic.offset } };
493
+ const atomicDBupdate = this.dbUtil.findAndModify(dedupeCollectionName, filter, [], update, false, undefined, false)
494
+ .then((findRes) => {
495
+ if (findRes && findRes.lastErrorObject && findRes.lastErrorObject.updatedExisting) {
496
+ log.debug(`Updated topic: ${eachTopic.topic} to offset: ${eachTopic.offset}`);
497
+ }
498
+ }).catch((reason) => {
499
+ log.error(`Couldn't update topic: ${eachTopic.topic}: ${reason}`);
500
+ });
501
+ promises.push(atomicDBupdate);
502
+ });
503
+
504
+ try {
505
+ await Promise.all(promises);
506
+ // Then read from it (important if only some topics are updated)
507
+ const options = { filter: { topic: { $in: topicNames } }, limit: 0 };
508
+ documents = await this.dbUtil.find(dedupeCollectionName, options, undefined, false);
509
+ } catch (err) {
510
+ log.error(err);
511
+ }
512
+ }
513
+
514
+ // Then update local .topics-id.json
515
+ if (documents) {
516
+ documents.forEach((document) => {
517
+ if (!document.topic) {
518
+ log.warn(`document ${document} does not contain property 'topic' when it should`);
519
+ return;
520
+ }
521
+ const topicEvent = this.topicsEvents.find((t) => t.topic === document.topic);
522
+ if (!topicEvent) {
523
+ log.warn(`topicEvents does not have topic: ${document.topic} when it should`);
524
+ return;
525
+ }
526
+
527
+ topicEvent.offset = Math.max(topicEvent.offset, document.offset);
528
+ });
529
+ }
530
+
472
531
  const split = topicsFile.split('/');
473
532
  log.debug(`Update ${split[split.length - 1]} file.`);
474
533
  fs.writeFileSync(topicsFile, JSON.stringify(this.topicsEvents, null, 2));
@@ -477,6 +536,47 @@ class Kafkav2 extends AdapterBaseCl {
477
536
  }
478
537
  }, intTime);
479
538
  }
539
+
540
+ const setupDatabase = async (dbUtil, collectionName, setupDBInfo, topicEvents) => {
541
+ await dbUtil.createCollection(collectionName, setupDBInfo, false);
542
+ const dbInserts = [];
543
+ topicEvents.forEach((topicEvent) => {
544
+ const insert = dbUtil.findAndModify(collectionName, { topic: topicEvent.topic }, [], { $setOnInsert: topicEvent }, true, setupDBInfo, false);
545
+ dbInserts.push(insert);
546
+ });
547
+ await Promise.all(dbInserts);
548
+
549
+ const topicNames = topicEvents.map((t) => t.topic);
550
+ const options = { filter: { topic: { $in: topicNames } }, limit: 0 };
551
+ const documents = await this.dbUtil.find(collectionName, options, undefined, false);
552
+ topicEvents.forEach((t, ind) => {
553
+ const topicEvent = this.topicsEvents[ind];
554
+ const doc = documents.find((d) => d.topic === topicEvent.topic);
555
+ if (!doc) throw new Error(`There is no document with topic ${topicEvent.topic} when there should be`);
556
+ topicEvent.offset = Math.max(topicEvent.offset, doc.offset);
557
+ });
558
+ return topicEvents;
559
+ };
560
+ // Connect and set up database + topics
561
+ this.dbUtil.connect()
562
+ .then((connectionInfo) => {
563
+ if (connectionInfo.isAlive) {
564
+ log.info('Connected to adapter database');
565
+ } else {
566
+ throw new Error('Could not connect to adapter database');
567
+ }
568
+ })
569
+ .then(async () => {
570
+ const databaseTopics = await setupDatabase(this.dbUtil, dedupeCollectionName, undefined, this.topicsEvents);
571
+ this.topicsEvents = databaseTopics;
572
+ databaseIsAlive = true;
573
+ this.allow_connect = true;
574
+ })
575
+ .catch((err) => {
576
+ databaseIsAlive = false;
577
+ log.error(`Database Connection error: ${err}`);
578
+ this.allow_connect = true;
579
+ });
480
580
  }
481
581
 
482
582
  /**
@@ -485,6 +585,15 @@ class Kafkav2 extends AdapterBaseCl {
485
585
  * @function connect
486
586
  */
487
587
  connect() {
588
+ // Wait to read from database before connecting
589
+ if (!this.allow_connect) {
590
+ const retryIn = 1000;
591
+ const sleep = new Promise((r) => setTimeout(r, retryIn));
592
+ sleep.then(() => this.connect());
593
+ log.warn('Waiting to connect...');
594
+ return;
595
+ }
596
+ log.info('Connecting...');
488
597
  const meth = 'adapter-connect';
489
598
  const origin = `${this.id}-${meth}`;
490
599
  log.trace(origin);
@@ -518,7 +627,6 @@ class Kafkav2 extends AdapterBaseCl {
518
627
  if (this.props.client.ssl && this.props.client.ssl.key) {
519
628
  combinedProps.ssl.key = fs.readFileSync(this.props.client.ssl.key, 'utf-8');
520
629
  }
521
-
522
630
  this.KafkaClient = new Kafka(combinedProps);
523
631
 
524
632
  if (this.props.client && this.props.client.producersasl) {
@@ -604,7 +712,16 @@ class Kafkav2 extends AdapterBaseCl {
604
712
  }
605
713
  }, intTime);
606
714
  }
607
- await this.consumer.run({
715
+
716
+ const topicOffsetsToSeek = this.topicsEvents.map((t) => (
717
+ {
718
+ topic: t.topic,
719
+ partition: t.partition,
720
+ offset: Number.parseInt(t.offset, 10) + 1 // Seek to the next offset you're going to read
721
+ }
722
+ ));
723
+
724
+ const runPromise = this.consumer.run({
608
725
  eachMessage: async ({ topic, partition, message }) => {
609
726
  log.info(`PROCESSING NEW MESSAGE ON TOPIC: ${topic} PARTITION: ${partition} WITH OFFSET: ${message.offset}`);
610
727
  const newMsg = message;
@@ -645,8 +762,12 @@ class Kafkav2 extends AdapterBaseCl {
645
762
  for (let t = 0; t < this.topicsEvents.length; t += 1) {
646
763
  if (this.topicsEvents[t].topic === topic && (this.topicsEvents[t].partition === partition || (Array.isArray(this.topicsEvents[t].partition) && this.topicsEvents[t].partition.includes(partition)))) {
647
764
  allowedPartition = true;
648
- if (this.topicsEvents[t].offset < message.offset) {
649
- this.topicsEvents[t].offset = message.offset;
765
+ const topicOffset = parseInt(this.topicsEvents[t].offset, 10);
766
+ const messageOffset = parseInt(message.offset, 10);
767
+ if (Number.isNaN(topicOffset) || Number.isNaN(messageOffset)) throw new Error('Topic offset or message offset is NaN');
768
+ // if (this.topicsEvents[t].offset < message.offset) {
769
+ if (topicOffset < messageOffset) {
770
+ this.topicsEvents[t].offset = messageOffset;
650
771
  }
651
772
  // determine if we are using AVRO and set flag
652
773
  if (this.topicsEvents[t].avro && this.topicsEvents[t].avro.toUpperCase() === 'YES') {
@@ -702,6 +823,13 @@ class Kafkav2 extends AdapterBaseCl {
702
823
  }
703
824
  }
704
825
  });
826
+
827
+ // Set consumer offsets to match database
828
+ topicOffsetsToSeek.forEach((topicPartitionOffset) => {
829
+ this.consumer.seek(topicPartitionOffset);
830
+ });
831
+
832
+ await runPromise;
705
833
  };
706
834
  run().catch((err) => console.error(`[consumer] ${err.message}`, err));
707
835
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itentialopensource/adapter-kafkav2",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Itential adapter to connect to kafka",
5
5
  "main": "adapter.js",
6
6
  "wizardVersion": "2.35.0",
@@ -14,7 +14,6 @@
14
14
  "test:baseunit": "mocha test/unit/adapterBaseTestUnit.js --LOG=error",
15
15
  "test:unit": "mocha test/unit/adapterTestUnit.js --LOG=error",
16
16
  "test:integration": "mocha test/integration/adapterTestIntegration.js --LOG=error",
17
- "test:cover": "nyc --reporter html --reporter text mocha --reporter dot test/*",
18
17
  "test": "npm run test:unit && npm run test:integration",
19
18
  "deploy": "npm publish --registry=https://registry.npmjs.org --access=public",
20
19
  "build": "npm run deploy"
@@ -49,6 +48,7 @@
49
48
  "fs-extra": "^11.1.1",
50
49
  "json-query": "^2.2.2",
51
50
  "kafkajs": "2.2.3",
51
+ "mongodb": "^6.2.0",
52
52
  "patch-package": "^6.4.7",
53
53
  "readline-sync": "^1.4.10",
54
54
  "request": "^2.88.2",
@@ -61,7 +61,6 @@
61
61
  "eslint-plugin-import": "^2.27.5",
62
62
  "eslint-plugin-json": "^3.1.0",
63
63
  "mocha": "^9.0.1",
64
- "nyc": "^15.1.0",
65
64
  "testdouble": "^3.18.0",
66
65
  "winston": "^3.3.3"
67
66
  },
package/pronghorn.json CHANGED
@@ -9,9 +9,7 @@
9
9
  "admin"
10
10
  ],
11
11
  "topics": {
12
- "kafka": {},
13
- "test-topic": {},
14
- "test_topic": {}
12
+ "kafka": {}
15
13
  },
16
14
  "methods": [
17
15
  {
Binary file