@jetit/publisher 5.5.1 → 5.6.1
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 +10 -0
- package/package.json +1 -1
- package/src/lib/publisher.d.ts +1 -1
- package/src/lib/redis/registry.d.ts +7 -7
- package/src/lib/redis/registry.js +46 -32
- package/src/lib/redis/scheduler.d.ts +2 -1
- package/src/lib/redis/scheduler.js +4 -3
- package/src/lib/redis/streams.d.ts +2 -1
- package/src/lib/redis/streams.js +36 -6
package/README.md
CHANGED
|
@@ -102,6 +102,16 @@ const config: Partial<IStreamsConfig> = {
|
|
|
102
102
|
const publisher = new Publisher('MyService', config);
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
Additionally, the `Publisher` constructor accepts a redisConnectionId parameter, which is used to identify the connection used by the publisher. This is useful when using multiple connections in a environment.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
setRedisConfig(options1, 'redis-connection-id');
|
|
109
|
+
const publisher1 = new Publisher('MyService', config, 'redis-connection-id'); // <-- use this connection (options1) for publishing
|
|
110
|
+
|
|
111
|
+
setRedisConfig(options2, 'another-redis-connection-id');
|
|
112
|
+
const publisher2 = new Publisher('MyService', config, 'another-redis-connection-id'); // <-- use this connection (options2) for publishing
|
|
113
|
+
```
|
|
114
|
+
|
|
105
115
|
### Publishing Events
|
|
106
116
|
|
|
107
117
|
```typescript
|
package/package.json
CHANGED
package/src/lib/publisher.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { ScheduledProcessor as __SCHEDULER_INTERNALS__ } from './redis/scheduler
|
|
|
4
4
|
export { UTILS as StreamUtilityFunctions } from './redis/utils';
|
|
5
5
|
export { PrometheusAdapter } from './monitoring/adapters/prom';
|
|
6
6
|
export { publishBatch, publishScheduledBatch } from './redis/batch';
|
|
7
|
-
export { IListenOptions, IStreamsConfig, TEventFilter } from './redis/types';
|
|
7
|
+
export type { EventData, IListenOptions, IOptions, IStreamsConfig, PublishData, TEventFilter } from './redis/types';
|
|
@@ -4,12 +4,12 @@ export type RedisType = Redis | Cluster;
|
|
|
4
4
|
export declare class RedisRegistry {
|
|
5
5
|
private static registry;
|
|
6
6
|
private static options;
|
|
7
|
-
static attemptConnection(connectionKey: string, storeRef?: number): RedisType;
|
|
8
|
-
static handleDisconnects(connection: RedisType, connectionKey: string, storeRef: number): void;
|
|
9
|
-
static handlePing(connection: RedisType): void;
|
|
10
|
-
static getConnection(connectionType?: string, storeRef?: number): RedisType;
|
|
11
|
-
static setOptions(options: IOptions): void;
|
|
12
|
-
static _getOptions(): IOptions;
|
|
7
|
+
static attemptConnection(redisConnectionId: string, connectionKey: string, storeRef?: number): RedisType;
|
|
8
|
+
static handleDisconnects(redisConnectionId: string, connection: RedisType, connectionKey: string, storeRef: number): void;
|
|
9
|
+
static handlePing(redisConnectionId: string, connection: RedisType): void;
|
|
10
|
+
static getConnection(redisConnectionId: string, connectionType?: string, storeRef?: number): RedisType;
|
|
11
|
+
static setOptions(options: IOptions, redisConnectionId: string): void;
|
|
12
|
+
static _getOptions(redisConnectionId: string): IOptions;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* This function is used to set Redis Connection options per instance. If no
|
|
@@ -19,4 +19,4 @@ export declare class RedisRegistry {
|
|
|
19
19
|
*
|
|
20
20
|
* @param options
|
|
21
21
|
*/
|
|
22
|
-
export declare function setRedisConnectionSettings(options: IOptions): void;
|
|
22
|
+
export declare function setRedisConnectionSettings(options: IOptions, redisConnectionId?: string): void;
|
|
@@ -5,35 +5,40 @@ exports.setRedisConnectionSettings = setRedisConnectionSettings;
|
|
|
5
5
|
const ioredis_1 = require("ioredis");
|
|
6
6
|
const logger_1 = require("./logger");
|
|
7
7
|
class RedisRegistry {
|
|
8
|
-
static attemptConnection(connectionKey, storeRef = 0) {
|
|
8
|
+
static attemptConnection(redisConnectionId, connectionKey, storeRef = 0) {
|
|
9
9
|
let ref;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const options = RedisRegistry.options[redisConnectionId];
|
|
11
|
+
if (!options)
|
|
12
|
+
throw new Error(`PUBLISHER: Redis config key ${redisConnectionId} not found`);
|
|
13
|
+
if (options.cluster) {
|
|
14
|
+
ref = new ioredis_1.Cluster(options.cluster.nodes, {
|
|
15
|
+
...options.cluster.options,
|
|
13
16
|
redisOptions: {
|
|
14
|
-
...
|
|
17
|
+
...options.cluster.options.redisOptions,
|
|
15
18
|
db: storeRef,
|
|
16
19
|
},
|
|
17
20
|
});
|
|
18
21
|
}
|
|
19
22
|
else
|
|
20
23
|
ref = new ioredis_1.default({
|
|
21
|
-
...
|
|
24
|
+
...options.redis,
|
|
22
25
|
db: storeRef,
|
|
23
26
|
});
|
|
24
|
-
RedisRegistry.registry
|
|
25
|
-
|
|
27
|
+
if (!RedisRegistry.registry[connectionKey])
|
|
28
|
+
RedisRegistry.registry[connectionKey] = new Map();
|
|
29
|
+
RedisRegistry.registry[connectionKey].set(redisConnectionId, ref);
|
|
30
|
+
RedisRegistry.handleDisconnects(redisConnectionId, ref, connectionKey, storeRef);
|
|
26
31
|
return ref;
|
|
27
32
|
}
|
|
28
|
-
static handleDisconnects(connection, connectionKey, storeRef) {
|
|
33
|
+
static handleDisconnects(redisConnectionId, connection, connectionKey, storeRef) {
|
|
29
34
|
connection.on('error', (error) => {
|
|
30
35
|
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Redis connection error : ${error.message}`);
|
|
31
36
|
connection.removeAllListeners();
|
|
32
37
|
connection.disconnect();
|
|
33
|
-
RedisRegistry.attemptConnection(connectionKey, storeRef);
|
|
38
|
+
RedisRegistry.attemptConnection(redisConnectionId, connectionKey, storeRef);
|
|
34
39
|
});
|
|
35
40
|
}
|
|
36
|
-
static handlePing(connection) {
|
|
41
|
+
static handlePing(redisConnectionId, connection) {
|
|
37
42
|
setInterval(async () => {
|
|
38
43
|
const res = await Promise.race([
|
|
39
44
|
connection.ping(),
|
|
@@ -45,52 +50,61 @@ class RedisRegistry {
|
|
|
45
50
|
]);
|
|
46
51
|
if (res === 'NO_PING') {
|
|
47
52
|
connection.disconnect(true);
|
|
48
|
-
logger_1.PUBLISHER_LOGGER.error('PUBLISHER: failed to ping redis, disconnecting and restarting service
|
|
53
|
+
logger_1.PUBLISHER_LOGGER.error('PUBLISHER: failed to ping redis, disconnecting and restarting service for redisConnectionId : ' + redisConnectionId);
|
|
49
54
|
process.exit(0);
|
|
50
55
|
}
|
|
51
56
|
}, 2000);
|
|
52
57
|
}
|
|
53
|
-
static getConnection(connectionType = 'primary', storeRef = 0) {
|
|
58
|
+
static getConnection(redisConnectionId, connectionType = 'primary', storeRef = 0) {
|
|
54
59
|
const connectionKey = `${connectionType}${storeRef}`;
|
|
55
|
-
|
|
60
|
+
const options = RedisRegistry.options[redisConnectionId];
|
|
61
|
+
if (!options)
|
|
62
|
+
throw new Error(`PUBLISHER: Redis config key ${redisConnectionId} not found`);
|
|
63
|
+
if (!this.registry[redisConnectionId])
|
|
64
|
+
this.registry[redisConnectionId] = new Map();
|
|
65
|
+
let ref = this.registry[redisConnectionId].get(connectionKey);
|
|
56
66
|
if (!ref) {
|
|
57
|
-
if (
|
|
58
|
-
ref = new ioredis_1.Cluster(
|
|
59
|
-
...
|
|
67
|
+
if (options.cluster) {
|
|
68
|
+
ref = new ioredis_1.Cluster(options.cluster.nodes, {
|
|
69
|
+
...options.cluster.options,
|
|
60
70
|
redisOptions: {
|
|
61
|
-
...
|
|
71
|
+
...options.cluster.options.redisOptions,
|
|
62
72
|
db: storeRef,
|
|
63
73
|
},
|
|
64
74
|
});
|
|
65
75
|
}
|
|
66
|
-
else if (
|
|
76
|
+
else if (options.sentinels) {
|
|
67
77
|
ref = new ioredis_1.default({
|
|
68
|
-
...
|
|
69
|
-
sentinels:
|
|
78
|
+
...options.sentinels.options,
|
|
79
|
+
sentinels: options.sentinels.nodes,
|
|
70
80
|
});
|
|
71
81
|
}
|
|
72
82
|
else {
|
|
73
83
|
ref = new ioredis_1.default({
|
|
74
|
-
...
|
|
84
|
+
...options.redis,
|
|
75
85
|
db: storeRef,
|
|
76
86
|
});
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
return ref;
|
|
80
90
|
}
|
|
81
|
-
static setOptions(options) {
|
|
82
|
-
RedisRegistry.options = options;
|
|
91
|
+
static setOptions(options, redisConnectionId) {
|
|
92
|
+
RedisRegistry.options[redisConnectionId] = options;
|
|
83
93
|
}
|
|
84
|
-
static _getOptions() {
|
|
85
|
-
return RedisRegistry.options;
|
|
94
|
+
static _getOptions(redisConnectionId) {
|
|
95
|
+
return RedisRegistry.options[redisConnectionId];
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
98
|
exports.RedisRegistry = RedisRegistry;
|
|
89
|
-
RedisRegistry.registry =
|
|
99
|
+
RedisRegistry.registry = {
|
|
100
|
+
default: new Map(),
|
|
101
|
+
};
|
|
90
102
|
RedisRegistry.options = {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
default: {
|
|
104
|
+
redis: {
|
|
105
|
+
port: parseInt(process.env['REDIS_PORT'] ?? '6379'),
|
|
106
|
+
host: process.env['REDIS_HOST'] ?? 'localhost',
|
|
107
|
+
},
|
|
94
108
|
},
|
|
95
109
|
};
|
|
96
110
|
/**
|
|
@@ -101,6 +115,6 @@ RedisRegistry.options = {
|
|
|
101
115
|
*
|
|
102
116
|
* @param options
|
|
103
117
|
*/
|
|
104
|
-
function setRedisConnectionSettings(options) {
|
|
105
|
-
RedisRegistry.setOptions(options);
|
|
118
|
+
function setRedisConnectionSettings(options, redisConnectionId = 'default') {
|
|
119
|
+
RedisRegistry.setOptions(options, redisConnectionId);
|
|
106
120
|
}
|
|
@@ -4,11 +4,12 @@ import { RedisType } from './registry';
|
|
|
4
4
|
* meant to be used internally by the scheduler application
|
|
5
5
|
*/
|
|
6
6
|
export declare class ScheduledProcessor {
|
|
7
|
+
private redisConnectionId;
|
|
7
8
|
private scheduledMessagesTimer;
|
|
8
9
|
private _redisPublisher?;
|
|
9
10
|
private previousTaskCompleted;
|
|
10
11
|
get redisPublisher(): RedisType;
|
|
11
|
-
constructor(duration?: number);
|
|
12
|
+
constructor(duration?: number, redisConnectionId?: string);
|
|
12
13
|
private processScheduledEvents;
|
|
13
14
|
getAllScheduledEvents(): Promise<Array<string>>;
|
|
14
15
|
close(): Promise<void>;
|
|
@@ -13,10 +13,11 @@ const utils_1 = require("./utils");
|
|
|
13
13
|
class ScheduledProcessor {
|
|
14
14
|
get redisPublisher() {
|
|
15
15
|
if (!this._redisPublisher)
|
|
16
|
-
this._redisPublisher = registry_1.RedisRegistry.getConnection('publish');
|
|
16
|
+
this._redisPublisher = registry_1.RedisRegistry.getConnection(this.redisConnectionId, 'publish');
|
|
17
17
|
return this._redisPublisher;
|
|
18
18
|
}
|
|
19
|
-
constructor(duration = 1000) {
|
|
19
|
+
constructor(duration = 1000, redisConnectionId = 'default') {
|
|
20
|
+
this.redisConnectionId = redisConnectionId;
|
|
20
21
|
this.previousTaskCompleted = true;
|
|
21
22
|
this.scheduledMessagesTimer = (0, rxjs_1.interval)(duration).subscribe(() => {
|
|
22
23
|
logger_1.PUBLISHER_LOGGER.log('Checking Streams messages at ', new Date().toISOString(), '...');
|
|
@@ -62,7 +63,7 @@ class ScheduledProcessor {
|
|
|
62
63
|
.xadd(streamName, key, 'data', JSON.stringify(eventData))
|
|
63
64
|
.catch((e) => logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Publishing event ${eventData.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(eventData)}, ${e} `));
|
|
64
65
|
if (key === '*')
|
|
65
|
-
key = generatedKey ?? key
|
|
66
|
+
key = `${generatedKey ?? key}`;
|
|
66
67
|
}
|
|
67
68
|
if (eventData.repeatInterval) {
|
|
68
69
|
const nextEventTime = currentTime + eventData.repeatInterval;
|
|
@@ -3,6 +3,7 @@ import { IAggregatedMetrics, TQueryableMetrics } from '../monitoring/types';
|
|
|
3
3
|
import { CircuitState } from '../performance/circuit_breaker';
|
|
4
4
|
import { EventData, IListenOptions, IStreamsConfig, PublishData } from './types';
|
|
5
5
|
export declare class Streams {
|
|
6
|
+
private redisConnectionId;
|
|
6
7
|
private _redisPublisher?;
|
|
7
8
|
private _redisGroups?;
|
|
8
9
|
private config;
|
|
@@ -33,7 +34,7 @@ export declare class Streams {
|
|
|
33
34
|
* // Create a new Streams instance for the "POS" service
|
|
34
35
|
* const streams = new Streams('POS');
|
|
35
36
|
*/
|
|
36
|
-
constructor(serviceName: string, config?: Partial<IStreamsConfig
|
|
37
|
+
constructor(serviceName: string, config?: Partial<IStreamsConfig>, redisConnectionId?: string);
|
|
37
38
|
private setupCircuitBreakerListeners;
|
|
38
39
|
private runClear;
|
|
39
40
|
/**
|
package/src/lib/redis/streams.js
CHANGED
|
@@ -14,12 +14,12 @@ const utils_1 = require("./utils");
|
|
|
14
14
|
class Streams {
|
|
15
15
|
get redisPublisher() {
|
|
16
16
|
if (!this._redisPublisher)
|
|
17
|
-
this._redisPublisher = registry_1.RedisRegistry.getConnection('publish');
|
|
17
|
+
this._redisPublisher = registry_1.RedisRegistry.getConnection(this.redisConnectionId, 'publish');
|
|
18
18
|
return this._redisPublisher;
|
|
19
19
|
}
|
|
20
20
|
get redisGroups() {
|
|
21
21
|
if (!this._redisGroups)
|
|
22
|
-
this._redisGroups = registry_1.RedisRegistry.getConnection('groups');
|
|
22
|
+
this._redisGroups = registry_1.RedisRegistry.getConnection(this.redisConnectionId, 'groups');
|
|
23
23
|
return this._redisGroups;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
@@ -36,7 +36,8 @@ class Streams {
|
|
|
36
36
|
* // Create a new Streams instance for the "POS" service
|
|
37
37
|
* const streams = new Streams('POS');
|
|
38
38
|
*/
|
|
39
|
-
constructor(serviceName, config = {}) {
|
|
39
|
+
constructor(serviceName, config = {}, redisConnectionId = 'default') {
|
|
40
|
+
this.redisConnectionId = redisConnectionId;
|
|
40
41
|
this.eventsListened = [];
|
|
41
42
|
this.subscriptions = new Map();
|
|
42
43
|
this.DEFAULT_STREAMS_CONFIG = {
|
|
@@ -367,6 +368,17 @@ class Streams {
|
|
|
367
368
|
* they need to be read using XRANGE.
|
|
368
369
|
*/
|
|
369
370
|
if (multicast || processPending) {
|
|
371
|
+
/**
|
|
372
|
+
* Very very rare case of this occurring. Cases where the running service is super overloaded
|
|
373
|
+
* that causes a messaged to be sent to processing with a delay, but eventually before being
|
|
374
|
+
* processed gets picked up by another instance, leading to multiple publications
|
|
375
|
+
*/
|
|
376
|
+
if (processPending) {
|
|
377
|
+
const claimed = await redisClient.xclaim(streamName, this.consumerGroupName, this.instanceId, 20000, messageId, 'JUSTID');
|
|
378
|
+
if (!claimed || claimed.length === 0) {
|
|
379
|
+
return; // Message already claimed or acknowledged by another consumer, so don't repush to the subscriber
|
|
380
|
+
}
|
|
381
|
+
}
|
|
370
382
|
const messages = await redisClient.xrange(streamName, messageId, messageId);
|
|
371
383
|
if (messages?.length) {
|
|
372
384
|
try {
|
|
@@ -468,13 +480,31 @@ class Streams {
|
|
|
468
480
|
/** Process Unprocessed Messages with rate limiting */
|
|
469
481
|
if (!processPending) {
|
|
470
482
|
const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, this.instanceId);
|
|
483
|
+
/**
|
|
484
|
+
* Dealing with the case where messages don't get processed due to large
|
|
485
|
+
* batch sizes. The previous default was 20ms, which seemed to work in
|
|
486
|
+
* most cases but seem to fail when the number of retry messages are high.
|
|
487
|
+
*
|
|
488
|
+
* The old solution did not take into account the fact that this library
|
|
489
|
+
* is dependent on the infrastructure of the app that runs it, so any memory/
|
|
490
|
+
* resource/stack overload on the app has an impact in this
|
|
491
|
+
*/
|
|
492
|
+
const getDelay = (count) => {
|
|
493
|
+
if (count > 100)
|
|
494
|
+
return 500; // 500ms for large backlogs
|
|
495
|
+
if (count > 50)
|
|
496
|
+
return 300; // 300ms for medium-large
|
|
497
|
+
if (count > 20)
|
|
498
|
+
return 200; // 200ms for medium
|
|
499
|
+
return 100; // 100ms for small
|
|
500
|
+
};
|
|
471
501
|
if (unprocessedMessageIds.countOnThisConsumer &&
|
|
472
502
|
unprocessedMessageIds.countOnThisConsumer > this.config.unprocessedMessageThreshold) {
|
|
473
503
|
logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
|
|
474
504
|
}
|
|
475
505
|
// Process messages with rate limiting
|
|
476
506
|
const processWithDelay = async (id, index) => {
|
|
477
|
-
await new Promise((resolve) => setTimeout(resolve, index *
|
|
507
|
+
await new Promise((resolve) => setTimeout(resolve, index * getDelay(unprocessedMessageIds.countOnThisConsumer ?? 1)));
|
|
478
508
|
await processMessage(redisClient, id, new tracker_1.MetricsTracker(), multicast, true);
|
|
479
509
|
};
|
|
480
510
|
unprocessedMessageIds.messageIds.map((id, index) => processWithDelay(id, index));
|
|
@@ -506,7 +536,7 @@ class Streams {
|
|
|
506
536
|
.then((consumerRegistered) => {
|
|
507
537
|
if (!consumerRegistered)
|
|
508
538
|
throw new Error('PUBLISHER: Cannot setup consumer');
|
|
509
|
-
const eventStreamClient = registry_1.RedisRegistry.getConnection(`sub-${eventName}`);
|
|
539
|
+
const eventStreamClient = registry_1.RedisRegistry.getConnection(this.redisConnectionId, `sub-${eventName}`);
|
|
510
540
|
eventStreamClient.subscribe(eventName).then(() => {
|
|
511
541
|
logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Redis Subscription connection initiated for ${eventName}`);
|
|
512
542
|
});
|
|
@@ -565,7 +595,7 @@ class Streams {
|
|
|
565
595
|
await this._redisPublisher.quit();
|
|
566
596
|
}
|
|
567
597
|
for (const eventName of this.eventsListened) {
|
|
568
|
-
await registry_1.RedisRegistry.getConnection(`sub-${eventName}`).quit();
|
|
598
|
+
await registry_1.RedisRegistry.getConnection(this.redisConnectionId, `sub-${eventName}`).quit();
|
|
569
599
|
}
|
|
570
600
|
if (this._redisGroups) {
|
|
571
601
|
await this._redisGroups.quit();
|