@jetit/publisher 5.6.0 → 5.6.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "5.6.0",
3
+ "version": "5.6.3",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "^0.0.13",
@@ -10,7 +10,11 @@ class PrometheusAdapter {
10
10
  constructor(streams, promClient) {
11
11
  this.streams = streams;
12
12
  this.promClient = promClient;
13
- this.registry = new this.promClient.Registry({ collectDefaultMetrics: { timeout: 60000 } });
13
+ this.registry = new this.promClient.Registry();
14
+ this.promClient.collectDefaultMetrics({
15
+ register: this.registry,
16
+ timeout: 60000,
17
+ });
14
18
  this.initializeMetrics();
15
19
  }
16
20
  initializeMetrics() {
@@ -159,10 +163,10 @@ class PrometheusAdapter {
159
163
  }
160
164
  });
161
165
  Object.entries(metrics.redisCommandLatencies).forEach(([command, latency]) => {
162
- this.redisCommandLatency.set({ command }, latency);
166
+ this.redisCommandLatency.set({ command }, latency?.total / latency?.count || 0);
163
167
  });
164
168
  Object.entries(metrics.consumerLag).forEach(([consumerGroup, lag]) => {
165
- this.consumerLag.set({ consumer_group: consumerGroup }, lag);
169
+ this.consumerLag.set({ consumer_group: consumerGroup }, lag?.total / lag?.count || 0);
166
170
  });
167
171
  }
168
172
  /**
@@ -171,7 +175,7 @@ class PrometheusAdapter {
171
175
  setupEndpoint(app, endPoint = '/metrics') {
172
176
  app.get(endPoint, async (_, res) => {
173
177
  await this.updateMetrics();
174
- res.set('Content-Type', this.registry.contentType);
178
+ res.header('Content-Type', this.registry.contentType);
175
179
  res.send(await this.registry.metrics());
176
180
  });
177
181
  }
@@ -69,7 +69,7 @@ class MetricsTracker {
69
69
  return i === 0 ? `0-${buckets[i]}` : `${buckets[i - 1] + 1}-${buckets[i]}`;
70
70
  }
71
71
  }
72
- return `>${buckets[buckets.length - 1]}`;
72
+ return `10000-100000`;
73
73
  }
74
74
  addRedisCommandLatency(command, time) {
75
75
  if (!this.metrics.redisCommandLatencies[command]) {
@@ -63,7 +63,7 @@ class ScheduledProcessor {
63
63
  .xadd(streamName, key, 'data', JSON.stringify(eventData))
64
64
  .catch((e) => logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Publishing event ${eventData.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(eventData)}, ${e} `));
65
65
  if (key === '*')
66
- key = generatedKey ?? key;
66
+ key = `${generatedKey ?? key}`;
67
67
  }
68
68
  if (eventData.repeatInterval) {
69
69
  const nextEventTime = currentTime + eventData.repeatInterval;
@@ -62,13 +62,14 @@ class Streams {
62
62
  this.instanceUniqueId = process.env['INSTANCE_ID'] ?? (0, id_1.generateID)('HEX', 'FE');
63
63
  this.instanceId = `${serviceName}:${this.instanceUniqueId}`;
64
64
  this.consumerGroupName = `cg-${serviceName}`;
65
- logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Instance ID: ${this.instanceId}`);
65
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Instance ID: ${this.instanceId} and with config: ${JSON.stringify(this.config)}`);
66
66
  const cleanUpInterval = this.config.cleanUpInterval ?? parseInt(process.env['CLEANUP_INTERVAL'] ?? `${this.config.cleanUpInterval}`, 10);
67
67
  this.cleanUpTimer = setInterval(() => {
68
68
  this.runClear(cleanUpInterval).catch((error) => {
69
- logger_1.PUBLISHER_LOGGER.error('Error during cleanup:', error);
69
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error during cleanup:', error);
70
70
  });
71
71
  }, cleanUpInterval);
72
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Clean Up process setup for ${cleanUpInterval} ms`);
72
73
  this.dlq = new dlq_1.DeadLetterQueue(this.redisPublisher, config.dlqEventThreshold);
73
74
  this.metricsCollector = new collector_1.MetricsCollector({
74
75
  redisClient: this.redisPublisher,
@@ -82,7 +83,7 @@ class Streams {
82
83
  }
83
84
  setupCircuitBreakerListeners() {
84
85
  this.circuitBreaker.on('stateChange', async (newState) => {
85
- logger_1.PUBLISHER_LOGGER.log(`Circuit breaker state changed to: ${circuit_breaker_1.CircuitState[newState]}`);
86
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Circuit breaker state changed to: ${circuit_breaker_1.CircuitState[newState]}`);
86
87
  if (newState === circuit_breaker_1.CircuitState.CLOSED) {
87
88
  await this.processStoredEvents();
88
89
  }
@@ -92,10 +93,10 @@ class Streams {
92
93
  logger_1.PUBLISHER_LOGGER.log('PUBLISHER: Running Clearance', this.eventsListened);
93
94
  const cleanupPromises = this.eventsListened.map((eventName) => this.cleanupAcknowledgedMessages(eventName, cleanUpInterval)
94
95
  .then(() => {
95
- logger_1.PUBLISHER_LOGGER.log(`Cleanup process for Acknowledged messages completed for ${eventName}`);
96
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Cleanup process for Acknowledged messages completed for ${eventName}`);
96
97
  })
97
98
  .catch((error) => {
98
- logger_1.PUBLISHER_LOGGER.error(`Error during cleanup for ${eventName}:`, error);
99
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error during cleanup for ${eventName}:`, error);
99
100
  }));
100
101
  await Promise.all(cleanupPromises);
101
102
  }
@@ -133,7 +134,7 @@ class Streams {
133
134
  */
