@mojaloop/central-services-shared 18.33.3 → 18.33.4
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/CHANGELOG.md +7 -0
- package/package.json +2 -2
- package/src/util/redis/pubSub.js +39 -8
- package/test/unit/util/redis/pubSub.test.js +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [18.33.4](https://github.com/mojaloop/central-services-shared/compare/v18.33.3...v18.33.4) (2025-09-23)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* recreate redis clients on specific errors ([#484](https://github.com/mojaloop/central-services-shared/issues/484)) ([2b15867](https://github.com/mojaloop/central-services-shared/commit/2b158676c1aee4f47ea0bd43a5449b154dbaa073))
|
|
11
|
+
|
|
5
12
|
### [18.33.3](https://github.com/mojaloop/central-services-shared/compare/v18.33.2...v18.33.3) (2025-09-22)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mojaloop/central-services-shared",
|
|
3
|
-
"version": "18.33.
|
|
3
|
+
"version": "18.33.4",
|
|
4
4
|
"description": "Shared code for mojaloop central services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "ModusBox",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
97
|
"@mojaloop/central-services-error-handling": "13.1.2",
|
|
98
|
-
"@mojaloop/central-services-logger": "11.10.
|
|
98
|
+
"@mojaloop/central-services-logger": "11.10.1",
|
|
99
99
|
"@mojaloop/central-services-metrics": "12.7.1",
|
|
100
100
|
"@mojaloop/event-sdk": "14.7.0",
|
|
101
101
|
"@mojaloop/sdk-standard-components": "19.17.0",
|
package/src/util/redis/pubSub.js
CHANGED
|
@@ -34,6 +34,7 @@ const isClusterConfig = (config) => { return 'cluster' in config }
|
|
|
34
34
|
const { rethrowRedisError } = require('../rethrow')
|
|
35
35
|
const { REDIS_SUCCESS, REDIS_IS_CONNECTED_STATUSES } = require('../../constants')
|
|
36
36
|
const { retryCommand } = require('./shared')
|
|
37
|
+
const RECREATE_RE = /Cannot read properties of undefined \(reading 'getInstance'\)|Too many Cluster redirections/
|
|
37
38
|
|
|
38
39
|
class PubSub {
|
|
39
40
|
/**
|
|
@@ -63,6 +64,36 @@ class PubSub {
|
|
|
63
64
|
: new Redis(this.config)
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
retry (client, fn, log, retryAttempts, retryDelayMs) {
|
|
68
|
+
return retryCommand(
|
|
69
|
+
async () => {
|
|
70
|
+
try {
|
|
71
|
+
return await fn()
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (RECREATE_RE.test(err.message)) {
|
|
74
|
+
if (client === 'publisher') {
|
|
75
|
+
const duplicateClient = this.publisherClient.duplicate()
|
|
76
|
+
log.warn('Recreated publisher due to error: ' + err.message)
|
|
77
|
+
this.publisherClient.quit()
|
|
78
|
+
this.publisherClient = duplicateClient
|
|
79
|
+
this.addEventListeners(duplicateClient)
|
|
80
|
+
} else if (client === 'subscriber') {
|
|
81
|
+
const duplicateClient = this.subscriberClient.duplicate()
|
|
82
|
+
log.warn('Recreated subscriber due to error: ' + err.message)
|
|
83
|
+
this.subscriberClient.quit()
|
|
84
|
+
this.subscriberClient = duplicateClient
|
|
85
|
+
this.addEventListeners(duplicateClient)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw err
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
log,
|
|
92
|
+
retryAttempts,
|
|
93
|
+
retryDelayMs
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
async connect () {
|
|
67
98
|
try {
|
|
68
99
|
await retryCommand(() => this.publisherClient.connect(), this.log, this.retryAttempts, this.retryDelayMs)
|
|
@@ -91,8 +122,8 @@ class PubSub {
|
|
|
91
122
|
|
|
92
123
|
async healthCheck () {
|
|
93
124
|
try {
|
|
94
|
-
const publisherStatus = await
|
|
95
|
-
const subscriberStatus = await
|
|
125
|
+
const publisherStatus = await this.retry('publisher', () => this.publisherClient.ping(), this.log, this.retryAttempts, this.retryDelayMs)
|
|
126
|
+
const subscriberStatus = await this.retry('subscriber', () => this.subscriberClient.ping(), this.log, this.retryAttempts, this.retryDelayMs)
|
|
96
127
|
const isHealthy = publisherStatus === 'PONG' && subscriberStatus === 'PONG'
|
|
97
128
|
this.log.debug(`Redis health check: ${isHealthy ? 'Healthy' : 'Unhealthy'}`)
|
|
98
129
|
return isHealthy
|
|
@@ -123,9 +154,9 @@ class PubSub {
|
|
|
123
154
|
try {
|
|
124
155
|
await this.ensureConnected(this.publisherClient)
|
|
125
156
|
if (this.isCluster) {
|
|
126
|
-
await
|
|
157
|
+
await this.retry('publisher', () => this.publisherClient.spublish(channel, JSON.stringify(message)), this.log, this.retryAttempts, this.retryDelayMs)
|
|
127
158
|
} else {
|
|
128
|
-
await
|
|
159
|
+
await this.retry('publisher', () => this.publisherClient.publish(channel, JSON.stringify(message)), this.log, this.retryAttempts, this.retryDelayMs)
|
|
129
160
|
}
|
|
130
161
|
this.log.info(`Message published to channel: ${channel}`)
|
|
131
162
|
} catch (err) {
|
|
@@ -139,7 +170,7 @@ class PubSub {
|
|
|
139
170
|
await this.ensureConnected(this.subscriberClient)
|
|
140
171
|
let listener
|
|
141
172
|
if (this.isCluster) {
|
|
142
|
-
await
|
|
173
|
+
await this.retry('subscriber', () => this.subscriberClient.ssubscribe(channel), this.log, this.retryAttempts, this.retryDelayMs)
|
|
143
174
|
listener = (subscribedChannel, message) => {
|
|
144
175
|
if (subscribedChannel === channel) {
|
|
145
176
|
callback(JSON.parse(message))
|
|
@@ -148,7 +179,7 @@ class PubSub {
|
|
|
148
179
|
this.subscriberClient.on('smessage', listener)
|
|
149
180
|
this._channelListeners.set(channel, { event: 'smessage', listener })
|
|
150
181
|
} else {
|
|
151
|
-
await
|
|
182
|
+
await this.retry('subscriber', () => this.subscriberClient.subscribe(channel), this.log, this.retryAttempts, this.retryDelayMs)
|
|
152
183
|
listener = (subscribedChannel, message) => {
|
|
153
184
|
if (subscribedChannel === channel) {
|
|
154
185
|
callback(JSON.parse(message))
|
|
@@ -175,9 +206,9 @@ class PubSub {
|
|
|
175
206
|
this._channelListeners.delete(channel)
|
|
176
207
|
}
|
|
177
208
|
if (this.isCluster) {
|
|
178
|
-
await
|
|
209
|
+
await this.retry('subscriber', () => this.subscriberClient.sunsubscribe(channel), this.log, this.retryAttempts, this.retryDelayMs)
|
|
179
210
|
} else {
|
|
180
|
-
await
|
|
211
|
+
await this.retry('subscriber', () => this.subscriberClient.unsubscribe(channel), this.log, this.retryAttempts, this.retryDelayMs)
|
|
181
212
|
}
|
|
182
213
|
this.log.info(`Unsubscribed from channel: ${channel}`)
|
|
183
214
|
} catch (err) {
|
|
@@ -48,6 +48,7 @@ Test('PubSub', (t) => {
|
|
|
48
48
|
connect: sandbox.stub().resolves(),
|
|
49
49
|
quit: sandbox.stub().resolves(),
|
|
50
50
|
on: sandbox.stub().returnsThis(),
|
|
51
|
+
duplicate: sandbox.stub().returnsThis(),
|
|
51
52
|
status: 'ready'
|
|
52
53
|
}
|
|
53
54
|
subscriberClientStub = {
|
|
@@ -61,6 +62,7 @@ Test('PubSub', (t) => {
|
|
|
61
62
|
quit: sandbox.stub().resolves(),
|
|
62
63
|
removeAllListeners: sandbox.stub().resolves(),
|
|
63
64
|
removeListener: sandbox.stub(),
|
|
65
|
+
duplicate: sandbox.stub().returnsThis(),
|
|
64
66
|
status: 'ready'
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -114,6 +116,25 @@ Test('PubSub', (t) => {
|
|
|
114
116
|
t.end()
|
|
115
117
|
})
|
|
116
118
|
|
|
119
|
+
t.test('should recreate publisher when publishing throws a specific error', async (t) => {
|
|
120
|
+
const config = {}
|
|
121
|
+
const pubSub = new PubSub(config, publisherClientStub, subscriberClientStub)
|
|
122
|
+
const channel = 'test-channel'
|
|
123
|
+
const message = { key: 'value' }
|
|
124
|
+
const error = new Error('Too many Cluster redirections')
|
|
125
|
+
|
|
126
|
+
pubSub.publisherClient.publish.rejects(error)
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await pubSub.publish(channel, message)
|
|
130
|
+
t.fail('Should have thrown an error')
|
|
131
|
+
} catch (err) {
|
|
132
|
+
t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
|
|
133
|
+
t.ok(pubSub.publisherClient.duplicate.calledOnce, 'publisherClient duplicate called')
|
|
134
|
+
}
|
|
135
|
+
t.end()
|
|
136
|
+
})
|
|
137
|
+
|
|
117
138
|
t.test('should subscribe to a channel and handle messages', async (t) => {
|
|
118
139
|
const config = {}
|
|
119
140
|
const pubSub = new PubSub(config, publisherClientStub, subscriberClientStub)
|
|
@@ -540,6 +561,27 @@ Test('PubSub', (t) => {
|
|
|
540
561
|
t.end()
|
|
541
562
|
})
|
|
542
563
|
|
|
564
|
+
t.test('should recreate subscriber when subscribing to a channel throws specific error', async (t) => {
|
|
565
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
566
|
+
const error = new Error('Cannot read properties of undefined (reading \'getInstance\')')
|
|
567
|
+
const subscriberClient = {
|
|
568
|
+
...subscriberClientStub,
|
|
569
|
+
ssubscribe: sandbox.stub().rejects(error)
|
|
570
|
+
}
|
|
571
|
+
const pubSub = new PubSub(config, publisherClientStub, subscriberClient)
|
|
572
|
+
const channel = 'cluster-channel'
|
|
573
|
+
const callback = sinon.stub()
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
await pubSub.subscribe(channel, callback)
|
|
577
|
+
t.fail('Should have thrown an error')
|
|
578
|
+
} catch (err) {
|
|
579
|
+
t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
|
|
580
|
+
t.ok(pubSub.subscriberClient.duplicate.calledOnce, 'subscriberClient duplicate called')
|
|
581
|
+
}
|
|
582
|
+
t.end()
|
|
583
|
+
})
|
|
584
|
+
|
|
543
585
|
t.test('should unsubscribe from a channel using sunsubscribe in cluster mode', async (t) => {
|
|
544
586
|
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
545
587
|
const subscriberClient = {
|