@jetit/publisher 5.6.3 → 6.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/README.md +66 -0
- package/package.json +7 -4
- package/src/lib/publisher.d.ts +1 -0
- package/src/lib/publisher.js +3 -1
- package/src/lib/redis/scheduler.js +1 -1
- package/src/lib/redis/streams-lite.d.ts +187 -0
- package/src/lib/redis/streams-lite.js +734 -0
- package/src/lib/redis/streams.d.ts +24 -0
- package/src/lib/redis/streams.js +298 -20
- package/src/lib/redis/types.d.ts +2 -0
- package/src/lib/redis/utils.js +1 -1
|
@@ -17,6 +17,9 @@ export declare class Streams {
|
|
|
17
17
|
private subscriptions;
|
|
18
18
|
private duplicateChecker;
|
|
19
19
|
private circuitBreaker;
|
|
20
|
+
private readonly findHighestStreamIdScript;
|
|
21
|
+
private findHighestStreamIdScriptSHA;
|
|
22
|
+
private optimizationActiveUntil;
|
|
20
23
|
private get redisPublisher();
|
|
21
24
|
private get redisGroups();
|
|
22
25
|
private DEFAULT_STREAMS_CONFIG;
|
|
@@ -35,6 +38,27 @@ export declare class Streams {
|
|
|
35
38
|
* const streams = new Streams('POS');
|
|
36
39
|
*/
|
|
37
40
|
constructor(serviceName: string, config?: Partial<IStreamsConfig>, redisConnectionId?: string);
|
|
41
|
+
/**
|
|
42
|
+
* Loads Lua scripts into Redis server cache
|
|
43
|
+
*/
|
|
44
|
+
private loadScripts;
|
|
45
|
+
/**
|
|
46
|
+
* Executes a cached script, falling back to EVAL if needed
|
|
47
|
+
*/
|
|
48
|
+
private executeScript;
|
|
49
|
+
/**
|
|
50
|
+
* Checks if optimization mode is active for a given event type.
|
|
51
|
+
* Cleans up expired entries during the check.
|
|
52
|
+
*/
|
|
53
|
+
private isOptimizationActive;
|
|
54
|
+
/**
|
|
55
|
+
* Activates optimization mode for a given event type for a configured duration.
|
|
56
|
+
*/
|
|
57
|
+
private activateOptimizationFor;
|
|
58
|
+
/**
|
|
59
|
+
* Deletes a specific message ID from multiple consumer group streams.
|
|
60
|
+
*/
|
|
61
|
+
private deleteMessageFromStreams;
|
|
38
62
|
private setupCircuitBreakerListeners;
|
|
39
63
|
private runClear;
|
|
40
64
|
/**
|
package/src/lib/redis/streams.js
CHANGED
|
@@ -40,6 +40,75 @@ class Streams {
|
|
|
40
40
|
this.redisConnectionId = redisConnectionId;
|
|
41
41
|
this.eventsListened = [];
|
|
42
42
|
this.subscriptions = new Map();
|
|
43
|
+
// Add these properties to store the script SHA and optimization state
|
|
44
|
+
this.findHighestStreamIdScript = `
|
|
45
|
+
-- KEYS[1]: event name
|
|
46
|
+
-- KEYS[2]: maximum streams to check
|
|
47
|
+
-- ARGV[1..n]: consumer group names
|
|
48
|
+
|
|
49
|
+
local eventName = KEYS[1]
|
|
50
|
+
local maxStreamsToCheck = tonumber(KEYS[2])
|
|
51
|
+
local highestTimestamp = 0
|
|
52
|
+
local highestSeq = 0
|
|
53
|
+
|
|
54
|
+
-- Calculate how many streams to check (all or max)
|
|
55
|
+
local streamsToCheck = math.min(#ARGV, maxStreamsToCheck)
|
|
56
|
+
|
|
57
|
+
-- If we have more streams than we want to check, use sampling
|
|
58
|
+
local streamIndices = {}
|
|
59
|
+
if streamsToCheck < #ARGV then
|
|
60
|
+
-- Deterministic sampling (every Nth stream)
|
|
61
|
+
local step = math.floor(#ARGV / streamsToCheck)
|
|
62
|
+
local offset = math.floor(step / 2) -- Start in the middle of each segment
|
|
63
|
+
|
|
64
|
+
for i=1, streamsToCheck do
|
|
65
|
+
local idx = offset + (i-1) * step
|
|
66
|
+
if idx <= #ARGV then
|
|
67
|
+
table.insert(streamIndices, idx)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
-- Ensure we also check the last stream in case it's ahead
|
|
72
|
+
if #streamIndices == 0 or streamIndices[#streamIndices] ~= #ARGV then
|
|
73
|
+
if #ARGV > 0 then table.insert(streamIndices, #ARGV) end
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
-- Check all streams
|
|
77
|
+
for i=1, #ARGV do
|
|
78
|
+
streamIndices[i] = i
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
-- Check selected streams
|
|
83
|
+
for _, i in ipairs(streamIndices) do
|
|
84
|
+
local streamName = eventName .. ":" .. ARGV[i]
|
|
85
|
+
local latestEntries = redis.call('XREVRANGE', streamName, '+', '-', 'COUNT', 1)
|
|
86
|
+
|
|
87
|
+
if #latestEntries > 0 then
|
|
88
|
+
local id = latestEntries[1][1]
|
|
89
|
+
local dashPos = string.find(id, '-')
|
|
90
|
+
|
|
91
|
+
if dashPos then
|
|
92
|
+
local timestamp = tonumber(string.sub(id, 1, dashPos - 1))
|
|
93
|
+
local seq = tonumber(string.sub(id, dashPos + 1))
|
|
94
|
+
|
|
95
|
+
if timestamp > highestTimestamp then
|
|
96
|
+
highestTimestamp = timestamp
|
|
97
|
+
highestSeq = seq
|
|
98
|
+
elseif timestamp == highestTimestamp and seq > highestSeq then
|
|
99
|
+
highestSeq = seq
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
-- Add a safety buffer to the sequence number
|
|
106
|
+
highestSeq = highestSeq + 10
|
|
107
|
+
|
|
108
|
+
return { highestTimestamp, highestSeq }
|
|
109
|
+
`;
|
|
110
|
+
this.findHighestStreamIdScriptSHA = null;
|
|
111
|
+
this.optimizationActiveUntil = new Map(); // Tracks optimization state per event
|
|
43
112
|
this.DEFAULT_STREAMS_CONFIG = {
|
|
44
113
|
immediatePublishThreshold: 500,
|
|
45
114
|
unprocessedMessageThreshold: 25,
|
|
@@ -56,6 +125,9 @@ class Streams {
|
|
|
56
125
|
halfOpenStateMaxAttempts: 10,
|
|
57
126
|
maxStoredEvents: 10000,
|
|
58
127
|
},
|
|
128
|
+
// New defaults
|
|
129
|
+
optimizationDurationMs: 2 * 60 * 1000, // 2 minutes
|
|
130
|
+
optimizationThreshold: 20, // Enable optimization for >20 consumer groups
|
|
59
131
|
};
|
|
60
132
|
/** Initialise Config properties */
|
|
61
133
|
this.config = { ...this.config, ...this.DEFAULT_STREAMS_CONFIG, ...config };
|
|
@@ -80,6 +152,120 @@ class Streams {
|
|
|
80
152
|
this.circuitBreaker = new circuit_breaker_1.CircuitBreaker(this.config.circuitBreaker, this.redisPublisher);
|
|
81
153
|
if (this.config.circuitBreaker.enabled)
|
|
82
154
|
this.setupCircuitBreakerListeners();
|
|
155
|
+
// Load scripts when Redis is available
|
|
156
|
+
this.loadScripts().catch((error) => {
|
|
157
|
+
logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Failed to load Redis scripts:', error);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Loads Lua scripts into Redis server cache
|
|
162
|
+
*/
|
|
163
|
+
async loadScripts() {
|
|
164
|
+
try {
|
|
165
|
+
// Wait until Redis connection is established (simple check)
|
|
166
|
+
if (!this._redisPublisher) {
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 100)); // Wait briefly
|
|
168
|
+
if (!this._redisPublisher) {
|
|
169
|
+
logger_1.PUBLISHER_LOGGER.warn('PUBLISHER: Redis connection not ready for script loading.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const redis = this.redisPublisher;
|
|
174
|
+
// Load the script and store its SHA
|
|
175
|
+
this.findHighestStreamIdScriptSHA = (await redis.script('LOAD', this.findHighestStreamIdScript));
|
|
176
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Successfully loaded stream ID script with SHA: ${this.findHighestStreamIdScriptSHA}`);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error loading Redis scripts:', error);
|
|
180
|
+
// We'll try again later if needed via executeScript fallback
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Executes a cached script, falling back to EVAL if needed
|
|
185
|
+
*/
|
|
186
|
+
async executeScript(script, scriptSHA, numKeys, ...args) {
|
|
187
|
+
try {
|
|
188
|
+
// If we have the SHA, try EVALSHA first
|
|
189
|
+
if (scriptSHA) {
|
|
190
|
+
try {
|
|
191
|
+
// Ensure redisPublisher is available
|
|
192
|
+
if (!this._redisPublisher)
|
|
193
|
+
throw new Error('Redis publisher connection not available');
|
|
194
|
+
return await this.redisPublisher.evalsha(scriptSHA, numKeys, ...args);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
// Add type 'any' to error
|
|
198
|
+
// If the script isn't found on the server, fall back to EVAL
|
|
199
|
+
if (error?.message && typeof error.message === 'string' && error.message.includes('NOSCRIPT')) {
|
|
200
|
+
logger_1.PUBLISHER_LOGGER.warn('PUBLISHER: Script not found on Redis server, reloading...');
|
|
201
|
+
if (!this._redisPublisher)
|
|
202
|
+
throw new Error('Redis publisher connection not available for script reload');
|
|
203
|
+
const newSHA = (await this.redisPublisher.script('LOAD', script)); // Cast result to string
|
|
204
|
+
// Update the stored SHA
|
|
205
|
+
if (script === this.findHighestStreamIdScript) {
|
|
206
|
+
this.findHighestStreamIdScriptSHA = newSHA;
|
|
207
|
+
}
|
|
208
|
+
// Retry with EVALSHA
|
|
209
|
+
return await this.redisPublisher.evalsha(newSHA, numKeys, ...args);
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// If we don't have the SHA yet, use EVAL directly
|
|
215
|
+
if (!this._redisPublisher)
|
|
216
|
+
throw new Error('Redis publisher connection not available for EVAL');
|
|
217
|
+
return await this.redisPublisher.eval(script, numKeys, ...args);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
// Add type 'any' to error
|
|
221
|
+
logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error executing Redis script:', error);
|
|
222
|
+
throw error; // Re-throw the original error
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Checks if optimization mode is active for a given event type.
|
|
227
|
+
* Cleans up expired entries during the check.
|
|
228
|
+
*/
|
|
229
|
+
isOptimizationActive(eventName) {
|
|
230
|
+
const expirationTime = this.optimizationActiveUntil.get(eventName);
|
|
231
|
+
if (!expirationTime)
|
|
232
|
+
return false;
|
|
233
|
+
if (Date.now() > expirationTime) {
|
|
234
|
+
// Clean up expired entry during publish operation
|
|
235
|
+
this.optimizationActiveUntil.delete(eventName);
|
|
236
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Deactivated optimization for ${eventName}`);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Activates optimization mode for a given event type for a configured duration.
|
|
243
|
+
*/
|
|
244
|
+
activateOptimizationFor(eventName) {
|
|
245
|
+
const durationMs = this.config.optimizationDurationMs ?? this.DEFAULT_STREAMS_CONFIG.optimizationDurationMs;
|
|
246
|
+
const expirationTime = Date.now() + durationMs;
|
|
247
|
+
this.optimizationActiveUntil.set(eventName, expirationTime);
|
|
248
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Activated optimization for ${eventName} until ${new Date(expirationTime).toISOString()}`);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Deletes a specific message ID from multiple consumer group streams.
|
|
252
|
+
*/
|
|
253
|
+
async deleteMessageFromStreams(eventName, consumerGroups, messageId) {
|
|
254
|
+
const deletionPromises = consumerGroups.map(async (consumerGroup) => {
|
|
255
|
+
const streamName = `${eventName}:${consumerGroup}`;
|
|
256
|
+
try {
|
|
257
|
+
if (!this._redisPublisher)
|
|
258
|
+
throw new Error('Redis publisher connection not available for XDEL');
|
|
259
|
+
await this.redisPublisher.xdel(streamName, messageId);
|
|
260
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Deleted message ${messageId} from ${streamName} for rollback`);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
// Log error but continue trying to delete from other streams
|
|
264
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error deleting message ${messageId} from ${streamName}:`, error);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
// Wait for all deletions to attempt completion
|
|
268
|
+
await Promise.allSettled(deletionPromises);
|
|
83
269
|
}
|
|
84
270
|
setupCircuitBreakerListeners() {
|
|
85
271
|
this.circuitBreaker.on('stateChange', async (newState) => {
|
|
@@ -141,25 +327,98 @@ class Streams {
|
|
|
141
327
|
tracker.startRedisOperation();
|
|
142
328
|
const consumerGroups = await (0, utils_1.getAllConsumerGroups)(data.eventName, this.redisPublisher);
|
|
143
329
|
tracker.endRedisOperation();
|
|
144
|
-
let key = '*';
|
|
330
|
+
let key = '*'; // Default to auto-generated ID
|
|
331
|
+
const publishedGroups = []; // Track successfully published groups for rollback
|
|
145
332
|
if (consumerGroups.length > 0) {
|
|
146
333
|
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')}`);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
334
|
+
// Check if optimization is already active or if threshold is met
|
|
335
|
+
const optimizationThreshold = this.config.optimizationThreshold ?? this.DEFAULT_STREAMS_CONFIG.optimizationThreshold;
|
|
336
|
+
const useOptimization = this.isOptimizationActive(data.eventName) || consumerGroups.length > optimizationThreshold;
|
|
337
|
+
// If optimization is needed, generate the optimized ID upfront
|
|
338
|
+
if (useOptimization) {
|
|
339
|
+
const maxStreamsToCheck = Math.max(5, Math.ceil(consumerGroups.length * 0.25));
|
|
150
340
|
tracker.startRedisOperation();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
341
|
+
try {
|
|
342
|
+
const result = (await this.executeScript(this.findHighestStreamIdScript, this.findHighestStreamIdScriptSHA, 2, // Two keys
|
|
343
|
+
data.eventName, maxStreamsToCheck.toString(), ...consumerGroups));
|
|
344
|
+
const [highestTimestamp, highestSeq] = result;
|
|
345
|
+
const currentTime = Date.now();
|
|
346
|
+
if (currentTime > highestTimestamp) {
|
|
347
|
+
key = `${currentTime}-0`;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
key = `${highestTimestamp}-${highestSeq}`;
|
|
351
|
+
}
|
|
352
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Using optimized ID generation: ${key}`);
|
|
353
|
+
}
|
|
354
|
+
catch (scriptError) {
|
|
355
|
+
logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error in optimized ID generation script, falling back to auto-generated ID:', scriptError);
|
|
356
|
+
key = '*';
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
tracker.endRedisOperation();
|
|
157
360
|
}
|
|
158
|
-
if (key === '*')
|
|
159
|
-
key = generatedKey ?? key;
|
|
160
361
|
}
|
|
161
|
-
|
|
162
|
-
|
|
362
|
+
// Publish to each consumer group
|
|
363
|
+
try {
|
|
364
|
+
for (const consumerGroup of consumerGroups) {
|
|
365
|
+
const streamName = `${data.eventName}:${consumerGroup}`;
|
|
366
|
+
tracker.startRedisOperation();
|
|
367
|
+
try {
|
|
368
|
+
const generatedKey = await this.redisPublisher.xadd(streamName, key, 'data', JSON.stringify(data));
|
|
369
|
+
tracker.endRedisOperation();
|
|
370
|
+
// If using auto-generated ID, capture it for the first stream
|
|
371
|
+
if (key === '*')
|
|
372
|
+
key = generatedKey ?? key;
|
|
373
|
+
// Track successful publish
|
|
374
|
+
publishedGroups.push(consumerGroup);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
tracker.endRedisOperation(); // Ensure tracker ends even on error
|
|
378
|
+
// Check if the error is the ID conflict error
|
|
379
|
+
if ((typeof error === 'string' && error.includes('equal or smaller than the target stream top item')) ||
|
|
380
|
+
(error?.message &&
|
|
381
|
+
typeof error.message === 'string' &&
|
|
382
|
+
error.message.includes('equal or smaller than the target stream top item'))) {
|
|
383
|
+
logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: ID conflict detected for ${streamName} (key: ${key}), switching to optimized mode for ${data.eventName} for duration: ${this.config.optimizationDurationMs ?? this.DEFAULT_STREAMS_CONFIG.optimizationDurationMs / 1000}. Consumers: ${consumerGroups.length}`);
|
|
384
|
+
// Delete already published messages if any (using the captured key)
|
|
385
|
+
if (publishedGroups.length > 0) {
|
|
386
|
+
await this.deleteMessageFromStreams(data.eventName, publishedGroups, key);
|
|
387
|
+
publishedGroups.length = 0;
|
|
388
|
+
}
|
|
389
|
+
// Activate optimization for this event type
|
|
390
|
+
this.activateOptimizationFor(data.eventName);
|
|
391
|
+
// Recursively call publish with same data (optimization will be active now)
|
|
392
|
+
// Important: Return the result of the recursive call
|
|
393
|
+
return this.publish(data, multicast);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// For other XADD errors, rethrow to trigger the outer catch block
|
|
397
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Unhandled XADD error for ${streamName} (key: ${key}): ${error?.message}`);
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Increment metrics only after successful publish
|
|
402
|
+
tracker.incrementEventCount();
|
|
403
|
+
tracker.incrementMessageRate('publish', data.eventName);
|
|
404
|
+
if (this.metricsCollector) {
|
|
405
|
+
this.metricsCollector.addMetrics(tracker.getMetrics());
|
|
406
|
+
}
|
|
407
|
+
} // End of for loop
|
|
408
|
+
// Notify subscribers only after all groups are successfully published (or retried)
|
|
409
|
+
await (0, utils_1.notifySubscribers)(this.redisPublisher, data.eventName, key, multicast);
|
|
410
|
+
await this.circuitBreaker.recordSuccess();
|
|
411
|
+
}
|
|
412
|
+
catch (publishError) {
|
|
413
|
+
// If we failed during the loop (and it wasn't handled by the adaptive logic),
|
|
414
|
+
// try to clean up any messages already sent in this attempt.
|
|
415
|
+
if (publishedGroups.length > 0) {
|
|
416
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error during multi-group publish for ${data.eventName}, rolling back published messages for key ${key}, publishedMessage: ${JSON.stringify(data)}`);
|
|
417
|
+
await this.deleteMessageFromStreams(data.eventName, publishedGroups, key);
|
|
418
|
+
}
|
|
419
|
+
// Rethrow the error that caused the failure
|
|
420
|
+
throw publishError;
|
|
421
|
+
}
|
|
163
422
|
}
|
|
164
423
|
else {
|
|
165
424
|
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Event publish failed for event ${data.eventName}, reason: no consumers ${consumerGroups}`);
|
|
@@ -348,8 +607,8 @@ class Streams {
|
|
|
348
607
|
// 3. Pending message reprocessing
|
|
349
608
|
if (!multicast && messageId !== '0' && !processPending) {
|
|
350
609
|
try {
|
|
351
|
-
const pendingDetails = await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1);
|
|
352
|
-
if (pendingDetails[
|
|
610
|
+
const pendingDetails = (await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1));
|
|
611
|
+
if (pendingDetails && pendingDetails.length > 0 && pendingDetails[0] && pendingDetails[0][3] === 0) {
|
|
353
612
|
logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: Message ${messageId} already processed for ${streamName}`);
|
|
354
613
|
return;
|
|
355
614
|
}
|
|
@@ -372,12 +631,28 @@ class Streams {
|
|
|
372
631
|
/**
|
|
373
632
|
* Very very rare case of this occurring. Cases where the running service is super overloaded
|
|
374
633
|
* 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
|
|
634
|
+
* processed gets picked up by another instance, leading to multiple publications. This makes
|
|
635
|
+
* sure that there is a check to ensure that the message belongs to this consumer before being
|
|
636
|
+
* processed
|
|
376
637
|
*/
|
|
377
638
|
if (processPending) {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
639
|
+
const pendingDetails = (await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1));
|
|
640
|
+
/** Message is not in PEL */
|
|
641
|
+
if (!pendingDetails || pendingDetails.length === 0)
|
|
642
|
+
return;
|
|
643
|
+
const [, instanceId, idleTime, deliveryCount] = pendingDetails[0][1];
|
|
644
|
+
/** Message is claimed by another consumer within the group, so do not process */
|
|
645
|
+
if (instanceId !== this.instanceId) {
|
|
646
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Process Pending message is claimed by another consumer : ${JSON.stringify({
|
|
647
|
+
instanceId,
|
|
648
|
+
idleTime,
|
|
649
|
+
deliveryCount,
|
|
650
|
+
messageId,
|
|
651
|
+
streamName,
|
|
652
|
+
originalInstanceId: this.instanceId,
|
|
653
|
+
consumerGroupName: this.consumerGroupName,
|
|
654
|
+
})}`);
|
|
655
|
+
return;
|
|
381
656
|
}
|
|
382
657
|
}
|
|
383
658
|
const messages = await redisClient.xrange(streamName, messageId, messageId);
|
|
@@ -597,6 +872,9 @@ class Streams {
|
|
|
597
872
|
*/
|
|
598
873
|
async close() {
|
|
599
874
|
try {
|
|
875
|
+
// Reset optimization state and script SHA
|
|
876
|
+
this.optimizationActiveUntil.clear();
|
|
877
|
+
this.findHighestStreamIdScriptSHA = null;
|
|
600
878
|
if (this.cleanUpTimer) {
|
|
601
879
|
clearInterval(this.cleanUpTimer);
|
|
602
880
|
}
|
package/src/lib/redis/types.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface IStreamsConfig {
|
|
|
54
54
|
halfOpenStateMaxAttempts: number;
|
|
55
55
|
maxStoredEvents: number;
|
|
56
56
|
};
|
|
57
|
+
optimizationDurationMs?: number;
|
|
58
|
+
optimizationThreshold?: number;
|
|
57
59
|
}
|
|
58
60
|
export type TEventFilter<T> = (event: EventData<T, string>) => boolean;
|
|
59
61
|
export interface ISubscription<T, TName extends string = string> {
|
package/src/lib/redis/utils.js
CHANGED
|
@@ -12,7 +12,7 @@ exports.decodeScheduledMessage = decodeScheduledMessage;
|
|
|
12
12
|
const logger_1 = require("./logger");
|
|
13
13
|
async function getAllConsumerGroups(eventName, redisConnection) {
|
|
14
14
|
const consumerGroups = await redisConnection.smembers(`${eventName}`);
|
|
15
|
-
return consumerGroups;
|
|
15
|
+
return consumerGroups.sort();
|
|
16
16
|
}
|
|
17
17
|
function* getSummaryOnStreamConsumerGroup(redisClient, consumerGroupName, streamName) {
|
|
18
18
|
const [count, , , consumers] = (yield redisClient.xpending(streamName, consumerGroupName));
|