@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 +16 -0
- package/adapter.js +133 -5
- package/package.json +2 -3
- package/pronghorn.json +1 -3
- package/refs?service=git-upload-pack +0 -0
- package/utils/dbUtil.js +1603 -0
- package/utils/pre-commit.sh +1 -1
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
|
-
|
|
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
|
-
|
|
649
|
-
|
|
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.
|
|
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
|
Binary file
|