@jetit/publisher 5.0.0 → 5.2.1
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/README.md +35 -1
- package/package.json +1 -1
- package/src/lib/monitoring/adapters/prom.d.ts +5 -0
- package/src/lib/monitoring/adapters/prom.js +53 -0
- package/src/lib/monitoring/collector.d.ts +5 -0
- package/src/lib/monitoring/collector.js +57 -3
- package/src/lib/monitoring/tracker.d.ts +5 -0
- package/src/lib/monitoring/tracker.js +47 -0
- package/src/lib/monitoring/types.d.ts +22 -1
- package/src/lib/redis/duplication.js +3 -1
- package/src/lib/redis/registry.js +8 -1
- package/src/lib/redis/scheduler.js +1 -1
- package/src/lib/redis/streams.d.ts +3 -0
- package/src/lib/redis/streams.js +45 -10
- package/src/lib/redis/types.d.ts +7 -1
package/README.md
CHANGED
|
@@ -116,11 +116,45 @@ await publisher.publish(eventData);
|
|
|
116
116
|
### Subscribing to Events
|
|
117
117
|
|
|
118
118
|
```typescript
|
|
119
|
+
// Basic subscription with automatic acknowledgment
|
|
119
120
|
publisher.listen('user-registered').subscribe(event => {
|
|
120
121
|
console.log('New user registered:', event.data);
|
|
121
122
|
});
|
|
123
|
+
|
|
124
|
+
// Subscription with external acknowledgment
|
|
125
|
+
const options = {
|
|
126
|
+
externalAcknowledgement: true
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
publisher.listen('user-registered', options).subscribe(async event => {
|
|
130
|
+
try {
|
|
131
|
+
console.log('New user registered:', event.data);
|
|
132
|
+
// Process the event
|
|
133
|
+
await processUserRegistration(event.data);
|
|
134
|
+
|
|
135
|
+
// Manually acknowledge the message after successful processing
|
|
136
|
+
await publisher.acknowledgeMessage(event.ackKey);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Handle error - message will not be acknowledged and will be reprocessed
|
|
139
|
+
console.error('Failed to process user registration:', error);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
122
142
|
```
|
|
123
143
|
|
|
144
|
+
The `externalAcknowledgement` option allows you to manually control when messages are acknowledged. This is useful when:
|
|
145
|
+
- You need to ensure message processing is complete before acknowledgment
|
|
146
|
+
- You want to implement custom retry logic
|
|
147
|
+
- You need to coordinate acknowledgment with other operations
|
|
148
|
+
- You want to implement transaction-like behavior
|
|
149
|
+
|
|
150
|
+
When `externalAcknowledgement` is set to `true`:
|
|
151
|
+
1. Messages won't be automatically acknowledged after delivery
|
|
152
|
+
2. Each message contains an `ackKey` that must be used to acknowledge it
|
|
153
|
+
3. Unacknowledged messages will be redelivered to other consumers
|
|
154
|
+
4. You must explicitly call `acknowledgeMessage(event.ackKey)` after successful processing
|
|
155
|
+
|
|
156
|
+
**Note:** Be careful with external acknowledgment as failing to acknowledge messages can lead to message redelivery and potential duplicate processing.
|
|
157
|
+
|
|
124
158
|
### Scheduled Publishing
|
|
125
159
|
|
|
126
160
|
```typescript
|
|
@@ -278,4 +312,4 @@ If you encounter issues:
|
|
|
278
312
|
2. Verify that consumer groups are correctly created
|
|
279
313
|
3. Monitor the DLQ for failed events
|
|
280
314
|
4. Review the performance metrics for any anomalies
|
|
281
|
-
5. Check the logs for detailed error messages
|
|
315
|
+
5. Check the logs for detailed error messages
|
package/package.json
CHANGED
|
@@ -21,6 +21,11 @@ export declare class PrometheusAdapter {
|
|
|
21
21
|
private subscribeErrorCount;
|
|
22
22
|
private individualQueueDepth;
|
|
23
23
|
private duplicateEventCount;
|
|
24
|
+
private messageRatePublish;
|
|
25
|
+
private messageRateSubscribe;
|
|
26
|
+
private processingTimeHistogram;
|
|
27
|
+
private redisCommandLatency;
|
|
28
|
+
private consumerLag;
|
|
24
29
|
/**
|
|
25
30
|
*
|
|
26
31
|
* @param streams [Publisher]
|
|
@@ -75,6 +75,36 @@ class PrometheusAdapter {
|
|
|
75
75
|
labelNames: ['queue_name'],
|
|
76
76
|
registers: [this.registry],
|
|
77
77
|
});
|
|
78
|
+
this.messageRatePublish = new this.promClient.Gauge({
|
|
79
|
+
name: 'message_rate_publish',
|
|
80
|
+
help: 'Number of messages published per event type',
|
|
81
|
+
labelNames: ['event_type'],
|
|
82
|
+
registers: [this.registry],
|
|
83
|
+
});
|
|
84
|
+
this.messageRateSubscribe = new this.promClient.Gauge({
|
|
85
|
+
name: 'message_rate_subscribe',
|
|
86
|
+
help: 'Number of messages subscribed per event type',
|
|
87
|
+
labelNames: ['event_type'],
|
|
88
|
+
registers: [this.registry],
|
|
89
|
+
});
|
|
90
|
+
this.processingTimeHistogram = new this.promClient.Histogram({
|
|
91
|
+
name: 'processing_time_histogram',
|
|
92
|
+
help: 'Distribution of event processing times',
|
|
93
|
+
buckets: [0, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000],
|
|
94
|
+
registers: [this.registry],
|
|
95
|
+
});
|
|
96
|
+
this.redisCommandLatency = new this.promClient.Gauge({
|
|
97
|
+
name: 'redis_command_latency',
|
|
98
|
+
help: 'Latency of Redis commands',
|
|
99
|
+
labelNames: ['command'],
|
|
100
|
+
registers: [this.registry],
|
|
101
|
+
});
|
|
102
|
+
this.consumerLag = new this.promClient.Gauge({
|
|
103
|
+
name: 'consumer_lag',
|
|
104
|
+
help: 'Lag of consumers in milliseconds',
|
|
105
|
+
labelNames: ['consumer_group'],
|
|
106
|
+
registers: [this.registry],
|
|
107
|
+
});
|
|
78
108
|
this.registry.registerMetric(this.queueDepth);
|
|
79
109
|
this.registry.registerMetric(this.dlqSize);
|
|
80
110
|
this.registry.registerMetric(this.dlqRate);
|
|
@@ -87,6 +117,11 @@ class PrometheusAdapter {
|
|
|
87
117
|
this.registry.registerMetric(this.subscribeErrorCount);
|
|
88
118
|
this.registry.registerMetric(this.individualQueueDepth);
|
|
89
119
|
this.registry.registerMetric(this.duplicateEventCount);
|
|
120
|
+
this.registry.registerMetric(this.messageRatePublish);
|
|
121
|
+
this.registry.registerMetric(this.messageRateSubscribe);
|
|
122
|
+
this.registry.registerMetric(this.processingTimeHistogram);
|
|
123
|
+
this.registry.registerMetric(this.redisCommandLatency);
|
|
124
|
+
this.registry.registerMetric(this.consumerLag);
|
|
90
125
|
}
|
|
91
126
|
async updateMetrics() {
|
|
92
127
|
const metrics = await this.streams.getLatestMetrics();
|
|
@@ -111,6 +146,24 @@ class PrometheusAdapter {
|
|
|
111
146
|
this.individualQueueDepth.set({ queue_name: queueName }, depth);
|
|
112
147
|
});
|
|
113
148
|
}
|
|
149
|
+
Object.entries(metrics.messageRatePublish).forEach(([eventType, rate]) => {
|
|
150
|
+
this.messageRatePublish.set({ event_type: eventType }, rate);
|
|
151
|
+
});
|
|
152
|
+
Object.entries(metrics.messageRateSubscribe).forEach(([eventType, rate]) => {
|
|
153
|
+
this.messageRateSubscribe.set({ event_type: eventType }, rate);
|
|
154
|
+
});
|
|
155
|
+
Object.entries(metrics.processingTimeDistribution).forEach(([bucket, count]) => {
|
|
156
|
+
const [lower, upper] = bucket.split('-').map(Number);
|
|
157
|
+
for (let i = 0; i < count; i++) {
|
|
158
|
+
this.processingTimeHistogram.observe(upper || lower);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
Object.entries(metrics.redisCommandLatencies).forEach(([command, latency]) => {
|
|
162
|
+
this.redisCommandLatency.set({ command }, latency);
|
|
163
|
+
});
|
|
164
|
+
Object.entries(metrics.consumerLag).forEach(([consumerGroup, lag]) => {
|
|
165
|
+
this.consumerLag.set({ consumer_group: consumerGroup }, lag);
|
|
166
|
+
});
|
|
114
167
|
}
|
|
115
168
|
/**
|
|
116
169
|
* @param app This needs to be an instance of express or fastify something that supports the express api
|
|
@@ -10,6 +10,11 @@ export declare class MetricsCollector {
|
|
|
10
10
|
constructor(config: IMetricsCollectorConfig, dlq: DeadLetterQueue);
|
|
11
11
|
addMetrics(metrics: IPerformanceMetrics): void;
|
|
12
12
|
private aggregateAndStoreMetrics;
|
|
13
|
+
private aggregateMessageRates;
|
|
14
|
+
private aggregateHistogram;
|
|
15
|
+
private aggregateRedisCommandLatencies;
|
|
16
|
+
private aggregateConsumerLag;
|
|
17
|
+
private calculateAverages;
|
|
13
18
|
private getQueueDepth;
|
|
14
19
|
private storeMetrics;
|
|
15
20
|
getMetrics(startTime: number, endTime: number): Promise<IAggregatedMetrics[]>;
|
|
@@ -32,6 +32,11 @@ class MetricsCollector {
|
|
|
32
32
|
dlqRate: 0,
|
|
33
33
|
operationCount: this.metrics.length,
|
|
34
34
|
duplicateEventsCount: 0,
|
|
35
|
+
messageRatePublish: {},
|
|
36
|
+
messageRateSubscribe: {},
|
|
37
|
+
processingTimeDistribution: {},
|
|
38
|
+
redisCommandLatencies: {},
|
|
39
|
+
consumerLag: {},
|
|
35
40
|
};
|
|
36
41
|
for (const metric of this.metrics) {
|
|
37
42
|
aggregated.totalTime += metric.totalTime;
|
|
@@ -41,6 +46,11 @@ class MetricsCollector {
|
|
|
41
46
|
aggregated.publishErrorCount += metric.publishErrorCount;
|
|
42
47
|
aggregated.subscribeErrorCount += metric.subscribeErrorCount;
|
|
43
48
|
aggregated.duplicateEventsCount += metric.duplicateEventsCount;
|
|
49
|
+
this.aggregateMessageRates(aggregated.messageRatePublish, metric.messageRatePublish);
|
|
50
|
+
this.aggregateMessageRates(aggregated.messageRateSubscribe, metric.messageRateSubscribe);
|
|
51
|
+
this.aggregateHistogram(aggregated.processingTimeDistribution, metric.processingTimeDistribution);
|
|
52
|
+
this.aggregateRedisCommandLatencies(aggregated.redisCommandLatencies, metric.redisCommandLatencies);
|
|
53
|
+
this.aggregateConsumerLag(aggregated.consumerLag, metric.consumerLag);
|
|
44
54
|
}
|
|
45
55
|
// Calculate averages for time-based metrics
|
|
46
56
|
aggregated.totalTime /= aggregated.operationCount;
|
|
@@ -49,21 +59,65 @@ class MetricsCollector {
|
|
|
49
59
|
const dlqStats = await this.dlq.getDLQStats();
|
|
50
60
|
aggregated.dlqSize = dlqStats.size;
|
|
51
61
|
aggregated.dlqRate = dlqStats.additionRate;
|
|
62
|
+
this.calculateAverages(aggregated.redisCommandLatencies);
|
|
63
|
+
this.calculateAverages(aggregated.consumerLag);
|
|
52
64
|
await this.storeMetrics(aggregated);
|
|
53
65
|
this.metrics = [];
|
|
54
66
|
}
|
|
67
|
+
aggregateMessageRates(target, source) {
|
|
68
|
+
for (const [eventType, count] of Object.entries(source)) {
|
|
69
|
+
if (!target[eventType])
|
|
70
|
+
target[eventType] = 0;
|
|
71
|
+
target[eventType] += count;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
aggregateHistogram(target, source) {
|
|
75
|
+
for (const [bucket, count] of Object.entries(source)) {
|
|
76
|
+
if (!target[bucket])
|
|
77
|
+
target[bucket] = 0;
|
|
78
|
+
target[bucket] += count;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
aggregateRedisCommandLatencies(target, source) {
|
|
82
|
+
for (const [command, { total, count }] of Object.entries(source)) {
|
|
83
|
+
if (!target[command]) {
|
|
84
|
+
target[command] = { total: 0, count: 0 };
|
|
85
|
+
}
|
|
86
|
+
target[command].total += total;
|
|
87
|
+
target[command].count += count;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
aggregateConsumerLag(target, source) {
|
|
91
|
+
for (const [consumerGroup, { total, count }] of Object.entries(source)) {
|
|
92
|
+
if (!target[consumerGroup]) {
|
|
93
|
+
target[consumerGroup] = { total: 0, count: 0 };
|
|
94
|
+
}
|
|
95
|
+
target[consumerGroup].total += total;
|
|
96
|
+
target[consumerGroup].count += count;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
calculateAverages(data) {
|
|
100
|
+
for (const key in data) {
|
|
101
|
+
if (data[key].count > 0) {
|
|
102
|
+
data[key].total = data[key].total / data[key].count;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
data[key].total = 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
55
109
|
async getQueueDepth() {
|
|
56
110
|
let totalDepth = 0;
|
|
57
111
|
const individualDepths = {};
|
|
58
112
|
let cursor = '0';
|
|
59
113
|
do {
|
|
60
|
-
const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH',
|
|
114
|
+
const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', `ack:*:cg-*`, 'COUNT', 100);
|
|
61
115
|
cursor = nextCursor;
|
|
62
116
|
if (keys.length > 0) {
|
|
63
117
|
const pipeline = this.redisClient.pipeline();
|
|
64
118
|
keys.forEach((key) => {
|
|
65
|
-
pipeline.xlen(key);
|
|
66
|
-
pipeline.zcard(
|
|
119
|
+
pipeline.xlen(key.slice(4));
|
|
120
|
+
pipeline.zcard(key);
|
|
67
121
|
});
|
|
68
122
|
const results = await pipeline.exec();
|
|
69
123
|
if (!results) {
|
|
@@ -11,5 +11,10 @@ export declare class MetricsTracker {
|
|
|
11
11
|
incrementDuplicateEvent(): void;
|
|
12
12
|
incrementErrorCount(type: 'publish' | 'subscribe'): void;
|
|
13
13
|
getMetrics(): IPerformanceMetrics;
|
|
14
|
+
incrementMessageRate(type: 'publish' | 'subscribe', eventType: string): void;
|
|
15
|
+
addProcessingTime(time: number): void;
|
|
16
|
+
private getBucket;
|
|
17
|
+
addRedisCommandLatency(command: string, time: number): void;
|
|
18
|
+
setConsumerLag(consumerGroup: string, lag: number): void;
|
|
14
19
|
reset(): void;
|
|
15
20
|
}
|
|
@@ -12,6 +12,11 @@ class MetricsTracker {
|
|
|
12
12
|
publishErrorCount: 0,
|
|
13
13
|
subscribeErrorCount: 0,
|
|
14
14
|
duplicateEventsCount: 0,
|
|
15
|
+
messageRatePublish: {},
|
|
16
|
+
messageRateSubscribe: {},
|
|
17
|
+
processingTimeDistribution: {},
|
|
18
|
+
redisCommandLatencies: {},
|
|
19
|
+
consumerLag: {},
|
|
15
20
|
};
|
|
16
21
|
this.startTime = (0, node_process_1.hrtime)();
|
|
17
22
|
}
|
|
@@ -43,6 +48,43 @@ class MetricsTracker {
|
|
|
43
48
|
this.metrics.totalTime = totalSeconds * 1000 + totalNanoseconds / 1e6;
|
|
44
49
|
return { ...this.metrics };
|
|
45
50
|
}
|
|
51
|
+
incrementMessageRate(type, eventType) {
|
|
52
|
+
const key = `messageRate${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
|
53
|
+
if (!this.metrics[key][eventType]) {
|
|
54
|
+
this.metrics[key][eventType] = 0;
|
|
55
|
+
}
|
|
56
|
+
this.metrics[key][eventType]++;
|
|
57
|
+
}
|
|
58
|
+
addProcessingTime(time) {
|
|
59
|
+
const bucket = this.getBucket(time);
|
|
60
|
+
if (!this.metrics.processingTimeDistribution[bucket]) {
|
|
61
|
+
this.metrics.processingTimeDistribution[bucket] = 0;
|
|
62
|
+
}
|
|
63
|
+
this.metrics.processingTimeDistribution[bucket]++;
|
|
64
|
+
}
|
|
65
|
+
getBucket(time) {
|
|
66
|
+
const buckets = [0, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
|
|
67
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
68
|
+
if (time <= buckets[i]) {
|
|
69
|
+
return i === 0 ? `0-${buckets[i]}` : `${buckets[i - 1] + 1}-${buckets[i]}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return `>${buckets[buckets.length - 1]}`;
|
|
73
|
+
}
|
|
74
|
+
addRedisCommandLatency(command, time) {
|
|
75
|
+
if (!this.metrics.redisCommandLatencies[command]) {
|
|
76
|
+
this.metrics.redisCommandLatencies[command] = { total: 0, count: 0 };
|
|
77
|
+
}
|
|
78
|
+
this.metrics.redisCommandLatencies[command].total += time;
|
|
79
|
+
this.metrics.redisCommandLatencies[command].count++;
|
|
80
|
+
}
|
|
81
|
+
setConsumerLag(consumerGroup, lag) {
|
|
82
|
+
if (!this.metrics.consumerLag[consumerGroup]) {
|
|
83
|
+
this.metrics.consumerLag[consumerGroup] = { total: 0, count: 0 };
|
|
84
|
+
}
|
|
85
|
+
this.metrics.consumerLag[consumerGroup].total += lag;
|
|
86
|
+
this.metrics.consumerLag[consumerGroup].count++;
|
|
87
|
+
}
|
|
46
88
|
reset() {
|
|
47
89
|
this.startTime = (0, node_process_1.hrtime)();
|
|
48
90
|
this.metrics = {
|
|
@@ -53,6 +95,11 @@ class MetricsTracker {
|
|
|
53
95
|
publishErrorCount: 0,
|
|
54
96
|
subscribeErrorCount: 0,
|
|
55
97
|
duplicateEventsCount: 0,
|
|
98
|
+
messageRatePublish: {},
|
|
99
|
+
messageRateSubscribe: {},
|
|
100
|
+
processingTimeDistribution: {},
|
|
101
|
+
redisCommandLatencies: {},
|
|
102
|
+
consumerLag: {},
|
|
56
103
|
};
|
|
57
104
|
}
|
|
58
105
|
}
|
|
@@ -19,7 +19,7 @@ export interface IMetricsCollectorConfig {
|
|
|
19
19
|
collectionInterval: number;
|
|
20
20
|
retentionPeriod: number;
|
|
21
21
|
}
|
|
22
|
-
export type TQueryableMetrics = Omit<IAggregatedMetrics, 'timestamp' | 'individualQueueDepts'>;
|
|
22
|
+
export type TQueryableMetrics = Omit<IAggregatedMetrics, 'timestamp' | 'individualQueueDepts' | 'messageRatePublish' | 'messageRateSubscribe' | 'processingTimeDistribution' | 'redisCommandLatencies' | 'consumerLag'>;
|
|
23
23
|
export interface IQueueDepths {
|
|
24
24
|
total: number;
|
|
25
25
|
individual: {
|
|
@@ -34,4 +34,25 @@ export interface IPerformanceMetrics {
|
|
|
34
34
|
publishErrorCount: number;
|
|
35
35
|
subscribeErrorCount: number;
|
|
36
36
|
duplicateEventsCount: number;
|
|
37
|
+
messageRatePublish: {
|
|
38
|
+
[eventType: string]: number;
|
|
39
|
+
};
|
|
40
|
+
messageRateSubscribe: {
|
|
41
|
+
[eventType: string]: number;
|
|
42
|
+
};
|
|
43
|
+
processingTimeDistribution: {
|
|
44
|
+
[bucket: string]: number;
|
|
45
|
+
};
|
|
46
|
+
redisCommandLatencies: {
|
|
47
|
+
[command: string]: {
|
|
48
|
+
total: number;
|
|
49
|
+
count: number;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
consumerLag: {
|
|
53
|
+
[consumerGroup: string]: {
|
|
54
|
+
total: number;
|
|
55
|
+
count: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
37
58
|
}
|
|
@@ -15,7 +15,9 @@ class ContentBasedDeduplication {
|
|
|
15
15
|
return hmac.update(JSON.stringify(hashableData)).digest('hex');
|
|
16
16
|
}
|
|
17
17
|
async isDuplicate(event, consumerGroupName) {
|
|
18
|
-
|
|
18
|
+
if (event.data === null)
|
|
19
|
+
return false;
|
|
20
|
+
const eventHash = this.calculateEventHash(event);
|
|
19
21
|
const key = `processed:${consumerGroupName}:${eventHash}`;
|
|
20
22
|
const result = await this.redisClient.set(key, '1', 'EX', this.ttl, 'NX');
|
|
21
23
|
const isDuplicate = result === null;
|
|
@@ -62,11 +62,18 @@ class RedisRegistry {
|
|
|
62
62
|
},
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
else
|
|
65
|
+
else if (RedisRegistry.options.sentinels) {
|
|
66
|
+
ref = new ioredis_1.default({
|
|
67
|
+
...RedisRegistry.options.sentinels.options,
|
|
68
|
+
sentinels: RedisRegistry.options.sentinels.nodes,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
66
72
|
ref = new ioredis_1.default({
|
|
67
73
|
...RedisRegistry.options.redis,
|
|
68
74
|
db: storeRef,
|
|
69
75
|
});
|
|
76
|
+
}
|
|
70
77
|
}
|
|
71
78
|
return ref;
|
|
72
79
|
}
|
|
@@ -59,7 +59,7 @@ class ScheduledProcessor {
|
|
|
59
59
|
// Publish the event to each consumer group's stream
|
|
60
60
|
const streamName = `${eventData.eventName}:${consumerGroup}`;
|
|
61
61
|
const generatedKey = await this.redisPublisher
|
|
62
|
-
.xadd(streamName,
|
|
62
|
+
.xadd(streamName, key, 'data', JSON.stringify(eventData))
|
|
63
63
|
.catch((e) => logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Publishing event ${eventData.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(eventData)}, ${e} `));
|
|
64
64
|
if (key === '*')
|
|
65
65
|
key = generatedKey ?? key;
|
package/src/lib/redis/streams.js
CHANGED
|
@@ -53,7 +53,7 @@ class Streams {
|
|
|
53
53
|
errorThresholdPercentage: 50,
|
|
54
54
|
openStateDuration: 30000,
|
|
55
55
|
halfOpenStateMaxAttempts: 10,
|
|
56
|
-
maxStoredEvents:
|
|
56
|
+
maxStoredEvents: 10000,
|
|
57
57
|
},
|
|
58
58
|
};
|
|
59
59
|
/** Initialise Config properties */
|
|
@@ -149,6 +149,7 @@ class Streams {
|
|
|
149
149
|
const generatedKey = await this.redisPublisher.xadd(streamName, key, 'data', JSON.stringify(data));
|
|
150
150
|
tracker.endRedisOperation();
|
|
151
151
|
tracker.incrementEventCount();
|
|
152
|
+
tracker.incrementMessageRate('publish', data.eventName);
|
|
152
153
|
if (this.metricsCollector) {
|
|
153
154
|
this.metricsCollector.addMetrics(tracker.getMetrics());
|
|
154
155
|
}
|
|
@@ -232,10 +233,11 @@ class Streams {
|
|
|
232
233
|
initialDelay: this.config.initialRetryDelay,
|
|
233
234
|
filterKeepAlive: this.config.filterKeepAlive,
|
|
234
235
|
publishOnceGuarantee: false,
|
|
236
|
+
externalAcknowledgement: false,
|
|
235
237
|
...listenerOptions,
|
|
236
238
|
};
|
|
237
239
|
const subscriptionId = (0, id_1.generateID)('HEX');
|
|
238
|
-
return this.listenInternals(eventName, subscriptionId, options.eventFilter, options.filterKeepAlive, options.publishOnceGuarantee).pipe((0, rxjs_1.retry)({
|
|
240
|
+
return this.listenInternals(eventName, subscriptionId, options.eventFilter, options.filterKeepAlive, options.publishOnceGuarantee, options.externalAcknowledgement).pipe((0, rxjs_1.retry)({
|
|
239
241
|
count: options.maxRetries,
|
|
240
242
|
delay: (error, retryAttempt) => {
|
|
241
243
|
const delay = options.initialDelay * Math.pow(2, retryAttempt);
|
|
@@ -256,7 +258,13 @@ class Streams {
|
|
|
256
258
|
this.eventsListened.push(eventName);
|
|
257
259
|
try {
|
|
258
260
|
// Check if the consumer group already exists
|
|
259
|
-
|
|
261
|
+
let groupInfo = [];
|
|
262
|
+
try {
|
|
263
|
+
groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName));
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
// Do nothing
|
|
267
|
+
}
|
|
260
268
|
let groupExists = false;
|
|
261
269
|
for (const group of groupInfo) {
|
|
262
270
|
if (group[1] === this.consumerGroupName) {
|
|
@@ -276,7 +284,13 @@ class Streams {
|
|
|
276
284
|
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
|
|
277
285
|
}
|
|
278
286
|
// Check if the consumer already exists in the group
|
|
279
|
-
|
|
287
|
+
let consumers = [];
|
|
288
|
+
try {
|
|
289
|
+
consumers = (await this.redisGroups.xinfo('CONSUMERS', streamName, this.consumerGroupName));
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
// Do nothing
|
|
293
|
+
}
|
|
280
294
|
let consumerExists = false;
|
|
281
295
|
for (const consumer of consumers) {
|
|
282
296
|
if (consumer[1] === this.instanceId) {
|
|
@@ -298,7 +312,7 @@ class Streams {
|
|
|
298
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 })}`);
|
|
299
313
|
return createConsumerStatus === 0 || createConsumerStatus === 1;
|
|
300
314
|
}
|
|
301
|
-
listenInternals(eventName, subscriptionId, eventFilter, filterKeepAlive = 24 * 60 * 60 * 1000, publishOnceGuarantee = false) {
|
|
315
|
+
listenInternals(eventName, subscriptionId, eventFilter, filterKeepAlive = 24 * 60 * 60 * 1000, publishOnceGuarantee = false, externalAcknowledgement = false) {
|
|
302
316
|
if (!this.subscriptions.has(eventName)) {
|
|
303
317
|
this.subscriptions.set(eventName, new Map());
|
|
304
318
|
}
|
|
@@ -314,7 +328,6 @@ class Streams {
|
|
|
314
328
|
/** Clear earlier unprocessed messages. Runs every 10 seconds */
|
|
315
329
|
await processMessage(this.redisGroups, '0', new tracker_1.MetricsTracker(), false);
|
|
316
330
|
});
|
|
317
|
-
let lastMatchTime = Date.now();
|
|
318
331
|
const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1), (0, rxjs_1.finalize)(() => {
|
|
319
332
|
/** Cleanup timer */
|
|
320
333
|
timer.unsubscribe();
|
|
@@ -361,6 +374,10 @@ class Streams {
|
|
|
361
374
|
else {
|
|
362
375
|
const messages = (await redisClient.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'STREAMS', streamName, '>'));
|
|
363
376
|
if (messages && messages.length) {
|
|
377
|
+
if (messageId === '0') {
|
|
378
|
+
messageId = messages[0][1][0][0];
|
|
379
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Reprocessing unprocessed message with id: ${messageId}`);
|
|
380
|
+
}
|
|
364
381
|
eventData = JSON.parse(messages[0][1][0][1][1]);
|
|
365
382
|
}
|
|
366
383
|
}
|
|
@@ -376,13 +393,14 @@ class Streams {
|
|
|
376
393
|
}
|
|
377
394
|
}
|
|
378
395
|
try {
|
|
396
|
+
const ackKey = this.frameMessageKey(streamName, messageId);
|
|
379
397
|
const subscriptions = this.subscriptions.get(eventName);
|
|
380
398
|
if (subscriptions) {
|
|
381
399
|
const subscriptionEntries = Array.from(subscriptions.entries());
|
|
382
400
|
for (let i = 0; i < subscriptionEntries.length; i++) {
|
|
383
401
|
const [subId, sub] = subscriptionEntries[i];
|
|
384
402
|
if (!sub.filter || sub.filter(eventData)) {
|
|
385
|
-
sub.subject.next(eventData);
|
|
403
|
+
sub.subject.next({ ...eventData, ackKey });
|
|
386
404
|
sub.lastMatchTime = Date.now();
|
|
387
405
|
}
|
|
388
406
|
else if (Date.now() - sub.lastMatchTime > sub.keepAlive) {
|
|
@@ -397,8 +415,8 @@ class Streams {
|
|
|
397
415
|
}
|
|
398
416
|
}
|
|
399
417
|
}
|
|
400
|
-
|
|
401
|
-
|
|
418
|
+
if (!externalAcknowledgement)
|
|
419
|
+
this.acknowledgeMessage(ackKey);
|
|
402
420
|
}
|
|
403
421
|
catch (processingError) {
|
|
404
422
|
logger_1.PUBLISHER_LOGGER.error(`Processing error for message ${messageId}:`, processingError);
|
|
@@ -412,6 +430,11 @@ class Streams {
|
|
|
412
430
|
};
|
|
413
431
|
await this.dlq.addToDLQ(dlqEvent);
|
|
414
432
|
}
|
|
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);
|
|
415
438
|
}
|
|
416
439
|
else {
|
|
417
440
|
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
|
|
@@ -475,7 +498,7 @@ class Streams {
|
|
|
475
498
|
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Stream Notification Received for event ${eventName} with message ID ${messageIdRead}`);
|
|
476
499
|
await processMessage(this.redisGroups, messageIdRead, tracker, multicastRead);
|
|
477
500
|
const metrics = tracker.getMetrics();
|
|
478
|
-
logger_1.PERFORMANCE_LOGGER.log(`STIME;${messageIdRead};${
|
|
501
|
+
logger_1.PERFORMANCE_LOGGER.log(`STIME;${messageIdRead};${eventName};${Date.now()};${metrics.totalTime};${metrics.redisOperationTime};${metrics.processingTime}`);
|
|
479
502
|
if (this.metricsCollector) {
|
|
480
503
|
this.metricsCollector.addMetrics(tracker.getMetrics());
|
|
481
504
|
}
|
|
@@ -650,5 +673,17 @@ class Streams {
|
|
|
650
673
|
await this.circuitBreaker.clearStoredEvents();
|
|
651
674
|
}
|
|
652
675
|
}
|
|
676
|
+
async acknowledgeMessage(ackKey) {
|
|
677
|
+
const { streamName, messageId } = this.demergeMessageKey(ackKey);
|
|
678
|
+
await this.redisGroups.xack(streamName, this.consumerGroupName, messageId);
|
|
679
|
+
await this.redisGroups.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
|
|
680
|
+
}
|
|
681
|
+
frameMessageKey(streamName, messageId) {
|
|
682
|
+
return `${streamName}##${messageId}`;
|
|
683
|
+
}
|
|
684
|
+
demergeMessageKey(messageKey) {
|
|
685
|
+
const [streamName, messageId] = messageKey.split('##');
|
|
686
|
+
return { streamName, messageId };
|
|
687
|
+
}
|
|
653
688
|
}
|
|
654
689
|
exports.Streams = Streams;
|
package/src/lib/redis/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type EventData<TData, TName extends string> = {
|
|
|
5
5
|
createdAt?: number;
|
|
6
6
|
republishEvent?: string;
|
|
7
7
|
repeatInterval?: number;
|
|
8
|
+
ackKey?: string;
|
|
8
9
|
};
|
|
9
10
|
export type PublishData<TData, TName extends string> = {
|
|
10
11
|
data: TData;
|
|
@@ -13,7 +14,7 @@ export type PublishData<TData, TName extends string> = {
|
|
|
13
14
|
};
|
|
14
15
|
export type PendingMessages = [never, never, string, string, Array<[string, number]>];
|
|
15
16
|
export type ClaimedMessages = Array<[never, string]>;
|
|
16
|
-
import { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis';
|
|
17
|
+
import { ClusterNode, ClusterOptions, RedisOptions, SentinelAddress, SentinelConnectionOptions } from 'ioredis';
|
|
17
18
|
import { BehaviorSubject } from 'rxjs';
|
|
18
19
|
export interface IOptions {
|
|
19
20
|
redis?: RedisOptions;
|
|
@@ -21,6 +22,10 @@ export interface IOptions {
|
|
|
21
22
|
options: ClusterOptions;
|
|
22
23
|
nodes: Array<ClusterNode>;
|
|
23
24
|
};
|
|
25
|
+
sentinels?: {
|
|
26
|
+
options: SentinelConnectionOptions;
|
|
27
|
+
nodes: Array<SentinelAddress>;
|
|
28
|
+
};
|
|
24
29
|
}
|
|
25
30
|
export interface IDLQEvent extends EventData<unknown, string> {
|
|
26
31
|
eventId: string;
|
|
@@ -63,6 +68,7 @@ export interface IListenOptions<T> {
|
|
|
63
68
|
eventFilter?: TEventFilter<T>;
|
|
64
69
|
filterKeepAlive: number;
|
|
65
70
|
publishOnceGuarantee: boolean;
|
|
71
|
+
externalAcknowledgement: boolean;
|
|
66
72
|
}
|
|
67
73
|
export interface BatchPublishOptions {
|
|
68
74
|
batchSize?: number;
|