@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
|
@@ -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(
|
|
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.
|
|
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
|
|
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;
|
package/src/lib/redis/streams.js
CHANGED
|
@@ -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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 *
|
|
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
|
-
|
|
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
|
}
|