@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 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",
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.0",
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",
@@ -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 retryCommand(() => this.publisherClient.ping(), this.log, this.retryAttempts, this.retryDelayMs)
95
- const subscriberStatus = await retryCommand(() => this.subscriberClient.ping(), this.log, this.retryAttempts, this.retryDelayMs)
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 retryCommand(() => this.publisherClient.spublish(channel, JSON.stringify(message)), this.log, this.retryAttempts, this.retryDelayMs)
157
+ await this.retry('publisher', () => this.publisherClient.spublish(channel, JSON.stringify(message)), this.log, this.retryAttempts, this.retryDelayMs)
127
158
  } else {
128
- await retryCommand(() => this.publisherClient.publish(channel, JSON.stringify(message)), this.log, this.retryAttempts, this.retryDelayMs)
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 retryCommand(() => this.subscriberClient.ssubscribe(channel), this.log, this.retryAttempts, this.retryDelayMs)
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 retryCommand(() => this.subscriberClient.subscribe(channel), this.log, this.retryAttempts, this.retryDelayMs)
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 retryCommand(() => this.subscriberClient.sunsubscribe(channel), this.log, this.retryAttempts, this.retryDelayMs)
209
+ await this.retry('subscriber', () => this.subscriberClient.sunsubscribe(channel), this.log, this.retryAttempts, this.retryDelayMs)
179
210
  } else {
180
- await retryCommand(() => this.subscriberClient.unsubscribe(channel), this.log, this.retryAttempts, this.retryDelayMs)
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 = {