134
135
  if (this.config.circuitBreaker.enabled && !this.circuitBreaker.isAllowed()) {
135
136
  await this.circuitBreaker.storeEvent(data);
136
- logger_1.PUBLISHER_LOGGER.error('Circuit is open, event stored for later processing');
137
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Circuit is open, event stored for later processing');
137
138
  return 'CIRCUIT_BREAKER_FLOW';
138
139
  }
139
140
  try {
@@ -264,14 +265,14 @@ class Streams {
264
265
  // If both exist, this will be a no-op
265
266
  try {
266
267
  await this.redisGroups.xgroup('CREATE', streamName, this.consumerGroupName, '0', 'MKSTREAM');
267
- logger_1.PUBLISHER_LOGGER.log(`Group created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
268
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Group created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
268
269
  }
269
270
  catch (e) {
270
271
  // BUSYGROUP error means group already exists, which is fine
271
272
  if (!e.message.includes('BUSYGROUP')) {
272
273
  throw e;
273
274
  }
274
- logger_1.PUBLISHER_LOGGER.log(`Group already exists for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
275
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Group already exists for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
275
276
  }
276
277
  // Create consumer (idempotent operation)
277
278
  const createConsumerStatus = await this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId);
@@ -320,11 +321,11 @@ class Streams {
320
321
  await processMessage(this.redisGroups, '0', new tracker_1.MetricsTracker(), false);
321
322
  }
322
323
  catch (error) {
323
- logger_1.PUBLISHER_LOGGER.error('Error in running recurring cleanup task:', error);
324
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error in running recurring cleanup task:', error);
324
325
  }
325
326
  },
326
327
  error: (error) => {
327
- logger_1.PUBLISHER_LOGGER.error('Fatal error in cleanup timer:', error);
328
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Fatal error in cleanup timer:', error);
328
329
  },
329
330
  });
330
331
  // Create observable with proper cleanup
