@jetit/publisher 3.3.5 → 4.0.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 +1 -1
- package/src/lib/redis/streams.d.ts +13 -2
- package/src/lib/redis/streams.js +45 -8
- package/src/lib/redis/utils.d.ts +2 -1
- package/src/lib/redis/utils.js +10 -4
package/package.json
CHANGED
|
@@ -43,10 +43,11 @@ export declare class Streams {
|
|
|
43
43
|
* const eventData = { eventName: 'order.created', data: orderData };
|
|
44
44
|
* await streams.publish(eventData);
|
|
45
45
|
*/
|
|
46
|
-
publish<TData = unknown, TName extends string = string>(data: PublishData<TData, TName>, multicast?: boolean): Promise<
|
|
46
|
+
publish<TData = unknown, TName extends string = string>(data: PublishData<TData, TName>, multicast?: boolean): Promise<string>;
|
|
47
47
|
/**
|
|
48
48
|
* Schedules an event to be published at a specified future time. Thee event gets published if the
|
|
49
|
-
* differnece between the current time and the scheduled time is less than 500ms.
|
|
49
|
+
* differnece between the current time and the scheduled time is less than 500ms. The granularity
|
|
50
|
+
* of scheduled publish is 5 seconds. So it doesnt make sense to run anything less than the 5 secs time
|
|
50
51
|
*
|
|
51
52
|
* @param scheduledTime - The Date object representing the future time when the event should be published.
|
|
52
53
|
* @param eventData - The event data object, containing the event name and its associated data.
|
|
@@ -120,4 +121,14 @@ export declare class Streams {
|
|
|
120
121
|
*/
|
|
121
122
|
close(): Promise<void>;
|
|
122
123
|
private cleanupAcknowledgedMessages;
|
|
124
|
+
/**
|
|
125
|
+
* This function should be added to Surf Signal to publish periodic diagnostic information
|
|
126
|
+
* on the health of the stream
|
|
127
|
+
*/
|
|
128
|
+
getUnacknowledgedMessagesForStream(eventName: string): Promise<{
|
|
129
|
+
count: number;
|
|
130
|
+
countOnThisConsumer?: number | undefined;
|
|
131
|
+
messageIds: string[];
|
|
132
|
+
messages?: unknown[] | undefined;
|
|
133
|
+
}>;
|
|
123
134
|
}
|
package/src/lib/redis/streams.js
CHANGED
|
@@ -88,6 +88,7 @@ class Streams {
|
|
|
88
88
|
const publishEndTime = process.hrtime(publishStartTime);
|
|
89
89
|
const elapsedTime = publishEndTime[0] * 1000 + publishEndTime[1] / 1000000;
|
|
90
90
|
logger_1.PERFORMANCE_LOGGER.log(`PTIME;${key};${data.eventName};${Date.now()};${elapsedTime}`);
|
|
91
|
+
return key;
|
|
91
92
|
}
|
|
92
93
|
async scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0, multicast = false) {
|
|
93
94
|
const currentTime = new Date();
|
|
@@ -175,13 +176,23 @@ class Streams {
|
|
|
175
176
|
}
|
|
176
177
|
listenInternals(eventName) {
|
|
177
178
|
const bs = new rxjs_1.BehaviorSubject(null);
|
|
178
|
-
const
|
|
179
|
+
const timer = (0, rxjs_1.interval)(10000).subscribe(async () => {
|
|
180
|
+
/** Clear earlier unprocessed messages. Runs every 10 seconds */
|
|
181
|
+
await processMessage(this.redisGroups, '0', false);
|
|
182
|
+
});
|
|
183
|
+
const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1), (0, rxjs_1.finalize)(() => {
|
|
184
|
+
/** Cleanup timer */
|
|
185
|
+
timer.unsubscribe();
|
|
186
|
+
}));
|
|
179
187
|
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
180
188
|
const processMessage = async (redisClient, messageId, multicast = false, processPending = false) => {
|
|
181
189
|
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Processing message ${messageId} for ${streamName}`);
|
|
182
190
|
try {
|
|
183
191
|
try {
|
|
184
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Check if the message is already acquired by another client and is pending.
|
|
194
|
+
*/
|
|
195
|
+
const pendingDetails = await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1);
|
|
185
196
|
if (pendingDetails[2] === 0 && multicast === false) {
|
|
186
197
|
logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: MACK ${messageId} for ${streamName}`);
|
|
187
198
|
return;
|
|
@@ -192,20 +203,37 @@ class Streams {
|
|
|
192
203
|
logger_1.PUBLISHER_LOGGER.error('XPENDING ERROR: To be handled');
|
|
193
204
|
logger_1.PUBLISHER_LOGGER.warn(JSON.stringify(e));
|
|
194
205
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
let eventData;
|
|
207
|
+
/**
|
|
208
|
+
* Both multicast messages and pending messages cannot be read by xreadgroup
|
|
209
|
+
* Multicast messages should not be claimed by a single consumer. And pending messages
|
|
210
|
+
* are usually behind in the stream so XREADGROUP will not read them and hence
|
|
211
|
+
* they need to be read using XRANGE.
|
|
212
|
+
*/
|
|
213
|
+
if (multicast === true || processPending) {
|
|
214
|
+
const messages = await redisClient.xrange(streamName, messageId, messageId);
|
|
215
|
+
if (messages && messages.length) {
|
|
216
|
+
eventData = JSON.parse(messages[0][1][1]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const messages = (await redisClient.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'STREAMS', streamName, '>'));
|
|
221
|
+
if (messages && messages.length) {
|
|
222
|
+
eventData = JSON.parse(messages[0][1][0][1][1]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (eventData) {
|
|
198
226
|
bs.next(eventData);
|
|
199
227
|
await redisClient.xack(streamName, this.consumerGroupName, messageId);
|
|
200
228
|
await redisClient.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
|
|
201
229
|
}
|
|
202
230
|
else {
|
|
203
|
-
logger_1.PUBLISHER_LOGGER.
|
|
231
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
|
|
204
232
|
}
|
|
205
233
|
/** Process Unprocessed Message if this is a main tree, otherwise limit to processing 100 messages that are unacknowledged */
|
|
206
234
|
if (!processPending) {
|
|
207
|
-
const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName,
|
|
208
|
-
if (unprocessedMessageIds.
|
|
235
|
+
const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, this.instanceId);
|
|
236
|
+
if (unprocessedMessageIds.countOnThisConsumer && unprocessedMessageIds.countOnThisConsumer > 25) {
|
|
209
237
|
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
|
|
210
238
|
}
|
|
211
239
|
for (const id of unprocessedMessageIds.messageIds) {
|
|
@@ -289,5 +317,14 @@ class Streams {
|
|
|
289
317
|
await this.redisGroups.zremrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
|
|
290
318
|
}
|
|
291
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* This function should be added to Surf Signal to publish periodic diagnostic information
|
|
322
|
+
* on the health of the stream
|
|
323
|
+
*/
|
|
324
|
+
async getUnacknowledgedMessagesForStream(eventName) {
|
|
325
|
+
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
326
|
+
const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(this.redisGroups, this.consumerGroupName, streamName, this.instanceId);
|
|
327
|
+
return unprocessedMessageIds;
|
|
328
|
+
}
|
|
292
329
|
}
|
|
293
330
|
exports.Streams = Streams;
|
package/src/lib/redis/utils.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { RedisType } from './registry';
|
|
2
2
|
import { EventData } from './types';
|
|
3
3
|
export declare function getAllConsumerGroups(eventName: string, redisConnection: RedisType): Promise<string[]>;
|
|
4
|
-
export declare function getUnacknowledgedMessages(redisClient: RedisType, consumerGroupName: string, streamName: string, count?: number): Promise<{
|
|
4
|
+
export declare function getUnacknowledgedMessages(redisClient: RedisType, consumerGroupName: string, streamName: string, consumerName: string, count?: number): Promise<{
|
|
5
5
|
count: number;
|
|
6
|
+
countOnThisConsumer?: number;
|
|
6
7
|
messageIds: string[];
|
|
7
8
|
messages?: unknown[];
|
|
8
9
|
}>;
|
package/src/lib/redis/utils.js
CHANGED
|
@@ -7,20 +7,26 @@ async function getAllConsumerGroups(eventName, redisConnection) {
|
|
|
7
7
|
return consumerGroups;
|
|
8
8
|
}
|
|
9
9
|
exports.getAllConsumerGroups = getAllConsumerGroups;
|
|
10
|
-
async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, count = 500) {
|
|
10
|
+
async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, consumerName, count = 500) {
|
|
11
11
|
try {
|
|
12
12
|
// Get pending messages summary
|
|
13
13
|
const summary = await redisClient.xpending(streamName, consumerGroupName);
|
|
14
|
-
if (!summary || summary[
|
|
14
|
+
if (!summary || summary[0] === 0) {
|
|
15
15
|
// If count is zero
|
|
16
16
|
return { count: 0, messageIds: [] };
|
|
17
17
|
}
|
|
18
18
|
// Use the smallest and largest IDs to get a detailed range
|
|
19
|
-
const pendingMessageCount = summary[
|
|
19
|
+
const pendingMessageCount = summary[0];
|
|
20
20
|
// Get detailed information in the range
|
|
21
|
-
|
|
21
|
+
let pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, '-', '+', count, consumerName));
|
|
22
|
+
/** If no pending messages on consumer, fetch messages from other consumers that haven't been claimed for more than 10s */
|
|
23
|
+
if (count > pendingMessages.length && pendingMessages.length === 0) {
|
|
24
|
+
await redisClient.xautoclaim(streamName, consumerGroupName, consumerName, 10000, '0-0', 'COUNT', 100);
|
|
25
|
+
pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, '-', '+', count, consumerName));
|
|
26
|
+
}
|
|
22
27
|
return {
|
|
23
28
|
count: pendingMessageCount,
|
|
29
|
+
countOnThisConsumer: pendingMessages.length,
|
|
24
30
|
messageIds: pendingMessages.map((message) => message[0]),
|
|
25
31
|
messages: pendingMessages,
|
|
26
32
|
};
|