@jetit/publisher 5.2.2 → 5.4.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/package.json +5 -5
- package/src/lib/monitoring/collector.js +15 -22
- package/src/lib/performance/circuit_breaker.d.ts +0 -1
- package/src/lib/redis/batch.js +2 -3
- package/src/lib/redis/registry.js +2 -2
- package/src/lib/redis/streams.d.ts +4 -0
- package/src/lib/redis/streams.js +185 -134
- package/src/lib/redis/utils.js +16 -12
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetit/publisher",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@jetit/id": "^0.0.
|
|
7
|
-
"events": "3.3.0",
|
|
6
|
+
"@jetit/id": "^0.0.13",
|
|
8
7
|
"ioredis": "^5.3.0",
|
|
9
8
|
"rxjs": "^7.8.0",
|
|
10
|
-
"tslib": "2.
|
|
9
|
+
"tslib": "2.7.0"
|
|
11
10
|
},
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
12
|
"main": "./src/index.js"
|
|
13
|
-
}
|
|
13
|
+
}
|
|
@@ -111,33 +111,26 @@ class MetricsCollector {
|
|
|
111
111
|
const individualDepths = {};
|
|
112
112
|
let cursor = '0';
|
|
113
113
|
do {
|
|
114
|
-
const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH',
|
|
114
|
+
const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', '*:cg-*', 'COUNT', 100);
|
|
115
115
|
cursor = nextCursor;
|
|
116
116
|
if (keys.length > 0) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const [ackCountErr, ackCount] = results[i + 1];
|
|
130
|
-
if (streamLengthErr) {
|
|
131
|
-
console.error(`Error getting length for key: ${streamLengthErr}`);
|
|
132
|
-
continue;
|
|
117
|
+
for (const streamKey of keys) {
|
|
118
|
+
try {
|
|
119
|
+
// Get stream length and pending info
|
|
120
|
+
const streamLength = await this.redisClient.xlen(streamKey);
|
|
121
|
+
// Extract consumer group name from stream key (format: eventName:cg-serviceName)
|
|
122
|
+
const consumerGroup = streamKey.split(':')[1];
|
|
123
|
+
// XPENDING returns [count, min-id, max-id, consumer-list]
|
|
124
|
+
const pendingInfo = await this.redisClient.xpending(streamKey, consumerGroup);
|
|
125
|
+
const totalPending = pendingInfo ? Number(pendingInfo[0]) : 0;
|
|
126
|
+
const queueDepth = Math.max(0, streamLength - totalPending);
|
|
127
|
+
totalDepth += queueDepth;
|
|
128
|
+
individualDepths[streamKey] = queueDepth;
|
|
133
129
|
}
|
|
134
|
-
|
|
135
|
-
console.error(`Error
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error(`Error processing key ${streamKey}:`, error);
|
|
136
132
|
continue;
|
|
137
133
|
}
|
|
138
|
-
const queueDepth = Math.max(0, streamLength - ackCount);
|
|
139
|
-
totalDepth += queueDepth;
|
|
140
|
-
individualDepths[key] = queueDepth;
|
|
141
134
|
}
|
|
142
135
|
}
|
|
143
136
|
} while (cursor !== '0');
|
package/src/lib/redis/batch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.publishBatch = publishBatch;
|
|
4
|
+
exports.publishScheduledBatch = publishScheduledBatch;
|
|
4
5
|
const id_1 = require("@jetit/id");
|
|
5
6
|
const tracker_1 = require("../monitoring/tracker");
|
|
6
7
|
const logger_1 = require("./logger");
|
|
@@ -72,8 +73,6 @@ async function publishBatchWithRetry(streams, events, options) {
|
|
|
72
73
|
function publishBatch(streams, events, options = {}) {
|
|
73
74
|
return publishBatchWithRetry(streams, events, options);
|
|
74
75
|
}
|
|
75
|
-
exports.publishBatch = publishBatch;
|
|
76
76
|
function publishScheduledBatch(streams, events, options) {
|
|
77
77
|
return publishBatchWithRetry(streams, events, options);
|
|
78
78
|
}
|
|
79
|
-
exports.publishScheduledBatch = publishScheduledBatch;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.RedisRegistry = void 0;
|
|
4
|
+
exports.setRedisConnectionSettings = setRedisConnectionSettings;
|
|
4
5
|
const ioredis_1 = require("ioredis");
|
|
5
6
|
const logger_1 = require("./logger");
|
|
6
7
|
class RedisRegistry {
|
|
@@ -103,4 +104,3 @@ RedisRegistry.options = {
|
|
|
103
104
|
function setRedisConnectionSettings(options) {
|
|
104
105
|
RedisRegistry.setOptions(options);
|
|
105
106
|
}
|
|
106
|
-
exports.setRedisConnectionSettings = setRedisConnectionSettings;
|
|
@@ -200,6 +200,10 @@ export declare class Streams {
|
|
|
200
200
|
* circuit is OPEN
|
|
201
201
|
*/
|
|
202
202
|
processStoredEvents(): Promise<void>;
|
|
203
|
+
/**
|
|
204
|
+
* Acknowledges a message and updates the last acknowledged message ID.
|
|
205
|
+
* This is used to track cleanup progress and ensure we don't delete unprocessed messages.
|
|
206
|
+
*/
|
|
203
207
|
acknowledgeMessage(ackKey: string): Promise<void>;
|
|
204
208
|
private frameMessageKey;
|
|
205
209
|
private demergeMessageKey;
|
package/src/lib/redis/streams.js
CHANGED
|
@@ -257,131 +257,151 @@ class Streams {
|
|
|
257
257
|
const setKeyForK8sHandling = `instance:${this.instanceUniqueId}:consumerGroupName`;
|
|
258
258
|
this.eventsListened.push(eventName);
|
|
259
259
|
try {
|
|
260
|
-
//
|
|
261
|
-
|
|
260
|
+
// Try to create the consumer group and consumer in one go
|
|
261
|
+
// If group doesn't exist, this will create it with MKSTREAM
|
|
262
|
+
// If group exists but consumer doesn't, this will create just the consumer
|
|
263
|
+
// If both exist, this will be a no-op
|
|
262
264
|
try {
|
|
263
|
-
groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName));
|
|
264
|
-
}
|
|
265
|
-
catch (e) {
|
|
266
|
-
// Do nothing
|
|
267
|
-
}
|
|
268
|
-
let groupExists = false;
|
|
269
|
-
for (const group of groupInfo) {
|
|
270
|
-
if (group[1] === this.consumerGroupName) {
|
|
271
|
-
groupExists = true;
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
if (!groupExists) {
|
|
276
265
|
await this.redisGroups.xgroup('CREATE', streamName, this.consumerGroupName, '0', 'MKSTREAM');
|
|
277
266
|
logger_1.PUBLISHER_LOGGER.log(`Group created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
|
|
278
267
|
}
|
|
279
|
-
|
|
268
|
+
catch (e) {
|
|
269
|
+
// BUSYGROUP error means group already exists, which is fine
|
|
270
|
+
if (!e.message.includes('BUSYGROUP')) {
|
|
271
|
+
throw e;
|
|
272
|
+
}
|
|
280
273
|
logger_1.PUBLISHER_LOGGER.log(`Group already exists for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
|
|
281
274
|
}
|
|
275
|
+
// Create consumer (idempotent operation)
|
|
276
|
+
const createConsumerStatus = await this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId);
|
|
277
|
+
// Register event and consumer group in parallel
|
|
278
|
+
const [addToCGSet, addToFlushSet] = await Promise.all([
|
|
279
|
+
this.redisGroups.sadd(`${eventName}`, this.consumerGroupName),
|
|
280
|
+
this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName),
|
|
281
|
+
this.redisGroups.sadd(key, eventName),
|
|
282
|
+
]);
|
|
283
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Consumer Registered with ${this.instanceId} under ${this.consumerGroupName} with status ${JSON.stringify({
|
|
284
|
+
createConsumerStatus,
|
|
285
|
+
addToCGSet,
|
|
286
|
+
addToFlushSet,
|
|
287
|
+
})}`);
|
|
288
|
+
return true;
|
|
282
289
|
}
|
|
283
|
-
catch (
|
|
284
|
-
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER:
|
|
285
|
-
|
|
286
|
-
// Check if the consumer already exists in the group
|
|
287
|
-
let consumers = [];
|
|
288
|
-
try {
|
|
289
|
-
consumers = (await this.redisGroups.xinfo('CONSUMERS', streamName, this.consumerGroupName));
|
|
290
|
-
}
|
|
291
|
-
catch (e) {
|
|
292
|
-
// Do nothing
|
|
293
|
-
}
|
|
294
|
-
let consumerExists = false;
|
|
295
|
-
for (const consumer of consumers) {
|
|
296
|
-
if (consumer[1] === this.instanceId) {
|
|
297
|
-
consumerExists = true;
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
let createConsumerStatus;
|
|
302
|
-
if (!consumerExists) {
|
|
303
|
-
createConsumerStatus = await this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId);
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
createConsumerStatus = 0; // Consumer already exists
|
|
307
|
-
logger_1.PUBLISHER_LOGGER.log(`Consumer already exists for ${JSON.stringify({ streamName, cgn: this.consumerGroupName, instanceId: this.instanceId })}`);
|
|
290
|
+
catch (error) {
|
|
291
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Consumer registration failed for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}:`, error);
|
|
292
|
+
return false;
|
|
308
293
|
}
|
|
309
|
-
await this.redisGroups.sadd(key, eventName);
|
|
310
|
-
const addToCGSet = await this.redisGroups.sadd(`${eventName}`, this.consumerGroupName);
|
|
311
|
-
const addToFlushSet = await this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName);
|
|
312
|
-
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Consumer Registered and created with ${this.instanceId} under ${this.consumerGroupName} with ${createConsumerStatus} consumers and with the following status ${JSON.stringify({ addToCGSet, addToFlushSet })}`);
|
|
313
|
-
return createConsumerStatus === 0 || createConsumerStatus === 1;
|
|
314
294
|
}
|
|
315
295
|
listenInternals(eventName, subscriptionId, eventFilter, filterKeepAlive = 24 * 60 * 60 * 1000, publishOnceGuarantee = false, externalAcknowledgement = false) {
|
|
316
|
-
|
|
317
|
-
|
|
296
|
+
// Get or create subscription map for this event
|
|
297
|
+
const eventSubscriptions = this.subscriptions.get(eventName) || new Map();
|
|
298
|
+
const isNewSubscription = !this.subscriptions.has(eventName);
|
|
299
|
+
if (isNewSubscription) {
|
|
300
|
+
this.subscriptions.set(eventName, eventSubscriptions);
|
|
318
301
|
}
|
|
319
302
|
const bs = new rxjs_1.BehaviorSubject(null);
|
|
320
|
-
|
|
303
|
+
// Making the subscription Immutable
|
|
304
|
+
const subscription = Object.freeze({
|
|
321
305
|
subject: bs,
|
|
322
306
|
filter: eventFilter,
|
|
323
307
|
lastMatchTime: Date.now(),
|
|
324
308
|
keepAlive: filterKeepAlive,
|
|
325
|
-
};
|
|
326
|
-
this.subscriptions.get(eventName).set(subscriptionId, subscription);
|
|
327
|
-
const timer = (0, rxjs_1.interval)(10000).subscribe(async () => {
|
|
328
|
-
/** Clear earlier unprocessed messages. Runs every 10 seconds */
|
|
329
|
-
await processMessage(this.redisGroups, '0', new tracker_1.MetricsTracker(), false);
|
|
330
309
|
});
|
|
310
|
+
eventSubscriptions.set(subscriptionId, subscription);
|
|
311
|
+
// Return early if not first subscription
|
|
312
|
+
if (!isNewSubscription) {
|
|
313
|
+
return bs.asObservable().pipe((0, rxjs_1.skip)(1));
|
|
314
|
+
}
|
|
315
|
+
const cleanupInterval = 10000; // 10 seconds
|
|
316
|
+
const timer = (0, rxjs_1.interval)(cleanupInterval).subscribe({
|
|
317
|
+
next: async () => {
|
|
318
|
+
try {
|
|
319
|
+
await processMessage(this.redisGroups, '0', new tracker_1.MetricsTracker(), false);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
logger_1.PUBLISHER_LOGGER.error('Error in running recurring cleanup task:', error);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
error: (error) => {
|
|
326
|
+
logger_1.PUBLISHER_LOGGER.error('Fatal error in cleanup timer:', error);
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
// Create observable with proper cleanup
|
|
331
330
|
const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1), (0, rxjs_1.finalize)(() => {
|
|
332
|
-
/** Cleanup timer */
|
|
333
331
|
timer.unsubscribe();
|
|
332
|
+
// Clean up subscription on completion
|
|
333
|
+
this.removeSubscription(eventName, subscriptionId);
|
|
334
334
|
}));
|
|
335
|
-
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
336
335
|
const processMessage = async (redisClient, messageId, tracker, multicast = false, processPending = false) => {
|
|
336
|
+
// Skip processing if subscription was removed. This is needed because the processing is independent of the subscription
|
|
337
|
+
if (!this.subscriptions.has(eventName)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
337
341
|
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Processing message ${messageId} for ${streamName}`);
|
|
338
342
|
try {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
343
|
+
// Skip XPENDING check for:
|
|
344
|
+
// 1. Multicast messages (meant for all consumers)
|
|
345
|
+
// 2. Initial message processing (messageId = '0')
|
|
346
|
+
// 3. Pending message reprocessing
|
|
347
|
+
if (!multicast && messageId !== '0' && !processPending) {
|
|
348
|
+
try {
|
|
349
|
+
const pendingDetails = await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1);
|
|
350
|
+
if (pendingDetails[2] === 0) {
|
|
351
|
+
logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: Message ${messageId} already processed for ${streamName}`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
// If XPENDING fails, continue with processing
|
|
357
|
+
logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: XPENDING check failed for ${messageId}, continuing with processing`);
|
|
348
358
|
}
|
|
349
|
-
}
|
|
350
|
-
catch (e) {
|
|
351
|
-
// Ignore the xpending error and continue
|
|
352
|
-
logger_1.PUBLISHER_LOGGER.error('XPENDING ERROR: To be handled');
|
|
353
|
-
logger_1.PUBLISHER_LOGGER.warn(JSON.stringify(e));
|
|
354
359
|
}
|
|
355
360
|
let eventData;
|
|
356
|
-
/**
|
|
357
|
-
* Both multicast messages and pending messages cannot be read by xreadgroup
|
|
358
|
-
* Multicast messages should not be claimed by a single consumer. And pending messages
|
|
359
|
-
* are usually behind in the stream so XREADGROUP will not read them and hence
|
|
360
|
-
* they need to be read using XRANGE.
|
|
361
|
-
*/
|
|
362
361
|
tracker.startRedisOperation();
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
362
|
+
try {
|
|
363
|
+
/**
|
|
364
|
+
* Both multicast messages and pending messages cannot be read by xreadgroup
|
|
365
|
+
* Multicast messages should not be claimed by a single consumer. And pending messages
|
|
366
|
+
* are usually behind in the stream so XREADGROUP will not read them and hence
|
|
367
|
+
* they need to be read using XRANGE.
|
|
368
|
+
*/
|
|
369
|
+
if (multicast || processPending) {
|
|
370
|
+
const messages = await redisClient.xrange(streamName, messageId, messageId);
|
|
371
|
+
if (messages?.length) {
|
|
372
|
+
try {
|
|
373
|
+
eventData = JSON.parse(messages[0][1][1]);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
logger_1.PUBLISHER_LOGGER.error(`JSON parsing failed for message: ${messages[0][1][1]}`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
371
379
|
}
|
|
372
380
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
else {
|
|
382
|
+
const messages = (await redisClient.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'STREAMS', streamName, '>'));
|
|
383
|
+
if (messages?.length) {
|
|
384
|
+
if (messageId === '0') {
|
|
385
|
+
messageId = messages[0][1][0][0];
|
|
386
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Reprocessing unprocessed message with id: ${messageId}`);
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
eventData = JSON.parse(messages[0][1][0][1][1]);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
logger_1.PUBLISHER_LOGGER.error(`JSON parsing failed for message: ${messages[0][1][0][1][1]}`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
380
395
|
}
|
|
381
|
-
eventData = JSON.parse(messages[0][1][0][1][1]);
|
|
382
396
|
}
|
|
383
397
|
}
|
|
384
|
-
|
|
398
|
+
catch (error) {
|
|
399
|
+
logger_1.PUBLISHER_LOGGER.error('Error retrieving or parsing event data:', error);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
tracker.endRedisOperation();
|
|
404
|
+
}
|
|
385
405
|
tracker.startProcessing();
|
|
386
406
|
if (eventData) {
|
|
387
407
|
if (publishOnceGuarantee) {
|
|
@@ -396,61 +416,68 @@ class Streams {
|
|
|
396
416
|
const ackKey = this.frameMessageKey(streamName, messageId);
|
|
397
417
|
const subscriptions = this.subscriptions.get(eventName);
|
|
398
418
|
if (subscriptions) {
|
|
419
|
+
const currentTime = Date.now();
|
|
399
420
|
const subscriptionEntries = Array.from(subscriptions.entries());
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
sub.
|
|
404
|
-
|
|
421
|
+
// Process subscriptions in parallel for better performance
|
|
422
|
+
await Promise.all(subscriptionEntries.map(async ([subId, sub]) => {
|
|
423
|
+
try {
|
|
424
|
+
if (!sub.filter || sub.filter(eventData)) {
|
|
425
|
+
sub.subject.next({ ...eventData, ackKey });
|
|
426
|
+
sub.lastMatchTime = currentTime;
|
|
427
|
+
}
|
|
428
|
+
else if (currentTime - sub.lastMatchTime > sub.keepAlive) {
|
|
429
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: No matching events for ${eventName} (Subscription ${subId}) in the last ${sub.keepAlive / 1000 / 60 / 60} hours`);
|
|
430
|
+
sub.lastMatchTime = currentTime;
|
|
431
|
+
}
|
|
405
432
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
* log the data. Should add functionality to remove the filter
|
|
410
|
-
* if its not used at all to gain minor improvements in
|
|
411
|
-
* performace
|
|
412
|
-
*/
|
|
413
|
-
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: No matching events for ${eventName} (Subscription ${subId}) in the last ${sub.keepAlive / 1000 / 60 / 60} hours`);
|
|
414
|
-
sub.lastMatchTime = Date.now();
|
|
433
|
+
catch (error) {
|
|
434
|
+
// Log error but don't fail entire processing
|
|
435
|
+
logger_1.PUBLISHER_LOGGER.error(`Error processing subscription ${subId}:`, error);
|
|
415
436
|
}
|
|
416
|
-
}
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
// Acknowledge message if needed
|
|
440
|
+
if (!externalAcknowledgement) {
|
|
441
|
+
await this.acknowledgeMessage(ackKey);
|
|
417
442
|
}
|
|
418
|
-
|
|
419
|
-
|
|
443
|
+
// Update metrics
|
|
444
|
+
const currentTime = Date.now();
|
|
445
|
+
tracker.incrementMessageRate('subscribe', eventData.eventName);
|
|
446
|
+
const processingTime = currentTime - eventData.createdAt;
|
|
447
|
+
tracker.addProcessingTime(processingTime);
|
|
448
|
+
tracker.setConsumerLag(this.consumerGroupName, currentTime - eventData.createdAt);
|
|
420
449
|
}
|
|
421
|
-
catch (
|
|
422
|
-
logger_1.PUBLISHER_LOGGER.error(`Processing error for message ${messageId}:`,
|
|
450
|
+
catch (error) {
|
|
451
|
+
logger_1.PUBLISHER_LOGGER.error(`Processing error for message ${messageId}:`, error);
|
|
423
452
|
const dlqEvent = {
|
|
424
453
|
...eventData,
|
|
425
|
-
failureReason:
|
|
454
|
+
failureReason: error.message,
|
|
426
455
|
retryCount: (eventData.retryCount || 0) + 1,
|
|
427
456
|
originalStream: streamName,
|
|
428
457
|
consumerGroupName: this.consumerGroupName,
|
|
429
458
|
timestamp: Date.now(),
|
|
430
459
|
};
|
|
431
460
|
await this.dlq.addToDLQ(dlqEvent);
|
|
461
|
+
// Don't rethrow to prevent message loss
|
|
432
462
|
}
|
|
433
|
-
tracker.incrementMessageRate('subscribe', eventData.eventName);
|
|
434
|
-
const processingTime = Date.now() - eventData.createdAt;
|
|
435
|
-
tracker.addProcessingTime(processingTime);
|
|
436
|
-
const lag = Date.now() - eventData.createdAt;
|
|
437
|
-
tracker.setConsumerLag(this.consumerGroupName, lag);
|
|
438
463
|
}
|
|
439
464
|
else {
|
|
440
465
|
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
|
|
441
466
|
}
|
|
442
467
|
tracker.endProcessing();
|
|
443
|
-
/** Process Unprocessed
|
|
468
|
+
/** Process Unprocessed Messages with rate limiting */
|
|
444
469
|
if (!processPending) {
|
|
445
470
|
const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, this.instanceId);
|
|
446
471
|
if (unprocessedMessageIds.countOnThisConsumer &&
|
|
447
472
|
unprocessedMessageIds.countOnThisConsumer > this.config.unprocessedMessageThreshold) {
|
|
448
473
|
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
|
|
449
474
|
}
|
|
450
|
-
|
|
451
|
-
|
|
475
|
+
// Process messages with rate limiting
|
|
476
|
+
const processWithDelay = async (id, index) => {
|
|
477
|
+
await new Promise((resolve) => setTimeout(resolve, index * 20));
|
|
452
478
|
await processMessage(redisClient, id, new tracker_1.MetricsTracker(), multicast, true);
|
|
453
|
-
}
|
|
479
|
+
};
|
|
480
|
+
unprocessedMessageIds.messageIds.map((id, index) => processWithDelay(id, index));
|
|
454
481
|
}
|
|
455
482
|
}
|
|
456
483
|
catch (e) {
|
|
@@ -550,17 +577,30 @@ class Streams {
|
|
|
550
577
|
}
|
|
551
578
|
async cleanupAcknowledgedMessages(eventName, interval = this.config.acknowledgedMessageCleanupInterval) {
|
|
552
579
|
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
553
|
-
const
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
//
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
580
|
+
const lastAckKey = `last_ack:${streamName}`;
|
|
581
|
+
const oneHourAgo = Date.now() - interval;
|
|
582
|
+
try {
|
|
583
|
+
// Get consumer group info to check if consumers are active
|
|
584
|
+
const groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName));
|
|
585
|
+
// If no active consumers, leave stream as is
|
|
586
|
+
if (!groupInfo || !groupInfo.some((group) => group.consumers > 0)) {
|
|
587
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: No active consumers for ${streamName}, leaving stream as is`);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// Get last acknowledged message ID
|
|
591
|
+
const lastAckId = await this.redisGroups.get(lastAckKey);
|
|
592
|
+
if (!lastAckId) {
|
|
593
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: No acknowledged messages for ${streamName}`);
|
|
594
|
+
return;
|
|
561
595
|
}
|
|
562
|
-
//
|
|
563
|
-
|
|
596
|
+
// Extract timestamp from message ID
|
|
597
|
+
const [timestamp] = lastAckId.split('-').map(Number);
|
|
598
|
+
const cleanupThreshold = Math.min(timestamp, oneHourAgo);
|
|
599
|
+
await this.redisGroups.xtrim(streamName, 'MINID', cleanupThreshold);
|
|
600
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Cleaned up messages before last acknowledged message ${timestamp} from ${streamName}`);
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
logger_1.PUBLISHER_LOGGER.error(`Error during cleanup for ${streamName}:`, error);
|
|
564
604
|
}
|
|
565
605
|
}
|
|
566
606
|
async getDiagnosticData(events) {
|
|
@@ -673,10 +713,21 @@ class Streams {
|
|
|
673
713
|
await this.circuitBreaker.clearStoredEvents();
|
|
674
714
|
}
|
|
675
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* Acknowledges a message and updates the last acknowledged message ID.
|
|
718
|
+
* This is used to track cleanup progress and ensure we don't delete unprocessed messages.
|
|
719
|
+
*/
|
|
676
720
|
async acknowledgeMessage(ackKey) {
|
|
677
721
|
const { streamName, messageId } = this.demergeMessageKey(ackKey);
|
|
678
|
-
|
|
679
|
-
|
|
722
|
+
const lastAckKey = `last_ack:${streamName}`;
|
|
723
|
+
try {
|
|
724
|
+
// Update last acknowledged ID and acknowledge message atomically
|
|
725
|
+
await Promise.all([this.redisGroups.xack(streamName, this.consumerGroupName, messageId), this.redisGroups.set(lastAckKey, messageId)]);
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
logger_1.PUBLISHER_LOGGER.error(`Error acknowledging message ${messageId} for ${streamName}:`, error);
|
|
729
|
+
throw error;
|
|
730
|
+
}
|
|
680
731
|
}
|
|
681
732
|
frameMessageKey(streamName, messageId) {
|
|
682
733
|
return `${streamName}##${messageId}`;
|
package/src/lib/redis/utils.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.UTILS =
|
|
3
|
+
exports.UTILS = void 0;
|
|
4
|
+
exports.getAllConsumerGroups = getAllConsumerGroups;
|
|
5
|
+
exports.getSummaryOnStreamConsumerGroup = getSummaryOnStreamConsumerGroup;
|
|
6
|
+
exports.getUnacknowledgedMessages = getUnacknowledgedMessages;
|
|
7
|
+
exports.getMessageStatesCount = getMessageStatesCount;
|
|
8
|
+
exports.notifySubscribers = notifySubscribers;
|
|
9
|
+
exports.removedScheduledJob = removedScheduledJob;
|
|
10
|
+
exports.encodeScheduledMessage = encodeScheduledMessage;
|
|
11
|
+
exports.decodeScheduledMessage = decodeScheduledMessage;
|
|
4
12
|
const logger_1 = require("./logger");
|
|
5
13
|
async function getAllConsumerGroups(eventName, redisConnection) {
|
|
6
14
|
const consumerGroups = await redisConnection.smembers(`${eventName}`);
|
|
7
15
|
return consumerGroups;
|
|
8
16
|
}
|
|
9
|
-
exports.getAllConsumerGroups = getAllConsumerGroups;
|
|
10
17
|
function* getSummaryOnStreamConsumerGroup(redisClient, consumerGroupName, streamName) {
|
|
11
18
|
const [count, , , consumers] = (yield redisClient.xpending(streamName, consumerGroupName));
|
|
12
19
|
yield {
|
|
@@ -21,7 +28,6 @@ function* getSummaryOnStreamConsumerGroup(redisClient, consumerGroupName, stream
|
|
|
21
28
|
: [],
|
|
22
29
|
};
|
|
23
30
|
}
|
|
24
|
-
exports.getSummaryOnStreamConsumerGroup = getSummaryOnStreamConsumerGroup;
|
|
25
31
|
async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, consumerName, count = 500) {
|
|
26
32
|
try {
|
|
27
33
|
// Get pending messages summary
|
|
@@ -32,12 +38,16 @@ async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamN
|
|
|
32
38
|
}
|
|
33
39
|
// Use the smallest and largest IDs to get a detailed range
|
|
34
40
|
const pendingMessageCount = summary[0];
|
|
35
|
-
//
|
|
36
|
-
|
|
41
|
+
// Create a message ID for 2 seconds ago to exclude recent messages
|
|
42
|
+
const oneSecondAgo = Date.now() - 2000;
|
|
43
|
+
const minId = '0-0'; // Start from beginning
|
|
44
|
+
const maxId = `${oneSecondAgo}-0`; // Only include messages older than 1 second
|
|
45
|
+
// Get detailed information in the range, excluding recent messages
|
|
46
|
+
let pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, minId, maxId, count, consumerName));
|
|
37
47
|
/** If no pending messages on consumer, fetch messages from other consumers that haven't been claimed for more than 10s */
|
|
38
48
|
if (count > pendingMessages.length && pendingMessages.length === 0) {
|
|
39
49
|
await redisClient.xautoclaim(streamName, consumerGroupName, consumerName, 10000, '0-0', 'COUNT', 100);
|
|
40
|
-
pendingMessages = (await redisClient.xpending(streamName, consumerGroupName,
|
|
50
|
+
pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, minId, maxId, count, consumerName));
|
|
41
51
|
}
|
|
42
52
|
return {
|
|
43
53
|
count: pendingMessageCount,
|
|
@@ -51,7 +61,6 @@ async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamN
|
|
|
51
61
|
return { count: 0, messageIds: [] };
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
|
-
exports.getUnacknowledgedMessages = getUnacknowledgedMessages;
|
|
55
64
|
async function getMessageStatesCount(redisClient, streamName, consumerGroup) {
|
|
56
65
|
try {
|
|
57
66
|
const pendingInfo = (await redisClient.xpending(streamName, consumerGroup));
|
|
@@ -66,11 +75,9 @@ async function getMessageStatesCount(redisClient, streamName, consumerGroup) {
|
|
|
66
75
|
return { acknowledged: 0, unacknowledged: 0 };
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
|
-
exports.getMessageStatesCount = getMessageStatesCount;
|
|
70
78
|
async function notifySubscribers(redisClient, eventName, messageId, multicast = false) {
|
|
71
79
|
await redisClient.publish(eventName, JSON.stringify({ messageId, multicast }));
|
|
72
80
|
}
|
|
73
|
-
exports.notifySubscribers = notifySubscribers;
|
|
74
81
|
async function removedScheduledJob(redisClient, eventString) {
|
|
75
82
|
const currentTime = new Date().getTime();
|
|
76
83
|
const events = await redisClient.zrangebyscore('se', 0, currentTime);
|
|
@@ -79,7 +86,6 @@ async function removedScheduledJob(redisClient, eventString) {
|
|
|
79
86
|
const eventsLater = await redisClient.zrangebyscore('se', 0, currentTime);
|
|
80
87
|
logger_1.PUBLISHER_LOGGER.log(`Total Events in scheduled queue: ${eventsLater.length}`);
|
|
81
88
|
}
|
|
82
|
-
exports.removedScheduledJob = removedScheduledJob;
|
|
83
89
|
function encodeScheduledMessage(data) {
|
|
84
90
|
const eventName = data.eventName;
|
|
85
91
|
const eventData = JSON.stringify(data.data);
|
|
@@ -87,7 +93,6 @@ function encodeScheduledMessage(data) {
|
|
|
87
93
|
const eventDataBuffer = Buffer.from(eventData, 'utf8').toString('base64');
|
|
88
94
|
return `${eventName}%%${eventDataBuffer}%%${repeatInterval}`;
|
|
89
95
|
}
|
|
90
|
-
exports.encodeScheduledMessage = encodeScheduledMessage;
|
|
91
96
|
function decodeScheduledMessage(data) {
|
|
92
97
|
const parts = data.split('%%');
|
|
93
98
|
const eventName = parts[0];
|
|
@@ -97,7 +102,6 @@ function decodeScheduledMessage(data) {
|
|
|
97
102
|
repeatInterval: parseInt(parts[2]),
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
|
-
exports.decodeScheduledMessage = decodeScheduledMessage;
|
|
101
105
|
exports.UTILS = {
|
|
102
106
|
getMessageStatesCount,
|
|
103
107
|
getUnacknowledgedMessages,
|