@orion-js/echoes 2.10.3 → 2.11.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/lib/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
4
 
5
+ var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
6
+
5
7
  Object.defineProperty(exports, "__esModule", {
6
8
  value: true
7
9
  });
@@ -11,6 +13,12 @@ Object.defineProperty(exports, "startService", {
11
13
  return _startService.default;
12
14
  }
13
15
  });
16
+ Object.defineProperty(exports, "stopService", {
17
+ enumerable: true,
18
+ get: function () {
19
+ return _startService.stopService;
20
+ }
21
+ });
14
22
  Object.defineProperty(exports, "publish", {
15
23
  enumerable: true,
16
24
  get: function () {
@@ -30,7 +38,7 @@ Object.defineProperty(exports, "request", {
30
38
  }
31
39
  });
32
40
 
33
- var _startService = _interopRequireDefault(require("./startService"));
41
+ var _startService = _interopRequireWildcard(require("./startService"));
34
42
 
35
43
  var _publish = _interopRequireDefault(require("./publish"));
36
44
 
@@ -18,7 +18,7 @@ var _serialize = _interopRequireDefault(require("./serialize"));
18
18
  */
19
19
  async function _default(options) {
20
20
  if (!_config.default.producer) {
21
- throw new Error('You must initialize echoes configruation to use publish');
21
+ throw new Error('You must initialize echoes configuration to use publish');
22
22
  }
23
23
 
24
24
  const payload = {
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _kafkajs = require("kafkajs");
11
+
12
+ var _types = _interopRequireDefault(require("../echo/types"));
13
+
14
+ const HEARTBEAT_INTERVAL_SECONDS = 5; // This value must be less than the kafkajs session timeout
15
+
16
+ const CHECK_JOIN_CONSUMER_INTERVAL_SECONDS = 30;
17
+ const DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY = 4; // How many partitions to consume concurrently, adjust this with the members to partitions ratio to avoid idle consumers.
18
+
19
+ const DEFAULT_MEMBERS_TO_PARTITIONS_RATIO = 1; // How many members are in comparison to partitions, this is used to determine if the consumer group has room for more members. Numbers over 1 leads to idle consumers. Numbers under 1 needs partitionsConsumedConcurrently to be more than 1.
20
+
21
+ class KafkaManager {
22
+ constructor(options) {
23
+ this.kafka = new _kafkajs.Kafka(options.client);
24
+ this.options = options;
25
+ this.producer = this.kafka.producer(options.producer);
26
+ this.consumer = this.kafka.consumer(options.consumer);
27
+ this.topics = Object.keys(options.echoes).filter(key => options.echoes[key].type === _types.default.event);
28
+ }
29
+
30
+ async checkJoinConsumerGroupConditions() {
31
+ const admin = this.kafka.admin();
32
+
33
+ try {
34
+ await admin.connect();
35
+ const groupDescriptions = await admin.describeGroups([this.options.consumer.groupId]);
36
+ const group = groupDescriptions.groups[0];
37
+
38
+ if (group.state === 'Empty') {
39
+ console.info(`Echoes: Consumer group ${this.options.consumer.groupId} is empty, joining`);
40
+ return true;
41
+ }
42
+
43
+ const topicsMetadata = await admin.fetchTopicMetadata({
44
+ topics: this.topics
45
+ });
46
+ const totalPartitions = topicsMetadata.topics.reduce((acc, t) => acc + t.partitions.length, 0);
47
+ console.info(`Echoes: Consumer group ${this.options.consumer.groupId} has ${group.members.length} members and ${totalPartitions} partitions`);
48
+ const partitionsRatio = this.options.membersToPartitionsRatio || DEFAULT_MEMBERS_TO_PARTITIONS_RATIO;
49
+ const partitionsThreshold = Math.ceil(totalPartitions * partitionsRatio);
50
+
51
+ if (partitionsThreshold > group.members.length) {
52
+ console.info(`Echoes: Consumer group ${this.options.consumer.groupId} has room for more members ${group.members.length}/${partitionsThreshold}, joining`);
53
+ return true;
54
+ }
55
+ } catch (error) {
56
+ console.error(`Echoes: Error checking consumer group conditions, joining: ${error.message}`);
57
+ return true;
58
+ } finally {
59
+ await admin.disconnect().catch(error => {
60
+ console.error(`Echoes: Error disconnecting admin client: ${error.message}`);
61
+ });
62
+ }
63
+ }
64
+
65
+ async joinConsumerGroup() {
66
+ await this.consumer.connect();
67
+ await this.consumer.subscribe({
68
+ topics: this.topics
69
+ });
70
+ this.consumer.run({
71
+ partitionsConsumedConcurrently: this.options.partitionsConsumedConcurrently || DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY,
72
+ eachMessage: params => this.handleMessage(params)
73
+ });
74
+ }
75
+
76
+ async conditionalStart() {
77
+ if (await this.checkJoinConsumerGroupConditions()) {
78
+ await this.joinConsumerGroup();
79
+ return true;
80
+ }
81
+ }
82
+
83
+ async start() {
84
+ if (this.started) return;
85
+ await this.producer.connect();
86
+ this.started = await this.conditionalStart();
87
+ if (this.started) return;
88
+ console.info('Echoes: Delaying consumer group join, waiting for conditions to be met');
89
+ this.interval = setInterval(async () => {
90
+ this.started = await this.conditionalStart();
91
+ if (this.started) clearInterval(this.interval);
92
+ }, CHECK_JOIN_CONSUMER_INTERVAL_SECONDS * 1000);
93
+ }
94
+
95
+ async stop() {
96
+ console.warn("Echoes: Stopping echoes");
97
+
98
+ if (this.interval) {
99
+ clearInterval(this.interval);
100
+ }
101
+
102
+ if (this.consumer) {
103
+ await this.consumer.disconnect();
104
+ }
105
+
106
+ if (this.producer) {
107
+ await this.producer.disconnect();
108
+ }
109
+ }
110
+
111
+ async handleMessage(params) {
112
+ const echo = this.options.echoes[params.topic];
113
+
114
+ if (!echo || echo.type !== _types.default.event) {
115
+ console.warn(`Echoes: Received a message for an unknown topic: ${params.topic}, ignoring it`);
116
+ return;
117
+ }
118
+
119
+ let intervalsCount = 0;
120
+ const hInterval = setInterval(async () => {
121
+ await params.heartbeat().catch(error => {
122
+ console.warn(`Echoes: Error sending heartbeat: ${error.message}`);
123
+ });
124
+ intervalsCount++;
125
+
126
+ if (intervalsCount * HEARTBEAT_INTERVAL_SECONDS % 30 === 0) {
127
+ console.warn(`Echoes: Event is taking too long to process: ${params.topic} ${intervalsCount * HEARTBEAT_INTERVAL_SECONDS}s`);
128
+ }
129
+ }, HEARTBEAT_INTERVAL_SECONDS * 1000);
130
+
131
+ try {
132
+ await echo.onMessage(params).catch(error => this.handleRetries(echo, params, error));
133
+ } catch (error) {
134
+ console.error(`Echoes: error processing a message: ${params.topic} ${error.message}`);
135
+ throw error;
136
+ } finally {
137
+ clearInterval(hInterval);
138
+ }
139
+ }
140
+
141
+ async handleRetries(echo, params, error) {
142
+ const {
143
+ message,
144
+ topic
145
+ } = params;
146
+ const retries = getRetries(message);
147
+
148
+ if (echo.attemptsBeforeDeadLetter === undefined || echo.attemptsBeforeDeadLetter === null) {
149
+ throw error;
150
+ }
151
+
152
+ const maxRetries = echo.attemptsBeforeDeadLetter || 0;
153
+ const exceededMaxRetries = retries >= maxRetries;
154
+ const nextTopic = exceededMaxRetries ? `DLQ-${topic}` : topic;
155
+ await this.producer.send({
156
+ topic: nextTopic,
157
+ messages: [{
158
+ value: message.value.toString(),
159
+ headers: {
160
+ retries: String(retries + 1),
161
+ errorMessage: error.message
162
+ }
163
+ }]
164
+ });
165
+
166
+ if (exceededMaxRetries) {
167
+ console.error(`Echoes: a message has reached the maximum number of retries, sending it to DLQ: ${nextTopic}`);
168
+ } else {
169
+ console.warn(`Echoes: a retryable message failed "${error.message}", re-sending it to topic: ${nextTopic}`);
170
+ }
171
+ }
172
+
173
+ }
174
+
175
+ function getRetries(message) {
176
+ if (!message || !message.headers || !message.headers.retries) return 0;
177
+ return Number.parseInt(message.headers.retries.toString());
178
+ }
179
+
180
+ var _default = KafkaManager;
181
+ exports.default = _default;
@@ -6,57 +6,22 @@ Object.defineProperty(exports, "__esModule", {
6
6
  value: true
7
7
  });
8
8
  exports.default = startService;
9
-
10
- var _kafkajs = require("kafkajs");
9
+ exports.stopService = stopService;
11
10
 
12
11
  var _config = _interopRequireDefault(require("../config"));
13
12
 
14
13
  var _requestsHandler = _interopRequireDefault(require("../requestsHandler"));
15
14
 
16
- var _types = _interopRequireDefault(require("../echo/types"));
15
+ var _KafkaManager = _interopRequireDefault(require("./KafkaManager"));
17
16
 
18
- const HEARTBEAT_INTERVAL_SECONDS = 3;
17
+ let kafkaManager = null;
19
18
 
20
19
  async function startService(options) {
21
20
  if (options.client) {
22
- const kafka = new _kafkajs.Kafka(options.client);
23
- _config.default.producer = kafka.producer(options.producer);
24
- _config.default.consumer = kafka.consumer(options.consumer);
25
- await _config.default.producer.connect();
26
- await _config.default.consumer.connect();
27
-
28
- for (const topic in options.echoes) {
29
- const echo = options.echoes[topic];
30
- if (echo.type !== _types.default.event) continue;
31
- await _config.default.consumer.subscribe({
32
- topic,
33
- fromBeginning: options.readTopicsFromBeginning || false
34
- });
35
- }
36
-
37
- _config.default.consumer.run({
38
- eachMessage: async params => {
39
- const echo = options.echoes[params.topic];
40
- if (!echo) return;
41
- if (echo.type !== _types.default.event) return;
42
- let intervalsCount = 0;
43
- const interval = setInterval(async () => {
44
- await params.heartbeat().catch(error => {
45
- console.warn('Echoes: Error sending heartbeat:', error);
46
- });
47
- intervalsCount++;
48
-
49
- if (intervalsCount % 10 === 0) {
50
- console.warn(`Echoes: Event ${params.topic} is taking too long to process: ${intervalsCount * HEARTBEAT_INTERVAL_SECONDS}s`);
51
- }
52
- }, HEARTBEAT_INTERVAL_SECONDS * 1000);
53
- await echo.onMessage(params).catch(error => {
54
- clearInterval(interval);
55
- throw error;
56
- });
57
- clearInterval(interval);
58
- }
59
- });
21
+ kafkaManager = new _KafkaManager.default(options);
22
+ await kafkaManager.start();
23
+ _config.default.producer = kafkaManager.producer;
24
+ _config.default.consumer = kafkaManager.consumer;
60
25
  }
61
26
 
62
27
  if (options.requests) {
@@ -67,4 +32,12 @@ async function startService(options) {
67
32
  _config.default.requests.startHandler(_requestsHandler.default);
68
33
  }
69
34
  }
35
+ }
36
+
37
+ async function stopService() {
38
+ if (kafkaManager) {
39
+ console.info("Stoping echoes...");
40
+ await kafkaManager.stop();
41
+ console.info("Echoes stopped");
42
+ }
70
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orion-js/echoes",
3
- "version": "2.10.3",
3
+ "version": "2.11.0",
4
4
  "main": "lib/index.js",
5
5
  "author": "nicolaslopezj",
6
6
  "license": "MIT",
@@ -29,5 +29,5 @@
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
- "gitHead": "30a4b8f24d4fd596818058305488368844920479"
32
+ "gitHead": "7985a356f3e823b338d3d1c478acc29c5b10e1de"
33
33
  }