@@ -368,6 +369,17 @@ class Streams {
368
369
  * they need to be read using XRANGE.
369
370
  */
370
371
  if (multicast || processPending) {
372
+ /**
373
+ * Very very rare case of this occurring. Cases where the running service is super overloaded
374
+ * that causes a messaged to be sent to processing with a delay, but eventually before being
375
+ * processed gets picked up by another instance, leading to multiple publications
376
+ */
377
+ if (processPending) {
378
+ const claimed = await redisClient.xclaim(streamName, this.consumerGroupName, this.instanceId, 10000, messageId, 'JUSTID');
379
+ if (!claimed || claimed.length === 0) {
380
+ return; // Message already claimed or acknowledged by another consumer, so don't repush to the subscriber
381
+ }
382
+ }
371
383
  const messages = await redisClient.xrange(streamName, messageId, messageId);
372
384
  if (messages?.length) {
373
385
  try {
@@ -382,22 +394,30 @@ class Streams {
382
394
  else {
383
395
  const messages = (await redisClient.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'STREAMS', streamName, '>'));
384
396
  if (messages?.length) {
385
- if (messageId === '0') {
386
- messageId = messages[0][1][0][0];
387
- logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Reprocessing unprocessed message with id: ${messageId}`);
397
+ const messageIdRead = messages[0][1][0][0];
398
+ if (messageIdRead !== messageId) {
399
+ if (messageId === '0') {
400
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Reprocessing unprocessed message with id: ${messageId}`);
401
+ }
402
+ else {
403
+ const [timestamp] = messageIdRead?.split('-').map(Number);
404
+ const [publishedTimestamp] = messageId?.split('-').map(Number);
405
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Message queue processing is ${publishedTimestamp > timestamp ? 'ahead' : 'behind'}`);
406
+ }
388
407
  }
408
+ messageId = messageIdRead;
389
409
  try {
390
410
  eventData = JSON.parse(messages[0][1][0][1][1]);
391
411
  }
392
412
  catch (error) {
393
- logger_1.PUBLISHER_LOGGER.error(`JSON parsing failed for message: ${messages[0][1][0][1][1]}`);
413
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: JSON parsing failed for message: ${messages[0][1][0][1][1]}`);
394
414
  return;
395
415
  }
396
416
  }
397
417
  }
398
418
  }
399
419
  catch (error) {
400
- logger_1.PUBLISHER_LOGGER.error('Error retrieving or parsing event data:', error);
420
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error retrieving or parsing event data:', error);
401
421
  return;
402
422
  }
403
423
  finally {
@@ -433,7 +453,7 @@ class Streams {
433
453
  }
434
454
  catch (error) {
435
455
  // Log error but don't fail entire processing
436
- logger_1.PUBLISHER_LOGGER.error(`Error processing subscription ${subId}:`, error);
456
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error processing subscription ${subId}:`, error);
437
457
  }
438
458
  }));
439
459
  }
@@ -449,7 +469,7 @@ class Streams {
449
469
  tracker.setConsumerLag(this.consumerGroupName, currentTime - eventData.createdAt);
450
470
  }
451
471
  catch (error) {
452
- logger_1.PUBLISHER_LOGGER.error(`Processing error for message ${messageId}:`, error);
472
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Processing error for message ${messageId}:`, error);
453
473
  const dlqEvent = {
454
474
  ...eventData,
455
475
  failureReason: error.message,
@@ -469,13 +489,31 @@ class Streams {
469
489
  /** Process Unprocessed Messages with rate limiting */
470
490
  if (!processPending) {
471
491
  const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, this.instanceId);
492
+ /**
493
+ * Dealing with the case where messages don't get processed due to large
494
+ * batch sizes. The previous default was 20ms, which seemed to work in
495
+ * most cases but seem to fail when the number of retry messages are high.
496
+ *
497
+ * The old solution did not take into account the fact that this library
498
+ * is dependent on the infrastructure of the app that runs it, so any memory/
499
+ * resource/stack overload on the app has an impact in this
500
+ */
501
+ const getDelay = (count) => {
502
+ if (count > 100)
503
+ return 500; // 500ms for large backlogs
504
+ if (count > 50)
505
+ return 300; // 300ms for medium-large
506
+ if (count > 20)
507
+ return 200; // 200ms for medium
508
+ return 100; // 100ms for small
509
+ };
472
510
  if (unprocessedMessageIds.countOnThisConsumer &&
473
511
  unprocessedMessageIds.countOnThisConsumer > this.config.unprocessedMessageThreshold) {
474
512
  logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
475
513
  }
476
514
  // Process messages with rate limiting
477
515
  const processWithDelay = async (id, index) => {
478
- await new Promise((resolve) => setTimeout(resolve, index * 20));
516
+ await new Promise((resolve) => setTimeout(resolve, index * getDelay(unprocessedMessageIds.countOnThisConsumer ?? 1)));
479
517
  await processMessage(redisClient, id, new tracker_1.MetricsTracker(), multicast, true);
480
518
  };
481
519
  unprocessedMessageIds.messageIds.map((id, index) => processWithDelay(id, index));
@@ -573,7 +611,7 @@ class Streams {
573
611
  }
574
612
  }
575
613
  catch (error) {
576
- logger_1.PUBLISHER_LOGGER.error('Error during cleanup:', error);
614
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error during cleanup:', error);
577
615
  }
578
616
  }
579
617
  async cleanupAcknowledgedMessages(eventName, interval = this.config.acknowledgedMessageCleanupInterval) {
@@ -581,27 +619,34 @@ class Streams {
581
619
  const lastAckKey = `last_ack:${streamName}`;
582
620
  const oneHourAgo = Date.now() - interval;
583
621
  try {
584
- // Get consumer group info to check if consumers are active
585
- const groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName));
586
- // If no active consumers, leave stream as is
587
- if (!groupInfo || !groupInfo.some((group) => group.consumers > 0)) {
588
- logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: No active consumers for ${streamName}, leaving stream as is`);
589
- return;
590
- }
622
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Started [cleanupAcknowledgedMessages] for ${streamName} and with cleanUpInterval set as ${interval}`);
591
623
  // Get last acknowledged message ID
592
624
  const lastAckId = await this.redisGroups.get(lastAckKey);
625
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: [cleanupAcknowledgedMessages] {lastAckId: ${lastAckId}, lastAckKey: ${lastAckKey}} `);
593
626
  if (!lastAckId) {
594
- logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: No acknowledged messages for ${streamName}`);
627
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: [cleanupAcknowledgedMessages]No acknowledged messages for ${streamName}`);
595
628
  return;
596
629
  }
597
630
  // Extract timestamp from message ID
598
631
  const [timestamp] = lastAckId.split('-').map(Number);
599
632
  const cleanupThreshold = Math.min(timestamp, oneHourAgo);
633
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: [cleanupAcknowledgedMessages] {cleanUpThreshold: ${cleanupThreshold}} `);
634
+ // Get consumer group info to check if consumers are active
635
+ const groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName))?.map(([, name, , consumers]) => ({
636
+ name: name,
637
+ consumers: consumers,
638
+ }));
639
+ // If no active consumers, leave stream as is
640
+ if (groupInfo?.length === 0 || !groupInfo?.some((group) => group.consumers > 0)) {
641
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: No active consumers for ${streamName}, leaving stream as is`);
642
+ return;
643
+ }
644
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: [cleanupAcknowledgedMessages] XTRIM to be called`);
600
645
  await this.redisGroups.xtrim(streamName, 'MINID', cleanupThreshold);
601
646
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Cleaned up messages before last acknowledged message ${timestamp} from ${streamName}`);
602
647
  }
603
648
  catch (error) {
604
- logger_1.PUBLISHER_LOGGER.error(`Error during cleanup for ${streamName}:`, error);
649
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error during cleanup for ${streamName}:`, error);
605
650
  }
606
651
  }
607
652
  async getDiagnosticData(events) {
@@ -730,7 +775,7 @@ class Streams {
730
775
  ]);
731
776
  }
732
777
  catch (error) {
733
- logger_1.PUBLISHER_LOGGER.error(`Error acknowledging message ${messageId} for ${streamName}:`, error);
778
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error acknowledging message ${messageId} for ${streamName}:`, error);
734
779
  throw error;
735
780
  }
736
781
  }