@jetit/publisher 1.6.0 → 1.6.2

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 CHANGED
@@ -2,6 +2,164 @@
2
2
 
3
3
  publisher is a library for implementing an event-driven architecture using Redis PUB/SUB and Redis Streams. It provides a simple and scalable mechanism for publishing and consuming events in real-time, and supports features such as message deduplication, consumer group management, and scheduled event publishing.
4
4
 
5
+ ## IMPORTANT NOTE
6
+ This project currently does not have a means to clean up inactive consumers. This means that if you have a consumer that is no longer active, it will continue to receive events until it is removed from the consumer group. This is a known issue and will be addressed in a future release. A workaround is to use the following code to remove inactive consumers from the consumer group as part of your process cleanup:
7
+
8
+ ```javascript
9
+ const ioredis = require(`ioredis`);
10
+
11
+ const env = process.env;
12
+
13
+ async function bootstrap() {
14
+ const connection = new ioredis.Redis({
15
+ host: env.REDIS_HOST,
16
+ port: parseInt(env.REDIS_PORT),
17
+ });
18
+ console.log(`Redis Connection Status ${connection.status}`);
19
+ await waitForSeconds(0.3);
20
+ console.log(`Redis Connection Status ${connection.status}`);
21
+ const instanceUniqueId = env.INSTANCE_ID;
22
+ if (!instanceUniqueId) {
23
+ console.log(`Unique instance ID is not available`);
24
+ return;
25
+ }
26
+ console.log(`Instance Unique ID : ${instanceUniqueId}`);
27
+ const consumerGroupName = await getConsumerGroupName(connection, instanceUniqueId);
28
+ if (!consumerGroupName) {
29
+ console.log(`Consumer is not available, so graceful shutdown is not necessary.`);
30
+ return;
31
+ }
32
+ console.log(`Consumer Group Name : ${consumerGroupName}`);
33
+ const instanceId = getInstanceId(consumerGroupName.slice(3), instanceUniqueId);
34
+ console.log(`Instance ID : ${instanceId}`);
35
+ const subscribedEvents = await getAllEventsForInstance(connection, instanceId);
36
+ console.log(`Subscribed Events : ${JSON.stringify(subscribedEvents)}`);
37
+ await clearSubscribedEvents(connection, consumerGroupName, instanceId, subscribedEvents);
38
+ await deleteConsumerGroupNameForInstance(connection, instanceId);
39
+ await deleteAllEventsFroInstance(connection, instanceId);
40
+ }
41
+
42
+ /**
43
+ *
44
+ * @param {ioredis.Redis|ioredis.Cluster} connection
45
+ * @param {string} consumerGroupName
46
+ * @param {string} instanceId
47
+ * @param {Array<string>} events
48
+ */
49
+ async function clearSubscribedEvents(connection, consumerGroupName, instanceId, events) {
50
+ return Promise.all(
51
+ events.map(async (eventName) => {
52
+ console.log(`${eventName} is being cleared in publisher`);
53
+ const streamName = `${eventName}:${consumerGroupName}`;
54
+ console.log(`${streamName} is being removed.`);
55
+ await connection.srem(`${eventName}`, consumerGroupName);
56
+ console.log(`${eventName} is removed from ${consumerGroupName}`);
57
+ // Releasing all claims based on info from: https://redis.io/commands/xgroup-delconsumer/
58
+ await releaseAllClaims(connection, streamName, consumerGroupName, instanceId);
59
+ console.log(`${eventName} removes all claims`);
60
+ await connection.xgroup(`DELCONSUMER`, streamName, consumerGroupName, instanceId);
61
+ console.log(`${eventName} is deleted as a consumer from ${consumerGroupName}, ${instanceId}`);
62
+ })
63
+ );
64
+ }
65
+
66
+ /**
67
+ *
68
+ * @param {ioredis.Redis|ioredis.Cluster} connection
69
+ * @param {string} streamName
70
+ * @param {string} consumerGroupName
71
+ * @param {string} instanceId
72
+ */
73
+ async function releaseAllClaims(connection, streamName, consumerGroupName, instanceId) {
74
+ /**
75
+ * Retrieve the pending messages for the consumer. Note this only fetches the last
76
+ * 10000 events assigned to this consumer. This function has been modified to make sure
77
+ * that there is a temp instance that claims all this messages
78
+ */
79
+ const pendingMessages = await connection.xpending(streamName, consumerGroupName, `-`, `+`, 10000, instanceId);
80
+
81
+ if (pendingMessages && pendingMessages.length > 0) {
82
+ console.log(`${pendingMessages.length} messages to clean up.`);
83
+ const transaction = connection.multi({ pipeline: true });
84
+ const tempConsumerId = `${consumerGroupName}-temp`;
85
+ for (const [messageId] of pendingMessages) {
86
+ transaction.xclaim(streamName, consumerGroupName, tempConsumerId, 10, messageId);
87
+ }
88
+ await transaction.exec();
89
+ }
90
+ }
91
+
92
+ /**
93
+ *
94
+ * @param {string} serviceName
95
+ * @param {string} instanceUniqueId
96
+ */
97
+ function getInstanceId(serviceName, instanceUniqueId) {
98
+ const instanceId = `${serviceName}:${instanceUniqueId}`;
99
+ console.log(`Generated Instance ID : ${instanceId}`);
100
+ return instanceId;
101
+ }
102
+
103
+ /**
104
+ *
105
+ * @param {ioredis.Redis|ioredis.Cluster} serviceName
106
+ * @param {string} instanceId
107
+ * @returns {Promise<string>} consumer group name
108
+ */
109
+ async function getConsumerGroupName(connection, instanceId) {
110
+ const key = `instance:${instanceId}:consumerGroupName`;
111
+ console.log(`Get consumer group name called for key : ${key}`);
112
+ return await connection.get(key);
113
+ }
114
+
115
+ /**
116
+ *
117
+ * @param {ioredis.Redis|ioredis.Cluster} connection
118
+ * @param {string} instanceId
119
+ * @returns
120
+ */
121
+ async function deleteConsumerGroupNameForInstance(connection, instanceId) {
122
+ return await connection.del(`instance:${instanceId}:consumerGroupName`);
123
+ }
124
+
125
+ /**
126
+ *
127
+ * @param {ioredis.Redis|ioredis.Cluster} connection
128
+ * @param {string} instanceId
129
+ * @returns {Promise<Array<events>>} subscribed events for this instance
130
+ */
131
+ async function getAllEventsForInstance(connection, instanceId) {
132
+ const key = `instance:${instanceId}:subscribedEvents`;
133
+ console.log(`Get consumer group events : ${key}`);
134
+ return (await connection.sscan(key, 0))[1];
135
+ }
136
+
137
+ /**
138
+ *
139
+ * @param {ioredis.Redis|ioredis.Cluster} connection
140
+ * @param {string} instanceId
141
+ */
142
+ async function deleteAllEventsFroInstance(connection, instanceId) {
143
+ return await connection.del(`instance:${instanceId}:subscribedEvents`);
144
+ }
145
+
146
+ async function waitForSeconds(seconds = 10) {
147
+ return new Promise((res, _) => setTimeout(() => res(), seconds * 1000));
148
+ }
149
+
150
+ /**
151
+ * Start
152
+ */
153
+
154
+ bootstrap()
155
+ .then(() => process.exit(0))
156
+ .catch((e) => {
157
+ console.error(e);
158
+ process.exit(1);
159
+ });
160
+
161
+ ```
162
+
5
163
  ## Simple Example
