@jetit/publisher 3.3.3 → 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/README.md +3 -1
- package/package.json +3 -5
- package/src/lib/redis/logger.d.ts +8 -0
- package/src/lib/redis/logger.js +23 -0
- package/src/lib/redis/registry.js +4 -2
- package/src/lib/redis/scheduler.js +7 -6
- package/src/lib/redis/streams.d.ts +19 -8
- package/src/lib/redis/streams.js +93 -46
- package/src/lib/redis/utils.d.ts +3 -2
- package/src/lib/redis/utils.js +17 -10
package/README.md
CHANGED
|
@@ -198,4 +198,6 @@ streams.listen('my-event').subscribe(event => {
|
|
|
198
198
|
|
|
199
199
|
7. Event-driven workflows: You can use the publisher to create event-driven workflows, where each step in the workflow is triggered by the completion of a previous step. This can be useful for orchestrating complex, multi-step processes.
|
|
200
200
|
|
|
201
|
-
8. Message broadcasting: The publisher can be used to broadcast messages to multiple consumers or subscribers, allowing for efficient and scalable communication in applications with many components or services.
|
|
201
|
+
8. Message broadcasting: The publisher can be used to broadcast messages to multiple consumers or subscribers, allowing for efficient and scalable communication in applications with many components or services.
|
|
202
|
+
|
|
203
|
+
9. Multicast Publishing: This is the existing PUB/SUB implementation but with the event data being stored into streams for additional processing
|
package/package.json
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetit/publisher",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@jetit/id": "0.0.
|
|
6
|
+
"@jetit/id": "^0.0.12",
|
|
7
7
|
"ioredis": "^5.3.0",
|
|
8
|
-
"rxjs": "^7.8.0"
|
|
9
|
-
},
|
|
10
|
-
"peerDependencies": {
|
|
8
|
+
"rxjs": "^7.8.0",
|
|
11
9
|
"tslib": "1.14.1"
|
|
12
10
|
},
|
|
13
11
|
"main": "./src/index.js"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PERFORMANCE_LOGGER = exports.PUBLISHER_LOGGER = void 0;
|
|
4
|
+
exports.PUBLISHER_LOGGER = {
|
|
5
|
+
log: (...args) => {
|
|
6
|
+
if (process.env['DEBUG_LOGGING_ENABLED'] === 'TRUE') {
|
|
7
|
+
console.log(...args);
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
warn: (...args) => {
|
|
11
|
+
console.warn(...args);
|
|
12
|
+
},
|
|
13
|
+
error: (...args) => {
|
|
14
|
+
console.warn(...args);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
exports.PERFORMANCE_LOGGER = {
|
|
18
|
+
log: (...args) => {
|
|
19
|
+
if (process.env['PERFORMANCE_LOGGING_ENABLED'] !== 'FALSE') {
|
|
20
|
+
console.log(...args);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.setRedisConnectionSettings = exports.RedisRegistry = void 0;
|
|
4
4
|
const ioredis_1 = require("ioredis");
|
|
5
|
+
const logger_1 = require("./logger");
|
|
5
6
|
class RedisRegistry {
|
|
6
7
|
static attemptConnection(connectionKey, storeRef = 0) {
|
|
7
8
|
let ref;
|
|
@@ -25,8 +26,9 @@ class RedisRegistry {
|
|
|
25
26
|
}
|
|
26
27
|
static handleDisconnects(connection, connectionKey, storeRef) {
|
|
27
28
|
connection.on('error', (error) => {
|
|
28
|
-
|
|
29
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Redis connection error : ${error.message}`);
|
|
29
30
|
connection.removeAllListeners();
|
|
31
|
+
connection.disconnect();
|
|
30
32
|
RedisRegistry.attemptConnection(connectionKey, storeRef);
|
|
31
33
|
});
|
|
32
34
|
}
|
|
@@ -42,7 +44,7 @@ class RedisRegistry {
|
|
|
42
44
|
]);
|
|
43
45
|
if (res === 'NO_PING') {
|
|
44
46
|
connection.disconnect(true);
|
|
45
|
-
|
|
47
|
+
logger_1.PUBLISHER_LOGGER.error('PUBLISHER: failed to ping redis, disconnecting and restarting service.');
|
|
46
48
|
process.exit(0);
|
|
47
49
|
}
|
|
48
50
|
}, 2000);
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ScheduledProcessor = void 0;
|
|
4
4
|
const id_1 = require("@jetit/id");
|
|
5
5
|
const rxjs_1 = require("rxjs");
|
|
6
|
+
const logger_1 = require("./logger");
|
|
6
7
|
const registry_1 = require("./registry");
|
|
7
8
|
const utils_1 = require("./utils");
|
|
8
9
|
/**
|
|
@@ -18,27 +19,27 @@ class ScheduledProcessor {
|
|
|
18
19
|
constructor(duration = 1000) {
|
|
19
20
|
this.previousTaskCompleted = true;
|
|
20
21
|
this.scheduledMessagesTimer = (0, rxjs_1.interval)(duration).subscribe(() => {
|
|
21
|
-
|
|
22
|
+
logger_1.PUBLISHER_LOGGER.log('Checking Streams messages at ', new Date().toISOString(), '...');
|
|
22
23
|
/** Do not run scheduler if the previous run is not completed */
|
|
23
24
|
if (this.previousTaskCompleted) {
|
|
24
25
|
this.previousTaskCompleted = false;
|
|
25
26
|
this.processScheduledEvents()
|
|
26
27
|
.catch((error) => {
|
|
27
|
-
|
|
28
|
+
logger_1.PUBLISHER_LOGGER.error('Error while processing scheduled events:', error);
|
|
28
29
|
})
|
|
29
30
|
.then(() => {
|
|
30
31
|
this.previousTaskCompleted = true;
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
34
|
else {
|
|
34
|
-
|
|
35
|
+
logger_1.PUBLISHER_LOGGER.log('Skipping current scheduler run because previous run is in progress');
|
|
35
36
|
}
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
async processScheduledEvents() {
|
|
39
40
|
const currentTime = new Date().getTime();
|
|
40
41
|
const events = await this.redisPublisher.zrangebyscore('se', 0, currentTime);
|
|
41
|
-
|
|
42
|
+
logger_1.PUBLISHER_LOGGER.log('Events to process:', events.length);
|
|
42
43
|
for (const eventString of events) {
|
|
43
44
|
const eventData = (0, utils_1.decodeScheduledMessage)(eventString);
|
|
44
45
|
/**
|
|
@@ -52,14 +53,14 @@ class ScheduledProcessor {
|
|
|
52
53
|
eventData.eventId = (0, id_1.generateID)('HEX', 'FF');
|
|
53
54
|
await this.redisPublisher.zrem('se', eventString);
|
|
54
55
|
const consumerGroups = await (0, utils_1.getAllConsumerGroups)(eventData.eventName, this.redisPublisher);
|
|
55
|
-
|
|
56
|
+
logger_1.PUBLISHER_LOGGER.log('Scheduled Publishing to consumer groups: ', consumerGroups, 'with id ', eventData.eventId, '...');
|
|
56
57
|
let key = '*';
|
|
57
58
|
for (const consumerGroup of consumerGroups) {
|
|
58
59
|
// Publish the event to each consumer group's stream
|
|
59
60
|
const streamName = `${eventData.eventName}:${consumerGroup}`;
|
|
60
61
|
const generatedKey = await this.redisPublisher
|
|
61
62
|
.xadd(streamName, '*', 'data', JSON.stringify(eventData))
|
|
62
|
-
.catch((e) =>
|
|
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} `));
|
|
63
64
|
if (key === '*')
|
|
64
65
|
key = generatedKey ?? key;
|
|
65
66
|
}
|
|
@@ -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
|
|
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.
|
|
@@ -67,7 +68,7 @@ export declare class Streams {
|
|
|
67
68
|
*
|
|
68
69
|
* await streams.scheduledPublish(futureTime, eventData);
|
|
69
70
|
*/
|
|
70
|
-
scheduledPublish<TData = unknown, TName extends string = string>(scheduledTime: Date, eventData: PublishData<TData, TName>, uniquePerInstance?: boolean, repeatInterval?: number): Promise<void>;
|
|
71
|
+
scheduledPublish<TData = unknown, TName extends string = string>(scheduledTime: Date, eventData: PublishData<TData, TName>, uniquePerInstance?: boolean, repeatInterval?: number, multicast?: boolean): Promise<void>;
|
|
71
72
|
/**
|
|
72
73
|
* Listens for events with the given name and returns an Observable that emits an EventData<T> object
|
|
73
74
|
* each time a new event is received.
|
|
@@ -75,7 +76,7 @@ export declare class Streams {
|
|
|
75
76
|
* The method uses a BehaviorSubject to emit the events as Observables. The BehaviorSubject ensures
|
|
76
77
|
* that new subscribers receive the last emitted event, even if they subscribe after the event has been emitted.
|
|
77
78
|
*
|
|
78
|
-
* If an error occurs while subscribing, the method logs the error to the
|
|
79
|
+
* If an error occurs while subscribing, the method logs the error to the PUBLISHER_LOGGER and throws
|
|
79
80
|
* an error. This is done to prevent the service from continuing without a proper event subscription.
|
|
80
81
|
*
|
|
81
82
|
* There is retry logic with exponential backoff to handle error cases. These are also controllable by the
|
|
@@ -92,7 +93,7 @@ export declare class Streams {
|
|
|
92
93
|
*
|
|
93
94
|
* // Subscribe to the Observable and log each new event
|
|
94
95
|
* orderCreated.subscribe((event) => {
|
|
95
|
-
*
|
|
96
|
+
* PUBLISHER_LOGGER.log('New order created:', event.data);
|
|
96
97
|
* });
|
|
97
98
|
*/
|
|
98
99
|
listen<T = unknown, const TName extends string = string>(eventName: TName, maxRetries?: number, initialDelay?: number): Observable<EventData<T, TName>>;
|
|
@@ -108,16 +109,26 @@ export declare class Streams {
|
|
|
108
109
|
* process.on('SIGINT', shutdown);
|
|
109
110
|
*
|
|
110
111
|
* async function shutdown(): Promise<void> {
|
|
111
|
-
*
|
|
112
|
+
* PUBLISHER_LOGGER.log('Graceful shutdown initiated.');
|
|
112
113
|
* try {
|
|
113
114
|
* await streams.close();
|
|
114
|
-
*
|
|
115
|
+
* PUBLISHER_LOGGER.log('Resources and connections successfully closed.');
|
|
115
116
|
* } catch (error) {
|
|
116
|
-
*
|
|
117
|
+
* PUBLISHER_LOGGER.error('Error during graceful shutdown:', error);
|
|
117
118
|
* }
|
|
118
119
|
* process.exit(0);
|
|
119
120
|
* }
|
|
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
|
@@ -3,10 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Streams = void 0;
|
|
4
4
|
const id_1 = require("@jetit/id");
|
|
5
5
|
const rxjs_1 = require("rxjs");
|
|
6
|
+
const logger_1 = require("./logger");
|
|
6
7
|
const registry_1 = require("./registry");
|
|
7
8
|
const utils_1 = require("./utils");
|
|
8
9
|
function publisherErrorHandler(error) {
|
|
9
|
-
|
|
10
|
+
logger_1.PUBLISHER_LOGGER.error('PUBLISHER UNHANDLED ERROR: ', JSON.stringify(error));
|
|
10
11
|
}
|
|
11
12
|
class Streams {
|
|
12
13
|
get redisPublisher() {
|
|
@@ -38,23 +39,24 @@ class Streams {
|
|
|
38
39
|
this.instanceUniqueId = process.env['INSTANCE_ID'] ?? (0, id_1.generateID)('HEX', 'FE');
|
|
39
40
|
this.instanceId = `${serviceName}:${this.instanceUniqueId}`;
|
|
40
41
|
this.consumerGroupName = `cg-${serviceName}`;
|
|
41
|
-
|
|
42
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Instance ID: ${this.instanceId}`);
|
|
42
43
|
const cleanUpInterval = parseInt(process.env['CLEANUP_INTERVAL'] || `${1000 * 60 * 60}`, 10) ?? 1000 * 60 * 60;
|
|
43
44
|
this.cleanUpTimer = setInterval(() => {
|
|
44
45
|
this.runClear(cleanUpInterval);
|
|
45
46
|
}, cleanUpInterval);
|
|
46
47
|
}
|
|
47
48
|
async runClear(cleanUpInterval) {
|
|
48
|
-
|
|
49
|
+
logger_1.PUBLISHER_LOGGER.log('PUBLISHER: Running Clearance', this.eventsListened);
|
|
49
50
|
for (const eventName of this.eventsListened) {
|
|
50
51
|
process.nextTick(async () => {
|
|
51
52
|
/** This removes the messages from the stream after they have been processed according to cleanup interval */
|
|
52
53
|
await this.cleanupAcknowledgedMessages(eventName, cleanUpInterval).catch(publisherErrorHandler);
|
|
53
|
-
|
|
54
|
+
logger_1.PUBLISHER_LOGGER.log(`Cleanup process for Acknowledged messages completed for ${eventName}`);
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
|
-
async publish(data) {
|
|
58
|
+
async publish(data, multicast = false) {
|
|
59
|
+
const publishStartTime = process.hrtime();
|
|
58
60
|
if (data.eventId)
|
|
59
61
|
data.republishEvent = data.eventId;
|
|
60
62
|
data.eventId = (0, id_1.generateID)('HEX', 'FF');
|
|
@@ -63,28 +65,32 @@ class Streams {
|
|
|
63
65
|
const consumerGroups = await (0, utils_1.getAllConsumerGroups)(data.eventName, this.redisPublisher);
|
|
64
66
|
let key = '*';
|
|
65
67
|
if (consumerGroups.length > 0) {
|
|
66
|
-
|
|
68
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')}`);
|
|
67
69
|
try {
|
|
68
70
|
for (const consumerGroup of consumerGroups) {
|
|
69
71
|
// Publish the event to each consumer group's stream
|
|
70
72
|
const streamName = `${data.eventName}:${consumerGroup}`;
|
|
71
73
|
const generatedKey = await this.redisPublisher
|
|
72
74
|
.xadd(streamName, key, 'data', JSON.stringify(data))
|
|
73
|
-
.catch((e) =>
|
|
75
|
+
.catch((e) => logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(data)}, ${e} `));
|
|
74
76
|
if (key === '*')
|
|
75
77
|
key = generatedKey ?? key;
|
|
76
78
|
}
|
|
77
|
-
await (0, utils_1.notifySubscribers)(this.redisPublisher, data.eventName, key);
|
|
79
|
+
await (0, utils_1.notifySubscribers)(this.redisPublisher, data.eventName, key, multicast);
|
|
78
80
|
}
|
|
79
81
|
catch (error) {
|
|
80
|
-
|
|
82
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error while publishing event for service ${this.consumerGroupName} with instance ${this.instanceId}: `, error);
|
|
81
83
|
throw new Error('Publisher Error');
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
else
|
|
85
|
-
|
|
87
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Event publish failed for event ${data.eventName}, reason: no consumers ${consumerGroups}`);
|
|
88
|
+
const publishEndTime = process.hrtime(publishStartTime);
|
|
89
|
+
const elapsedTime = publishEndTime[0] * 1000 + publishEndTime[1] / 1000000;
|
|
90
|
+
logger_1.PERFORMANCE_LOGGER.log(`PTIME;${key};${data.eventName};${Date.now()};${elapsedTime}`);
|
|
91
|
+
return key;
|
|
86
92
|
}
|
|
87
|
-
async scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0) {
|
|
93
|
+
async scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0, multicast = false) {
|
|
88
94
|
const currentTime = new Date();
|
|
89
95
|
delete eventData.repeatInterval;
|
|
90
96
|
if (repeatInterval > 0) {
|
|
@@ -94,14 +100,14 @@ class Streams {
|
|
|
94
100
|
throw new Error('PUBLISHER: Cannot schedule an event in the past');
|
|
95
101
|
}
|
|
96
102
|
else if (Math.abs(scheduledTime.getTime() - currentTime.getTime()) <= 500) {
|
|
97
|
-
await this.publish(eventData);
|
|
103
|
+
await this.publish(eventData, multicast);
|
|
98
104
|
}
|
|
99
105
|
else {
|
|
100
106
|
const key = (0, utils_1.encodeScheduledMessage)(eventData);
|
|
101
107
|
if (uniquePerInstance === true) {
|
|
102
108
|
const existingJob = await this.redisPublisher.zscore('se', key);
|
|
103
109
|
if (existingJob) {
|
|
104
|
-
|
|
110
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Job with data '${eventData}' already exists. Skipping.`);
|
|
105
111
|
return;
|
|
106
112
|
}
|
|
107
113
|
}
|
|
@@ -115,7 +121,7 @@ class Streams {
|
|
|
115
121
|
* The method uses a BehaviorSubject to emit the events as Observables. The BehaviorSubject ensures
|
|
116
122
|
* that new subscribers receive the last emitted event, even if they subscribe after the event has been emitted.
|
|
117
123
|
*
|
|
118
|
-
* If an error occurs while subscribing, the method logs the error to the
|
|
124
|
+
* If an error occurs while subscribing, the method logs the error to the PUBLISHER_LOGGER and throws
|
|
119
125
|
* an error. This is done to prevent the service from continuing without a proper event subscription.
|
|
120
126
|
*
|
|
121
127
|
* There is retry logic with exponential backoff to handle error cases. These are also controllable by the
|
|
@@ -132,7 +138,7 @@ class Streams {
|
|
|
132
138
|
*
|
|
133
139
|
* // Subscribe to the Observable and log each new event
|
|
134
140
|
* orderCreated.subscribe((event) => {
|
|
135
|
-
*
|
|
141
|
+
* PUBLISHER_LOGGER.log('New order created:', event.data);
|
|
136
142
|
* });
|
|
137
143
|
*/
|
|
138
144
|
listen(eventName, maxRetries = 5, initialDelay = 1000) {
|
|
@@ -140,11 +146,11 @@ class Streams {
|
|
|
140
146
|
count: maxRetries,
|
|
141
147
|
delay: (error, retryAttempt) => {
|
|
142
148
|
const delay = initialDelay * Math.pow(2, retryAttempt);
|
|
143
|
-
|
|
149
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error in listen: ${error.message}. Retrying in ${delay}ms (attempt ${retryAttempt + 1})`);
|
|
144
150
|
return (0, rxjs_1.timer)(delay);
|
|
145
151
|
},
|
|
146
152
|
}), (0, rxjs_1.catchError)((error) => {
|
|
147
|
-
|
|
153
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error in listen after ${maxRetries} retries: ${error.message}`);
|
|
148
154
|
return (0, rxjs_1.throwError)(() => new Error(error.message));
|
|
149
155
|
}));
|
|
150
156
|
}
|
|
@@ -156,61 +162,88 @@ class Streams {
|
|
|
156
162
|
await this.redisGroups
|
|
157
163
|
.xgroup('CREATE', streamName, this.consumerGroupName, '0', 'MKSTREAM')
|
|
158
164
|
.then(() => {
|
|
159
|
-
|
|
165
|
+
logger_1.PUBLISHER_LOGGER.log(`Group created created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
|
|
160
166
|
})
|
|
161
167
|
.catch((e) => {
|
|
162
|
-
|
|
168
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
|
|
163
169
|
});
|
|
164
170
|
const createConsumerStatus = (await this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId));
|
|
165
171
|
await this.redisGroups.sadd(key, eventName);
|
|
166
172
|
const addToCGSet = await this.redisGroups.sadd(`${eventName}`, this.consumerGroupName);
|
|
167
173
|
const addToFlushSet = await this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName);
|
|
168
|
-
|
|
174
|
+
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 })}`);
|
|
169
175
|
return createConsumerStatus === 0 || createConsumerStatus === 1;
|
|
170
176
|
}
|
|
171
177
|
listenInternals(eventName) {
|
|
172
178
|
const bs = new rxjs_1.BehaviorSubject(null);
|
|
173
|
-
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
|
+
}));
|
|
174
187
|
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
175
|
-
const processMessage = async (redisClient, messageId, processPending = false) => {
|
|
176
|
-
|
|
188
|
+
const processMessage = async (redisClient, messageId, multicast = false, processPending = false) => {
|
|
189
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Processing message ${messageId} for ${streamName}`);
|
|
177
190
|
try {
|
|
178
191
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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);
|
|
196
|
+
if (pendingDetails[2] === 0 && multicast === false) {
|
|
197
|
+
logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: MACK ${messageId} for ${streamName}`);
|
|
182
198
|
return;
|
|
183
199
|
}
|
|
184
200
|
}
|
|
185
201
|
catch (e) {
|
|
186
202
|
// Ignore the xpending error and continue
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
logger_1.PUBLISHER_LOGGER.error('XPENDING ERROR: To be handled');
|
|
204
|
+
logger_1.PUBLISHER_LOGGER.warn(JSON.stringify(e));
|
|
205
|
+
}
|
|
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
|
+
}
|
|
189
218
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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) {
|
|
193
226
|
bs.next(eventData);
|
|
194
227
|
await redisClient.xack(streamName, this.consumerGroupName, messageId);
|
|
195
228
|
await redisClient.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
|
|
196
229
|
}
|
|
197
230
|
else {
|
|
198
|
-
|
|
231
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
|
|
199
232
|
}
|
|
200
233
|
/** Process Unprocessed Message if this is a main tree, otherwise limit to processing 100 messages that are unacknowledged */
|
|
201
234
|
if (!processPending) {
|
|
202
|
-
const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName,
|
|
203
|
-
if (unprocessedMessageIds.
|
|
204
|
-
|
|
235
|
+
const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, this.instanceId);
|
|
236
|
+
if (unprocessedMessageIds.countOnThisConsumer && unprocessedMessageIds.countOnThisConsumer > 25) {
|
|
237
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
|
|
205
238
|
}
|
|
206
239
|
for (const id of unprocessedMessageIds.messageIds) {
|
|
207
|
-
|
|
208
|
-
await processMessage(redisClient, id, true);
|
|
240
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Reporcessing unprocessed message with id: ${id}`);
|
|
241
|
+
await processMessage(redisClient, id, multicast, true);
|
|
209
242
|
}
|
|
210
243
|
}
|
|
211
244
|
}
|
|
212
245
|
catch (e) {
|
|
213
|
-
|
|
246
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error processing message ${messageId} for ${streamName}`, e);
|
|
214
247
|
}
|
|
215
248
|
};
|
|
216
249
|
/** Register the consumer and setup the Observable */
|
|
@@ -220,15 +253,20 @@ class Streams {
|
|
|
220
253
|
throw new Error('PUBLISHER: Cannot setup consumer');
|
|
221
254
|
const eventStreamClient = registry_1.RedisRegistry.getConnection(`sub-${eventName}`);
|
|
222
255
|
eventStreamClient.subscribe(eventName).then(() => {
|
|
223
|
-
|
|
256
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Redis Subscription connection initiated for ${eventName}`);
|
|
224
257
|
});
|
|
225
|
-
eventStreamClient.on('message', async (channel,
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
eventStreamClient.on('message', async (channel, data) => {
|
|
259
|
+
const subscribeStartTime = process.hrtime();
|
|
260
|
+
const { messageId, multicast } = JSON.parse(data);
|
|
261
|
+
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Stream Notification Received for event ${eventName} with message ID ${messageId}`);
|
|
262
|
+
await processMessage(this.redisGroups, messageId, multicast);
|
|
263
|
+
const subscribendTime = process.hrtime(subscribeStartTime);
|
|
264
|
+
const elapsedTime = subscribendTime[0] * 1000 + subscribendTime[1] / 1000000;
|
|
265
|
+
logger_1.PERFORMANCE_LOGGER.log(`STIME;${messageId};${data.eventName};${Date.now()};${elapsedTime}`);
|
|
228
266
|
});
|
|
229
267
|
})
|
|
230
268
|
.catch((e) => {
|
|
231
|
-
|
|
269
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error during consumer registration for ${eventName}`, e);
|
|
232
270
|
});
|
|
233
271
|
return observable;
|
|
234
272
|
}
|
|
@@ -242,12 +280,12 @@ class Streams {
|
|
|
242
280
|
* process.on('SIGINT', shutdown);
|
|
243
281
|
*
|
|
244
282
|
* async function shutdown(): Promise<void> {
|
|
245
|
-
*
|
|
283
|
+
* PUBLISHER_LOGGER.log('Graceful shutdown initiated.');
|
|
246
284
|
* try {
|
|
247
285
|
* await streams.close();
|
|
248
|
-
*
|
|
286
|
+
* PUBLISHER_LOGGER.log('Resources and connections successfully closed.');
|
|
249
287
|
* } catch (error) {
|
|
250
|
-
*
|
|
288
|
+
* PUBLISHER_LOGGER.error('Error during graceful shutdown:', error);
|
|
251
289
|
* }
|
|
252
290
|
* process.exit(0);
|
|
253
291
|
* }
|
|
@@ -279,5 +317,14 @@ class Streams {
|
|
|
279
317
|
await this.redisGroups.zremrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
|
|
280
318
|
}
|
|
281
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
|
+
}
|
|
282
329
|
}
|
|
283
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
|
}>;
|
|
@@ -10,7 +11,7 @@ export declare function getMessageStatesCount(redisClient: RedisType, streamName
|
|
|
10
11
|
acknowledged: number;
|
|
11
12
|
unacknowledged: number;
|
|
12
13
|
}>;
|
|
13
|
-
export declare function notifySubscribers(redisClient: RedisType, eventName: string, messageId: string): Promise<void>;
|
|
14
|
+
export declare function notifySubscribers(redisClient: RedisType, eventName: string, messageId: string, multicast?: boolean): Promise<void>;
|
|
14
15
|
export declare function removedScheduledJob(redisClient: RedisType, eventString: string): Promise<void>;
|
|
15
16
|
export declare function encodeScheduledMessage<TData, TName extends string>(data: EventData<TData, TName>): string;
|
|
16
17
|
export declare function decodeScheduledMessage(data: string): EventData<never, never>;
|
package/src/lib/redis/utils.js
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.UTILS = exports.decodeScheduledMessage = exports.encodeScheduledMessage = exports.removedScheduledJob = exports.notifySubscribers = exports.getMessageStatesCount = exports.getUnacknowledgedMessages = exports.getAllConsumerGroups = void 0;
|
|
4
|
+
const logger_1 = require("./logger");
|
|
4
5
|
async function getAllConsumerGroups(eventName, redisConnection) {
|
|
5
6
|
const consumerGroups = await redisConnection.smembers(`${eventName}`);
|
|
6
7
|
return consumerGroups;
|
|
7
8
|
}
|
|
8
9
|
exports.getAllConsumerGroups = getAllConsumerGroups;
|
|
9
|
-
async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, count = 500) {
|
|
10
|
+
async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, consumerName, count = 500) {
|
|
10
11
|
try {
|
|
11
12
|
// Get pending messages summary
|
|
12
13
|
const summary = await redisClient.xpending(streamName, consumerGroupName);
|
|
13
|
-
if (!summary || summary[
|
|
14
|
+
if (!summary || summary[0] === 0) {
|
|
14
15
|
// If count is zero
|
|
15
16
|
return { count: 0, messageIds: [] };
|
|
16
17
|
}
|
|
17
18
|
// Use the smallest and largest IDs to get a detailed range
|
|
18
|
-
const pendingMessageCount = summary[
|
|
19
|
+
const pendingMessageCount = summary[0];
|
|
19
20
|
// Get detailed information in the range
|
|
20
|
-
|
|
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
|
+
}
|
|
21
27
|
return {
|
|
22
28
|
count: pendingMessageCount,
|
|
29
|
+
countOnThisConsumer: pendingMessages.length,
|
|
23
30
|
messageIds: pendingMessages.map((message) => message[0]),
|
|
24
31
|
messages: pendingMessages,
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
catch (error) {
|
|
28
|
-
|
|
35
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error fetching unacknowledged messages for ${streamName}`, error);
|
|
29
36
|
return { count: 0, messageIds: [] };
|
|
30
37
|
}
|
|
31
38
|
}
|
|
@@ -40,22 +47,22 @@ async function getMessageStatesCount(redisClient, streamName, consumerGroup) {
|
|
|
40
47
|
};
|
|
41
48
|
}
|
|
42
49
|
catch (error) {
|
|
43
|
-
|
|
50
|
+
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error fetching message states count for ${streamName}`, error);
|
|
44
51
|
return { acknowledged: 0, unacknowledged: 0 };
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
exports.getMessageStatesCount = getMessageStatesCount;
|
|
48
|
-
async function notifySubscribers(redisClient, eventName, messageId) {
|
|
49
|
-
await redisClient.publish(eventName, messageId);
|
|
55
|
+
async function notifySubscribers(redisClient, eventName, messageId, multicast = false) {
|
|
56
|
+
await redisClient.publish(eventName, JSON.stringify({ messageId, multicast }));
|
|
50
57
|
}
|
|
51
58
|
exports.notifySubscribers = notifySubscribers;
|
|
52
59
|
async function removedScheduledJob(redisClient, eventString) {
|
|
53
60
|
const currentTime = new Date().getTime();
|
|
54
61
|
const events = await redisClient.zrangebyscore('se', 0, currentTime);
|
|
55
|
-
|
|
62
|
+
logger_1.PUBLISHER_LOGGER.log(`Total Events in scheduled queue: ${events.length}`);
|
|
56
63
|
await redisClient.zrem('se', eventString);
|
|
57
64
|
const eventsLater = await redisClient.zrangebyscore('se', 0, currentTime);
|
|
58
|
-
|
|
65
|
+
logger_1.PUBLISHER_LOGGER.log(`Total Events in scheduled queue: ${eventsLater.length}`);
|
|
59
66
|
}
|
|
60
67
|
exports.removedScheduledJob = removedScheduledJob;
|
|
61
68
|
function encodeScheduledMessage(data) {
|