@jetit/publisher 2.0.2 → 3.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 +2 -2
- package/src/lib/publisher.d.ts +1 -0
- package/src/lib/publisher.js +3 -1
- package/src/lib/redis/registry.js +28 -12
- package/src/lib/redis/scheduler.js +39 -39
- package/src/lib/redis/streams.d.ts +0 -3
- package/src/lib/redis/streams.js +132 -210
- package/src/lib/redis/utils.d.ts +18 -0
- package/src/lib/redis/utils.js +57 -0
- package/src/lib/redis/groups.d.ts +0 -2
- package/src/lib/redis/groups.js +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetit/publisher",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@jetit/id": "0.0.11",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"rxjs": "^7.8.0"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"tslib": "2.5.
|
|
11
|
+
"tslib": "2.5.3"
|
|
12
12
|
},
|
|
13
13
|
"main": "./src/index.js",
|
|
14
14
|
"types": "./src/index.d.ts"
|
package/src/lib/publisher.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { Streams as Publisher } from './redis/streams';
|
|
2
2
|
export { setRedisConnectionSettings as setRedisConfig } from './redis/registry';
|
|
3
3
|
export { ScheduledProcessor as __SCHEDULER_INTERNALS__ } from './redis/scheduler';
|
|
4
|
+
export { UTILS as StreamUtilityFunctions } from './redis/utils';
|
package/src/lib/publisher.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.__SCHEDULER_INTERNALS__ = exports.setRedisConfig = exports.Publisher = void 0;
|
|
3
|
+
exports.StreamUtilityFunctions = exports.__SCHEDULER_INTERNALS__ = exports.setRedisConfig = exports.Publisher = void 0;
|
|
4
4
|
var streams_1 = require("./redis/streams");
|
|
5
5
|
Object.defineProperty(exports, "Publisher", { enumerable: true, get: function () { return streams_1.Streams; } });
|
|
6
6
|
var registry_1 = require("./redis/registry");
|
|
7
7
|
Object.defineProperty(exports, "setRedisConfig", { enumerable: true, get: function () { return registry_1.setRedisConnectionSettings; } });
|
|
8
8
|
var scheduler_1 = require("./redis/scheduler");
|
|
9
9
|
Object.defineProperty(exports, "__SCHEDULER_INTERNALS__", { enumerable: true, get: function () { return scheduler_1.ScheduledProcessor; } });
|
|
10
|
+
var utils_1 = require("./redis/utils");
|
|
11
|
+
Object.defineProperty(exports, "StreamUtilityFunctions", { enumerable: true, get: function () { return utils_1.UTILS; } });
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var _a, _b;
|
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
3
|
exports.setRedisConnectionSettings = exports.RedisRegistry = void 0;
|
|
5
|
-
const tslib_1 = require("tslib");
|
|
6
4
|
const ioredis_1 = require("ioredis");
|
|
7
5
|
class RedisRegistry {
|
|
8
6
|
static attemptConnection(connectionKey, storeRef = 0) {
|
|
9
7
|
let ref;
|
|
10
8
|
if (RedisRegistry.options.cluster) {
|
|
11
|
-
ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes,
|
|
9
|
+
ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes, {
|
|
10
|
+
...RedisRegistry.options.cluster.options,
|
|
11
|
+
redisOptions: {
|
|
12
|
+
...RedisRegistry.options.cluster.options.redisOptions,
|
|
13
|
+
db: storeRef,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
12
16
|
}
|
|
13
17
|
else
|
|
14
|
-
ref = new ioredis_1.default(
|
|
18
|
+
ref = new ioredis_1.default({
|
|
19
|
+
...RedisRegistry.options.redis,
|
|
20
|
+
db: storeRef,
|
|
21
|
+
});
|
|
15
22
|
RedisRegistry.registry.set(connectionKey, ref);
|
|
16
23
|
RedisRegistry.handleDisconnects(ref, connectionKey, storeRef);
|
|
17
24
|
return ref;
|
|
@@ -24,8 +31,8 @@ class RedisRegistry {
|
|
|
24
31
|
});
|
|
25
32
|
}
|
|
26
33
|
static handlePing(connection) {
|
|
27
|
-
setInterval(() =>
|
|
28
|
-
const res =
|
|
34
|
+
setInterval(async () => {
|
|
35
|
+
const res = await Promise.race([
|
|
29
36
|
connection.ping(),
|
|
30
37
|
new Promise((res) => {
|
|
31
38
|
setTimeout(() => {
|
|
@@ -38,17 +45,26 @@ class RedisRegistry {
|
|
|
38
45
|
console.error('PUBLISHER: failed to ping redis, disconnecting and restarting service.');
|
|
39
46
|
process.exit(0);
|
|
40
47
|
}
|
|
41
|
-
}
|
|
48
|
+
}, 2000);
|
|
42
49
|
}
|
|
43
50
|
static getConnection(connectionType = 'primary', storeRef = 0) {
|
|
44
51
|
const connectionKey = `${connectionType}${storeRef}`;
|
|
45
52
|
let ref = this.registry.get(connectionKey);
|
|
46
53
|
if (!ref) {
|
|
47
54
|
if (RedisRegistry.options.cluster) {
|
|
48
|
-
ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes,
|
|
55
|
+
ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes, {
|
|
56
|
+
...RedisRegistry.options.cluster.options,
|
|
57
|
+
redisOptions: {
|
|
58
|
+
...RedisRegistry.options.cluster.options.redisOptions,
|
|
59
|
+
db: storeRef,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
49
62
|
}
|
|
50
63
|
else
|
|
51
|
-
ref = new ioredis_1.default(
|
|
64
|
+
ref = new ioredis_1.default({
|
|
65
|
+
...RedisRegistry.options.redis,
|
|
66
|
+
db: storeRef,
|
|
67
|
+
});
|
|
52
68
|
}
|
|
53
69
|
return ref;
|
|
54
70
|
}
|
|
@@ -59,14 +75,14 @@ class RedisRegistry {
|
|
|
59
75
|
return RedisRegistry.options;
|
|
60
76
|
}
|
|
61
77
|
}
|
|
78
|
+
exports.RedisRegistry = RedisRegistry;
|
|
62
79
|
RedisRegistry.registry = new Map();
|
|
63
80
|
RedisRegistry.options = {
|
|
64
81
|
redis: {
|
|
65
|
-
port: parseInt(
|
|
66
|
-
host:
|
|
82
|
+
port: parseInt(process.env['REDIS_PORT'] ?? '6379'),
|
|
83
|
+
host: process.env['REDIS_HOST'] ?? 'localhost',
|
|
67
84
|
},
|
|
68
85
|
};
|
|
69
|
-
exports.RedisRegistry = RedisRegistry;
|
|
70
86
|
/**
|
|
71
87
|
* This function is used to set Redis Connection options per instance. If no
|
|
72
88
|
* options are provided, then the service connects as to a single instance
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ScheduledProcessor = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
4
|
const id_1 = require("@jetit/id");
|
|
6
5
|
const rxjs_1 = require("rxjs");
|
|
7
6
|
const registry_1 = require("./registry");
|
|
8
|
-
const
|
|
7
|
+
const utils_1 = require("./utils");
|
|
9
8
|
/**
|
|
10
9
|
* DO NOT USE THIS CLASS IF YOU DON'T KNOW WHAT YOU ARE DOING. This class is
|
|
11
10
|
* meant to be used internally by the scheduler application
|
|
@@ -36,48 +35,49 @@ class ScheduledProcessor {
|
|
|
36
35
|
}
|
|
37
36
|
});
|
|
38
37
|
}
|
|
39
|
-
processScheduledEvents() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
yield this.redisPublisher.zadd('se', nextEventTime, nextEventString);
|
|
67
|
-
}
|
|
68
|
-
yield this.redisPublisher.publish(eventData.eventName, '');
|
|
38
|
+
async processScheduledEvents() {
|
|
39
|
+
const currentTime = new Date().getTime();
|
|
40
|
+
const events = await this.redisPublisher.zrangebyscore('se', 0, currentTime);
|
|
41
|
+
console.log('Events to process:', events.length);
|
|
42
|
+
for (const eventString of events) {
|
|
43
|
+
const eventData = JSON.parse(eventString);
|
|
44
|
+
/**
|
|
45
|
+
* Remove the event from the Redis Sorted Set first. Please note that
|
|
46
|
+
* there is a chance of failure here if the process crashes before
|
|
47
|
+
* the event is published. In that case, the event will be lost.
|
|
48
|
+
*
|
|
49
|
+
* Instead of using the publish method directly, the entire logic is
|
|
50
|
+
* copy pasted to reduce the case of failure.
|
|
51
|
+
*/
|
|
52
|
+
eventData.eventId = (0, id_1.generateID)('HEX', 'FF');
|
|
53
|
+
await this.redisPublisher.zrem('se', eventString);
|
|
54
|
+
const consumerGroups = await (0, utils_1.getAllConsumerGroups)(eventData.eventName, this.redisPublisher);
|
|
55
|
+
console.log('Scheduled Publishing to consumer groups: ', consumerGroups, 'with id ', eventData.eventId, '...');
|
|
56
|
+
let key = '*';
|
|
57
|
+
for (const consumerGroup of consumerGroups) {
|
|
58
|
+
// Publish the event to each consumer group's stream
|
|
59
|
+
const streamName = `${eventData.eventName}:${consumerGroup}`;
|
|
60
|
+
const generatedKey = await this.redisPublisher
|
|
61
|
+
.xadd(streamName, '*', 'data', JSON.stringify(eventData))
|
|
62
|
+
.catch((e) => console.error(`PUBLISHER: Publishing event ${eventData.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(eventData)}, ${e} `));
|
|
63
|
+
if (key === '*')
|
|
64
|
+
key = generatedKey ?? key;
|
|
69
65
|
}
|
|
70
|
-
|
|
66
|
+
if (eventData.repeatInterval) {
|
|
67
|
+
const nextEventTime = currentTime + eventData.repeatInterval;
|
|
68
|
+
const nextEventString = JSON.stringify({ ...eventData });
|
|
69
|
+
await this.redisPublisher.zadd('se', nextEventTime, nextEventString);
|
|
70
|
+
}
|
|
71
|
+
await (0, utils_1.notifySubscribers)(this.redisPublisher, eventData.eventName, key);
|
|
72
|
+
}
|
|
71
73
|
}
|
|
72
74
|
getAllScheduledEvents() {
|
|
73
75
|
return this.redisPublisher.zrange('se', 0, -1);
|
|
74
76
|
}
|
|
75
|
-
close() {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
});
|
|
77
|
+
async close() {
|
|
78
|
+
if (this.scheduledMessagesTimer) {
|
|
79
|
+
this.scheduledMessagesTimer.unsubscribe();
|
|
80
|
+
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
exports.ScheduledProcessor = ScheduledProcessor;
|
|
@@ -26,8 +26,6 @@ export declare class Streams {
|
|
|
26
26
|
*/
|
|
27
27
|
constructor(serviceName: string);
|
|
28
28
|
private runClear;
|
|
29
|
-
private isDuplicateMessage;
|
|
30
|
-
private clearDuplicationCheckKeys;
|
|
31
29
|
/**
|
|
32
30
|
* Publishes an event with the given data to the Redis event stream.
|
|
33
31
|
*
|
|
@@ -121,6 +119,5 @@ export declare class Streams {
|
|
|
121
119
|
* }
|
|
122
120
|
*/
|
|
123
121
|
close(): Promise<void>;
|
|
124
|
-
private scanAndClaimAUnclaimedMessage;
|
|
125
122
|
private cleanupAcknowledgedMessages;
|
|
126
123
|
}
|
package/src/lib/redis/streams.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Streams = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
4
|
const registry_1 = require("./registry");
|
|
6
5
|
const rxjs_1 = require("rxjs");
|
|
7
6
|
const id_1 = require("@jetit/id");
|
|
8
|
-
const
|
|
7
|
+
const utils_1 = require("./utils");
|
|
9
8
|
function publisherErrorHandler(error) {
|
|
10
|
-
console.error('PUBLISHER UNHANDLED ERROR: ', error);
|
|
9
|
+
console.error('PUBLISHER UNHANDLED ERROR: ', JSON.stringify(error));
|
|
11
10
|
}
|
|
12
11
|
class Streams {
|
|
13
12
|
get redisPublisher() {
|
|
@@ -35,114 +34,78 @@ class Streams {
|
|
|
35
34
|
* const streams = new Streams('POS');
|
|
36
35
|
*/
|
|
37
36
|
constructor(serviceName) {
|
|
38
|
-
var _a, _b;
|
|
39
37
|
this.eventsListened = [];
|
|
40
|
-
this.instanceUniqueId =
|
|
38
|
+
this.instanceUniqueId = process.env['INSTANCE_ID'] ?? (0, id_1.generateID)('HEX', 'FE');
|
|
41
39
|
this.instanceId = `${serviceName}:${this.instanceUniqueId}`;
|
|
42
40
|
this.consumerGroupName = `cg-${serviceName}`;
|
|
43
41
|
console.log(`PUBLISHER: Instance ID: ${this.instanceId}`);
|
|
44
|
-
const cleanUpInterval =
|
|
45
|
-
setTimeout(() => this.runClear(cleanUpInterval), 60000);
|
|
42
|
+
const cleanUpInterval = parseInt(process.env['CLEANUP_INTERVAL'] || `${1000 * 60 * 60}`, 10) ?? 1000 * 60 * 60;
|
|
46
43
|
this.cleanUpTimer = setInterval(() => {
|
|
47
44
|
this.runClear(cleanUpInterval);
|
|
48
45
|
}, cleanUpInterval);
|
|
49
46
|
}
|
|
50
|
-
runClear(cleanUpInterval) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* This process scans and claims any unclaimed message according to the cleanup interval.
|
|
61
|
-
* This triggers a cascaded reaction down the chain as message by message is claimed and processed
|
|
62
|
-
*/
|
|
63
|
-
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
64
|
-
yield this.scanAndClaimAUnclaimedMessage(streamName).catch(publisherErrorHandler);
|
|
65
|
-
console.log(`Unclaimed messages for ${streamName}`);
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
isDuplicateMessage(streamName, messageId) {
|
|
71
|
-
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
72
|
-
const processedMessagesKey = `pm:${this.consumerGroupName}:${streamName}`;
|
|
73
|
-
const temp = yield Promise.race([
|
|
74
|
-
this.redisGroups.zscore(processedMessagesKey, messageId),
|
|
75
|
-
/** ioRedis doesnt seem to return the nil event. So waiting for 100ms before moving on */
|
|
76
|
-
new Promise((res) => setTimeout(() => res(null), 100)),
|
|
77
|
-
]);
|
|
78
|
-
return temp !== null;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
clearDuplicationCheckKeys() {
|
|
82
|
-
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
83
|
-
const processedMessagesKeyPattern = `pm:${this.consumerGroupName}:*`;
|
|
84
|
-
let cursor = '0';
|
|
85
|
-
do {
|
|
86
|
-
const [nextCursor, keys] = yield this.redisGroups.scan(cursor, 'MATCH', processedMessagesKeyPattern);
|
|
87
|
-
cursor = nextCursor;
|
|
88
|
-
for (const key of keys) {
|
|
89
|
-
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
90
|
-
yield this.redisGroups.zremrangebyscore(key, '-inf', oneHourAgo);
|
|
91
|
-
}
|
|
92
|
-
} while (cursor !== '0');
|
|
93
|
-
});
|
|
47
|
+
async runClear(cleanUpInterval) {
|
|
48
|
+
console.log('PUBLISHER: Running Clearance', this.eventsListened);
|
|
49
|
+
for (const eventName of this.eventsListened) {
|
|
50
|
+
process.nextTick(async () => {
|
|
51
|
+
/** This removes the messages from the stream after they have been processed according to cleanup interval */
|
|
52
|
+
await this.cleanupAcknowledgedMessages(eventName, cleanUpInterval).catch(publisherErrorHandler);
|
|
53
|
+
console.log(`Cleanup process for Acknowledged messages completed for ${eventName}`);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
94
56
|
}
|
|
95
|
-
publish(data) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
console.error(`PUBLISHER: Error while publishing event for service ${this.consumerGroupName} with instance ${this.instanceId}: `, error);
|
|
115
|
-
throw new Error('Publisher Error');
|
|
57
|
+
async publish(data) {
|
|
58
|
+
if (data.eventId)
|
|
59
|
+
data.republishEvent = data.eventId;
|
|
60
|
+
data.eventId = (0, id_1.generateID)('HEX', 'FF');
|
|
61
|
+
if (!data.createdAt)
|
|
62
|
+
data.createdAt = Date.now();
|
|
63
|
+
const consumerGroups = await (0, utils_1.getAllConsumerGroups)(data.eventName, this.redisPublisher);
|
|
64
|
+
let key = '*';
|
|
65
|
+
if (consumerGroups.length > 0) {
|
|
66
|
+
console.log(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')}`);
|
|
67
|
+
try {
|
|
68
|
+
for (const consumerGroup of consumerGroups) {
|
|
69
|
+
// Publish the event to each consumer group's stream
|
|
70
|
+
const streamName = `${data.eventName}:${consumerGroup}`;
|
|
71
|
+
const generatedKey = await this.redisPublisher
|
|
72
|
+
.xadd(streamName, key, 'data', JSON.stringify(data))
|
|
73
|
+
.catch((e) => console.error(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(data)}, ${e} `));
|
|
74
|
+
if (key === '*')
|
|
75
|
+
key = generatedKey ?? key;
|
|
116
76
|
}
|
|
77
|
+
await (0, utils_1.notifySubscribers)(this.redisPublisher, data.eventName, key);
|
|
117
78
|
}
|
|
118
|
-
|
|
119
|
-
console.
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0) {
|
|
123
|
-
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
124
|
-
const currentTime = new Date();
|
|
125
|
-
delete eventData.repeatInterval;
|
|
126
|
-
if (repeatInterval > 0) {
|
|
127
|
-
eventData.repeatInterval = repeatInterval;
|
|
128
|
-
}
|
|
129
|
-
if (scheduledTime < currentTime) {
|
|
130
|
-
throw new Error('PUBLISHER: Cannot schedule an event in the past');
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error(`PUBLISHER: Error while publishing event for service ${this.consumerGroupName} with instance ${this.instanceId}: `, error);
|
|
81
|
+
throw new Error('Publisher Error');
|
|
131
82
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
console.log(`PUBLISHER: Event publish failed for event ${data.eventName}, reason: no consumers ${consumerGroups}`);
|
|
86
|
+
}
|
|
87
|
+
async scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0) {
|
|
88
|
+
const currentTime = new Date();
|
|
89
|
+
delete eventData.repeatInterval;
|
|
90
|
+
if (repeatInterval > 0) {
|
|
91
|
+
eventData.repeatInterval = repeatInterval;
|
|
92
|
+
}
|
|
93
|
+
if (scheduledTime < currentTime) {
|
|
94
|
+
throw new Error('PUBLISHER: Cannot schedule an event in the past');
|
|
95
|
+
}
|
|
96
|
+
else if (Math.abs(scheduledTime.getTime() - currentTime.getTime()) <= 500) {
|
|
97
|
+
await this.publish(eventData);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (uniquePerInstance === true) {
|
|
101
|
+
const existingJob = await this.redisPublisher.zscore('se', JSON.stringify(eventData));
|
|
102
|
+
if (existingJob) {
|
|
103
|
+
console.log(`PUBLISHER: Job with data '${eventData}' already exists. Skipping.`);
|
|
104
|
+
return;
|
|
142
105
|
}
|
|
143
|
-
yield this.redisPublisher.zadd('se', scheduledTime.getTime(), JSON.stringify(eventData));
|
|
144
106
|
}
|
|
145
|
-
|
|
107
|
+
await this.redisPublisher.zadd('se', scheduledTime.getTime(), JSON.stringify(eventData));
|
|
108
|
+
}
|
|
146
109
|
}
|
|
147
110
|
/**
|
|
148
111
|
* Listens for events with the given name and returns an Observable that emits an EventData<T> object
|
|
@@ -184,98 +147,75 @@ class Streams {
|
|
|
184
147
|
return (0, rxjs_1.throwError)(() => new Error(error.message));
|
|
185
148
|
}));
|
|
186
149
|
}
|
|
187
|
-
createConsumerAndRegister(eventName) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
console.log(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
|
|
200
|
-
});
|
|
201
|
-
const createConsumerStatus = (yield this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId));
|
|
202
|
-
yield this.redisGroups.sadd(key, eventName);
|
|
203
|
-
const addToCGSet = yield this.redisGroups.sadd(`${eventName}`, this.consumerGroupName);
|
|
204
|
-
const addToFlushSet = yield this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName);
|
|
205
|
-
console.log(`PUBLISHER: Consumer Registered and created with ${this.instanceId} under ${this.consumerGroupName} with ${createConsumerStatus} consumers and with the following status ${JSON.stringify({ addToCGSet, addToFlushSet })}`);
|
|
206
|
-
return createConsumerStatus === 0 || createConsumerStatus === 1;
|
|
150
|
+
async createConsumerAndRegister(eventName) {
|
|
151
|
+
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
152
|
+
const key = `instance:${this.instanceId}:subscribedEvents`;
|
|
153
|
+
const setKeyForK8sHandling = `instance:${this.instanceUniqueId}:consumerGroupName`;
|
|
154
|
+
this.eventsListened.push(eventName);
|
|
155
|
+
await this.redisGroups
|
|
156
|
+
.xgroup('CREATE', streamName, this.consumerGroupName, '0', 'MKSTREAM')
|
|
157
|
+
.then(() => {
|
|
158
|
+
console.log(`Group created created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
|
|
159
|
+
})
|
|
160
|
+
.catch((e) => {
|
|
161
|
+
console.error(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
|
|
207
162
|
});
|
|
163
|
+
const createConsumerStatus = (await this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId));
|
|
164
|
+
await this.redisGroups.sadd(key, eventName);
|
|
165
|
+
const addToCGSet = await this.redisGroups.sadd(`${eventName}`, this.consumerGroupName);
|
|
166
|
+
const addToFlushSet = await this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName);
|
|
167
|
+
console.log(`PUBLISHER: Consumer Registered and created with ${this.instanceId} under ${this.consumerGroupName} with ${createConsumerStatus} consumers and with the following status ${JSON.stringify({ addToCGSet, addToFlushSet })}`);
|
|
168
|
+
return createConsumerStatus === 0 || createConsumerStatus === 1;
|
|
208
169
|
}
|
|
209
170
|
listenInternals(eventName) {
|
|
210
|
-
/** Create the return observable */
|
|
211
171
|
const bs = new rxjs_1.BehaviorSubject(null);
|
|
212
172
|
const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1));
|
|
213
|
-
/** This gets called the first time the stream is registered to pickup any messages from the previous subscription */
|
|
214
173
|
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
215
|
-
const processMessage = (redisClient
|
|
216
|
-
|
|
217
|
-
setTimeout(resolve, 1300, 'RACE');
|
|
218
|
-
});
|
|
219
|
-
console.log(`PUBLISHER: processMessage called for ${streamName} cgn: ${this.consumerGroupName} inst: ${this.instanceId}`);
|
|
174
|
+
const processMessage = async (redisClient, messageId, processPending = false) => {
|
|
175
|
+
console.log(`PUBLISHER: Processing message ${messageId} for ${streamName}`);
|
|
220
176
|
try {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
bs.next(eventData);
|
|
242
|
-
const pmKey = `pm:${this.consumerGroupName}:${streamName}`;
|
|
243
|
-
const currentTime = Date.now();
|
|
244
|
-
yield redisClient.zadd(pmKey, currentTime, messageId);
|
|
245
|
-
yield redisClient.xack(streamName, this.consumerGroupName, id);
|
|
246
|
-
yield redisClient.zadd(`ack:${streamName}`, Date.now(), id);
|
|
177
|
+
const messages = await redisClient.xrange(streamName, messageId, messageId);
|
|
178
|
+
if (messages && messages.length) {
|
|
179
|
+
const eventData = JSON.parse(messages[0][1][1]);
|
|
180
|
+
bs.next(eventData);
|
|
181
|
+
await redisClient.xack(streamName, this.consumerGroupName, messageId);
|
|
182
|
+
await redisClient.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.warn(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
|
|
186
|
+
}
|
|
187
|
+
/** Process Unprocessed Message if this is a main tree, otherwise limit to processing 100 messages that are unacknowledged */
|
|
188
|
+
if (!processPending) {
|
|
189
|
+
const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, 25);
|
|
190
|
+
if (unprocessedMessageIds.count > 25) {
|
|
191
|
+
console.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
|
|
192
|
+
}
|
|
193
|
+
for (const id of unprocessedMessageIds.messageIds) {
|
|
194
|
+
console.log(`PUBLISHER: Reporcessing unprocessed message with id: ${id}`);
|
|
195
|
+
await processMessage(redisClient, id, true);
|
|
247
196
|
}
|
|
248
197
|
}
|
|
249
|
-
this.scanAndClaimAUnclaimedMessage(streamName)
|
|
250
|
-
.then()
|
|
251
|
-
.catch((e) => console.log('PUBLISHER: Err in handling unclaimed Messages ' + e.message));
|
|
252
198
|
}
|
|
253
199
|
catch (e) {
|
|
254
|
-
console.error(`PUBLISHER: ${
|
|
200
|
+
console.error(`PUBLISHER: Error processing message ${messageId} for ${streamName}`, e);
|
|
255
201
|
}
|
|
256
|
-
}
|
|
202
|
+
};
|
|
203
|
+
/** Register the consumer and setup the Observable */
|
|
257
204
|
this.createConsumerAndRegister(eventName)
|
|
258
205
|
.then((consumerRegistered) => {
|
|
259
206
|
if (!consumerRegistered)
|
|
260
207
|
throw new Error('PUBLISHER: Cannot setup consumer');
|
|
261
|
-
/** Create new REDIS connection and subscribe */
|
|
262
208
|
const eventStreamClient = registry_1.RedisRegistry.getConnection(`sub-${eventName}`);
|
|
263
209
|
eventStreamClient.subscribe(eventName).then(() => {
|
|
264
|
-
console.log(`PUBLISHER: Redis Subscription connection initiated for ${eventName}
|
|
265
|
-
|
|
266
|
-
|
|
210
|
+
console.log(`PUBLISHER: Redis Subscription connection initiated for ${eventName}`);
|
|
211
|
+
});
|
|
212
|
+
eventStreamClient.on('message', async (channel, messageId) => {
|
|
213
|
+
console.log(`PUBLISHER: Stream Notification Received for event ${eventName} with message ID ${messageId}`);
|
|
214
|
+
await processMessage(this.redisGroups, messageId);
|
|
267
215
|
});
|
|
268
|
-
eventStreamClient.on('message', () => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
269
|
-
console.log(`PUBLISHER: Stream Notification Received for event ${eventName}`);
|
|
270
|
-
yield processMessage(this.redisGroups);
|
|
271
|
-
}));
|
|
272
|
-
this.scanAndClaimAUnclaimedMessage(streamName)
|
|
273
|
-
.then()
|
|
274
|
-
.catch((e) => console.log('PUBLISHER: Err in handling unclaimed Messages ' + e.message));
|
|
275
216
|
})
|
|
276
217
|
.catch((e) => {
|
|
277
|
-
console.error(`PUBLISHER: ${
|
|
278
|
-
throw e;
|
|
218
|
+
console.error(`PUBLISHER: Error during consumer registration for ${eventName}`, e);
|
|
279
219
|
});
|
|
280
220
|
return observable;
|
|
281
221
|
}
|
|
@@ -299,50 +239,32 @@ class Streams {
|
|
|
299
239
|
* process.exit(0);
|
|
300
240
|
* }
|
|
301
241
|
*/
|
|
302
|
-
close() {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
scanAndClaimAUnclaimedMessage(streamName) {
|
|
319
|
-
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
320
|
-
const rows = yield this.redisGroups.xautoclaim(streamName, this.consumerGroupName, this.instanceId, 500, '0-0', 'COUNT', 1);
|
|
321
|
-
if (rows && rows[0] !== '0-0') {
|
|
322
|
-
console.log(`PUBLISHER: Handling pending unclaimed Message from ${streamName} for ${this.instanceId}`);
|
|
323
|
-
yield this.redisPublisher.publish(streamName.split(':')[0], '');
|
|
324
|
-
return this.scanAndClaimAUnclaimedMessage(streamName);
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
console.log(`PUBLISHER: No previous messages found for ${streamName}`);
|
|
328
|
-
}
|
|
329
|
-
return;
|
|
330
|
-
});
|
|
242
|
+
async close() {
|
|
243
|
+
if (this.cleanUpTimer) {
|
|
244
|
+
clearInterval(this.cleanUpTimer);
|
|
245
|
+
}
|
|
246
|
+
if (this.redisPublisher) {
|
|
247
|
+
await this.redisPublisher.quit();
|
|
248
|
+
}
|
|
249
|
+
for (const eventName of this.eventsListened) {
|
|
250
|
+
registry_1.RedisRegistry.getConnection(`sub-${eventName}`).quit();
|
|
251
|
+
}
|
|
252
|
+
if (this.redisGroups) {
|
|
253
|
+
await this.redisGroups.quit();
|
|
254
|
+
}
|
|
331
255
|
}
|
|
332
|
-
cleanupAcknowledgedMessages(eventName, interval = 60 * 60 * 1000) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
yield this.redisGroups.xdel(streamName, messageId);
|
|
341
|
-
}
|
|
342
|
-
// Remove acknowledged messages from the Sorted Set
|
|
343
|
-
yield this.redisGroups.zremrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
|
|
256
|
+
async cleanupAcknowledgedMessages(eventName, interval = 60 * 60 * 1000) {
|
|
257
|
+
const streamName = `${eventName}:${this.consumerGroupName}`;
|
|
258
|
+
const cleanupThreshold = Date.now() - interval;
|
|
259
|
+
const acknowledgedMessages = await this.redisGroups.zrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
|
|
260
|
+
if (acknowledgedMessages && acknowledgedMessages.length > 0) {
|
|
261
|
+
// Remove acknowledged messages from the stream
|
|
262
|
+
for (const messageId of acknowledgedMessages) {
|
|
263
|
+
await this.redisGroups.xdel(streamName, messageId);
|
|
344
264
|
}
|
|
345
|
-
|
|
265
|
+
// Remove acknowledged messages from the Sorted Set
|
|
266
|
+
await this.redisGroups.zremrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
|
|
267
|
+
}
|
|
346
268
|
}
|
|
347
269
|
}
|
|
348
270
|
exports.Streams = Streams;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RedisType } from './registry';
|
|
2
|
+
export declare function getAllConsumerGroups(eventName: string, redisConnection: RedisType): Promise<string[]>;
|
|
3
|
+
export declare function getUnacknowledgedMessages(redisClient: RedisType, consumerGroupName: string, streamName: string, count?: number): Promise<{
|
|
4
|
+
count: number;
|
|
5
|
+
messageIds: string[];
|
|
6
|
+
messages?: unknown[];
|
|
7
|
+
}>;
|
|
8
|
+
export declare function getMessageStatesCount(redisClient: RedisType, streamName: string, consumerGroup: string): Promise<{
|
|
9
|
+
acknowledged: number;
|
|
10
|
+
unacknowledged: number;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function notifySubscribers(redisClient: RedisType, eventName: string, messageId: string): Promise<void>;
|
|
13
|
+
export declare const UTILS: {
|
|
14
|
+
getMessageStatesCount: typeof getMessageStatesCount;
|
|
15
|
+
getUnacknowledgedMessages: typeof getUnacknowledgedMessages;
|
|
16
|
+
getAllConsumerGroupsForEvent: typeof getAllConsumerGroups;
|
|
17
|
+
notifySubscribers: typeof notifySubscribers;
|
|
18
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UTILS = exports.notifySubscribers = exports.getMessageStatesCount = exports.getUnacknowledgedMessages = exports.getAllConsumerGroups = void 0;
|
|
4
|
+
async function getAllConsumerGroups(eventName, redisConnection) {
|
|
5
|
+
const consumerGroups = await redisConnection.smembers(`${eventName}`);
|
|
6
|
+
return consumerGroups;
|
|
7
|
+
}
|
|
8
|
+
exports.getAllConsumerGroups = getAllConsumerGroups;
|
|
9
|
+
async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, count = 500) {
|
|
10
|
+
try {
|
|
11
|
+
// Get pending messages summary
|
|
12
|
+
const summary = await redisClient.xpending(streamName, consumerGroupName);
|
|
13
|
+
if (!summary || summary[1] === 0) {
|
|
14
|
+
// If count is zero
|
|
15
|
+
return { count: 0, messageIds: [] };
|
|
16
|
+
}
|
|
17
|
+
// Use the smallest and largest IDs to get a detailed range
|
|
18
|
+
const pendingMessageCount = summary[1];
|
|
19
|
+
// Get detailed information in the range
|
|
20
|
+
const pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, '-', '+', count));
|
|
21
|
+
return {
|
|
22
|
+
count: pendingMessageCount,
|
|
23
|
+
messageIds: pendingMessages.map((message) => message[0]),
|
|
24
|
+
messages: pendingMessages,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(`PUBLISHER: Error fetching unacknowledged messages for ${streamName}`, error);
|
|
29
|
+
return { count: 0, messageIds: [] };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.getUnacknowledgedMessages = getUnacknowledgedMessages;
|
|
33
|
+
async function getMessageStatesCount(redisClient, streamName, consumerGroup) {
|
|
34
|
+
try {
|
|
35
|
+
const pendingInfo = (await redisClient.xpending(streamName, consumerGroup));
|
|
36
|
+
const totalCount = await redisClient.xlen(streamName);
|
|
37
|
+
return {
|
|
38
|
+
acknowledged: totalCount - pendingInfo[1],
|
|
39
|
+
unacknowledged: pendingInfo[1],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error(`PUBLISHER: Error fetching message states count for ${streamName}`, error);
|
|
44
|
+
return { acknowledged: 0, unacknowledged: 0 };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.getMessageStatesCount = getMessageStatesCount;
|
|
48
|
+
async function notifySubscribers(redisClient, eventName, messageId) {
|
|
49
|
+
await redisClient.publish(eventName, messageId);
|
|
50
|
+
}
|
|
51
|
+
exports.notifySubscribers = notifySubscribers;
|
|
52
|
+
exports.UTILS = {
|
|
53
|
+
getMessageStatesCount,
|
|
54
|
+
getUnacknowledgedMessages,
|
|
55
|
+
getAllConsumerGroupsForEvent: getAllConsumerGroups,
|
|
56
|
+
notifySubscribers,
|
|
57
|
+
};
|
package/src/lib/redis/groups.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getAllConsumerGroups = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
|
-
function getAllConsumerGroups(eventName, redisConnection) {
|
|
6
|
-
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
7
|
-
const consumerGroups = yield redisConnection.smembers(`${eventName}`);
|
|
8
|
-
return consumerGroups;
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
exports.getAllConsumerGroups = getAllConsumerGroups;
|