6
164
 
7
165
  ```typescript
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "0.0.11",
7
7
  "ioredis": "^5.3.0",
8
- "rxjs": "^7.0.0"
8
+ "rxjs": "^7.8.0"
9
9
  },
10
10
  "peerDependencies": {
11
11
  "tslib": "2.5.0"
@@ -223,10 +223,20 @@ class Streams {
223
223
  const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1));
224
224
  /** This gets called the first time the stream is registered to pickup any messages from the previous subscription */
225
225
  const streamName = `${eventName}:${this.consumerGroupName}`;
226
- const processMessage = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
226
+ const processMessage = (redisClient) => tslib_1.__awaiter(this, void 0, void 0, function* () {
227
+ const racePr = new Promise((resolve, _) => {
228
+ setTimeout(resolve, 1100, 'RACE');
229
+ });
227
230
  console.log(`PUBLISHER: processMessage called for ${streamName} cgn: ${this.consumerGroupName} inst: ${this.instanceId}`);
228
231
  try {
229
- const result = yield this.redisGroups.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'BLOCK', 0, 'STREAMS', streamName, '>');
232
+ const prResult = yield Promise.race([
233
+ redisClient.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'BLOCK', 1000, 'STREAMS', streamName, '>'),
234
+ racePr,
235
+ ]);
236
+ console.log(`PUBLISHER: Promise race resolved with ${JSON.stringify(prResult)}`);
237
+ if (prResult && prResult === 'RACE')
238
+ return;
239
+ const result = prResult;
230
240
  console.log(`PUBLISHER: XREADGROUP returned with ${JSON.stringify(result[0])}`);
231
241
  if (result) {
232
242
  const [, streamMessages] = result[0];
@@ -236,13 +246,13 @@ class Streams {
236
246
  const isDuplicate = yield this.isDuplicateMessage(streamName, messageId);
237
247
  if (isDuplicate) {
238
248
  console.warn(`Duplicate message detected: ${messageId}`);
239
- yield this.redisGroups.xack(streamName, this.consumerGroupName, id);
249
+ yield redisClient.xack(streamName, this.consumerGroupName, id);
240
250
  continue;
241
251
  }
242
252
  bs.next(eventData);
243
253
  const pmKey = `pm:${this.consumerGroupName}:${streamName}`;
244
254
  const currentTime = Date.now();
245
- const transaction = this.redisGroups.multi({ pipeline: true });
255
+ const transaction = redisClient.multi({ pipeline: true });
246
256
  transaction.zadd(pmKey, currentTime, messageId);
247
257
  transaction.xack(streamName, this.consumerGroupName, id);
248
258
  transaction.zadd(`ack:${streamName}`, Date.now(), id);
@@ -270,7 +280,7 @@ class Streams {
270
280
  });
271
281
  eventStreamClient.on('message', () => tslib_1.__awaiter(this, void 0, void 0, function* () {
272
282
  console.log(`PUBLISHER: Stream Notification Recieved for event ${eventName}`);
273
- yield processMessage();
283
+ yield processMessage(this.redisGroups);
274
284
  }));
275
285
  this.scanAndClaimAUnclaimedMessage(streamName)
276
286
  .then()