From 1875813557617cf17c098b66ba6a302189146203 Mon Sep 17 00:00:00 2001 From: Trofymenko Vladyslav Date: Thu, 18 Sep 2025 17:26:52 +0300 Subject: [PATCH 1/6] cluster/node evets (#1855) --- packages/client/lib/cluster/cluster-slots.ts | 33 +++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 9c75b3ab4b..0955df16b2 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -80,9 +80,9 @@ type PubSubNode< RESP extends RespVersions, TYPE_MAPPING extends TypeMapping > = ( - Omit, 'client'> & - Required, 'client'>> -); + Omit, 'client'> & + Required, 'client'>> + ); type PubSubToResubscribe = Record< PUBSUB_TYPE['CHANNELS'] | PUBSUB_TYPE['PATTERNS'], @@ -153,6 +153,7 @@ export default class RedisClusterSlots< this.#isOpen = true; try { await this.#discoverWithRootNodes(); + this.#emit('connect'); } catch (err) { this.#isOpen = false; throw err; @@ -333,17 +334,26 @@ export default class RedisClusterSlots< } #createClient(node: ShardNode, readonly = node.readonly) { + const socket = + this.#getNodeAddress(node.address) ?? + { host: node.host, port: node.port, }; + const client = Object.freeze({ + host: socket.host, + port: socket.port, + }); + const emit = this.#emit; return this.#clientFactory( this.#clientOptionsDefaults({ clientSideCache: this.clientSideCache, RESP: this.#options.RESP, - socket: this.#getNodeAddress(node.address) ?? { - host: node.host, - port: node.port - }, - readonly - }) - ).on('error', err => console.error(err)); + socket, + readonly, + })) + .on('error', error => emit('node-error', error, client)) + .on('reconnecting', () => emit('node-reconnecting', client)) + .once('ready', () => emit('node-ready', client)) + .once('connect', () => emit('node-connect', client)) + .once('end', () => emit('node-disconnect', client)); } #createNodeClient(node: ShardNode, readonly?: boolean) { @@ -443,6 +453,7 @@ export default class RedisClusterSlots< this.nodeByAddress.clear(); await Promise.allSettled(promises); + this.#emit('disconnect'); } getClient( @@ -542,7 +553,7 @@ export default class RedisClusterSlots< node = index < this.masters.length ? this.masters[index] : this.replicas[index - this.masters.length], - client = this.#createClient(node, false); + client = this.#createClient(node, false); this.pubSubNode = { address: node.address, From 3855ae8b48ec6f58c681493509d6f88c1fa39e52 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 2 Oct 2025 14:15:11 +0300 Subject: [PATCH 2/6] add test template for the new cluster events --- packages/client/lib/cluster/index.spec.ts | 26 +++++++++++++++++++++++ packages/test-utils/lib/index.ts | 5 +++++ 2 files changed, 31 insertions(+) diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index 4db5f32e85..98ed4f3c8a 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -339,4 +339,30 @@ describe('Cluster', () => { minimumDockerVersion: [7] }); }); + + describe('clusterEvents', () => { + testUtils.testWithCluster('should fire events', async (cluster) => { + const log: string[] = []; + cluster + .on('connect', () => log.push('connect')) + .on('disconnect', () => log.push('disconnect')) + .on('error', () => log.push('error')) + .on('node-error', () => log.push('node-error')) + .on('node-reconnecting', () => log.push('node-reconnecting')) + .on('node-ready', () => log.push('node-ready')) + .on('node-connect', () => log.push('node-connect')) + .on('node-disconnect', () => log.push('node-disconnect')) + + + await cluster.connect(); + cluster.destroy(); + + /* assertions on the log */ + + }, { + ...GLOBAL.CLUSTERS.OPEN, + disableClusterSetup: true + }) + }); + }); diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 64b9abc7f4..117946a183 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -116,6 +116,7 @@ interface ClusterTestOptions< clusterConfiguration?: Partial>; numberOfMasters?: number; numberOfReplicas?: number; + disableClusterSetup?: boolean; } interface AllTestOptions< @@ -558,6 +559,10 @@ export default class TestUtils { ...options.clusterConfiguration }); + if(options.disableClusterSetup) { + return fn(cluster); + } + await cluster.connect(); try { From 787aa6ba55a1937066527cfb4c4f0a7d0e7accb9 Mon Sep 17 00:00:00 2001 From: NaughtySora Date: Fri, 3 Oct 2025 18:53:51 +0000 Subject: [PATCH 3/6] adding test for cluster events positive branch --- packages/client/lib/cluster/cluster-slots.ts | 1 + packages/client/lib/cluster/index.spec.ts | 38 ++++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 0955df16b2..737413677e 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -416,6 +416,7 @@ export default class RedisClusterSlots< this.#resetSlots(); this.nodeByAddress.clear(); + this.#emit('disconnect'); } *#clients() { diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index 98ed4f3c8a..6c7b1f9279 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -343,6 +343,13 @@ describe('Cluster', () => { describe('clusterEvents', () => { testUtils.testWithCluster('should fire events', async (cluster) => { const log: string[] = []; + const rootNodes = cluster._options.rootNodes.length; + const nodeConnect = rootNodes; + const nodeReady = nodeConnect + rootNodes; + const connect = nodeReady + 1; + const nodeDisconnect = connect + rootNodes; + const disconnect = nodeDisconnect + 1; + cluster .on('connect', () => log.push('connect')) .on('disconnect', () => log.push('disconnect')) @@ -353,16 +360,41 @@ describe('Cluster', () => { .on('node-connect', () => log.push('node-connect')) .on('node-disconnect', () => log.push('node-disconnect')) - await cluster.connect(); cluster.destroy(); - /* assertions on the log */ + assert.strictEqual(log.length, disconnect); + + assert.deepStrictEqual( + log.slice(0, nodeConnect), + new Array(rootNodes).fill('node-connect'), + ); + assert.deepStrictEqual( + log.slice(nodeConnect, nodeReady), + new Array(rootNodes).fill('node-ready'), + ); + assert.deepStrictEqual( + log.slice(nodeReady, connect), + new Array(1).fill('connect'), + ); + assert.deepStrictEqual( + log.slice(connect, nodeDisconnect), + new Array(rootNodes).fill('node-disconnect'), + ); + assert.deepStrictEqual( + log.slice(nodeDisconnect, disconnect), + new Array(1).fill('disconnect'), + ); + + assert.strictEqual(log.includes('error'), false); + assert.strictEqual(log.includes('node-error'), false); + assert.strictEqual(log.includes('node-reconnecting'), false); }, { ...GLOBAL.CLUSTERS.OPEN, disableClusterSetup: true - }) + } as any); + }); }); From 237912f467e3f591276ca8faa8ed906a760981d1 Mon Sep 17 00:00:00 2001 From: NaughtySora Date: Fri, 3 Oct 2025 19:26:08 +0000 Subject: [PATCH 4/6] adding cluster events docs section --- docs/clustering.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/clustering.md b/docs/clustering.md index 3e4f8446b6..dff51ed6b2 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -120,6 +120,24 @@ createCluster({ > This is a common problem when using ElastiCache. See [Accessing ElastiCache from outside AWS](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/accessing-elasticache.html) for more information on that. +### Events + +The Node Redis cluster class is an Nodejs EventEmitter and emits following events: + +| Name | When | Listener arguments | +| ----------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------- | +| `connect` | The cluster connected and ready to use | _No arguments_ | +| `disconnect` | The cluster has disconnected | _No arguments_ | +| `error` | The cluster has errored | `(error: Error)` | +| `node-connect` | One of the cluster's nodes has connected | `(node: { host: string, port: number })` | +| `node-disconnect` | One of the cluster's nodes has disconnected | `(node: { host: string, port: number })` | +| `node-ready` | One of the cluster's nodes is ready to connect | `(node: { host: string, port: number })` | +| `node-reconnecting` | One of the cluster's nodes is trying to reconnect after error has occurred | `(node: { host: string, port: number })` | +| `node-error` | One of the cluster's nodes has errored, usually during TCP connection | `(error: Error, node: { host: string, port: number }` | + +> :warning: You **MUST** listen to `error` events. If a cluster doesn't have at least one `error` listener registered and +> an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details. + ## Command Routing ### Commands that operate on Redis Keys From 945483955bb27cd901298098165648993909f314 Mon Sep 17 00:00:00 2001 From: Trofymenko Vladyslav Date: Mon, 6 Oct 2025 17:17:13 +0300 Subject: [PATCH 5/6] cluster events docs, cluster events tests --- docs/clustering.md | 14 ++++----- packages/client/lib/cluster/index.spec.ts | 37 +++++++++++------------ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/docs/clustering.md b/docs/clustering.md index dff51ed6b2..4afd95afd2 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -122,18 +122,18 @@ createCluster({ ### Events -The Node Redis cluster class is an Nodejs EventEmitter and emits following events: +The Node Redis Cluster class extends Node.js’s EventEmitter and emits the following events: | Name | When | Listener arguments | | ----------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------- | -| `connect` | The cluster connected and ready to use | _No arguments_ | +| `connect` | The cluster has successfully connected and is ready to us | _No arguments_ | | `disconnect` | The cluster has disconnected | _No arguments_ | | `error` | The cluster has errored | `(error: Error)` | -| `node-connect` | One of the cluster's nodes has connected | `(node: { host: string, port: number })` | -| `node-disconnect` | One of the cluster's nodes has disconnected | `(node: { host: string, port: number })` | -| `node-ready` | One of the cluster's nodes is ready to connect | `(node: { host: string, port: number })` | -| `node-reconnecting` | One of the cluster's nodes is trying to reconnect after error has occurred | `(node: { host: string, port: number })` | -| `node-error` | One of the cluster's nodes has errored, usually during TCP connection | `(error: Error, node: { host: string, port: number }` | +| `node-ready` | A cluster node is ready to establish a connection | `(node: { host: string, port: number })` | +| `node-connect` | A cluster node has connected | `(node: { host: string, port: number })` | +| `node-reconnecting` | A cluster node is attempting to reconnect after an error | `(node: { host: string, port: number })` | +| `node-disconnect` | A cluster node has disconnected | `(node: { host: string, port: number })` | +| `node-error` | A cluster node has has errored (usually during TCP connection) | `(error: Error, node: { host: string, port: number })` | > :warning: You **MUST** listen to `error` events. If a cluster doesn't have at least one `error` listener registered and > an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details. diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index 6c7b1f9279..e336f7d310 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -343,11 +343,11 @@ describe('Cluster', () => { describe('clusterEvents', () => { testUtils.testWithCluster('should fire events', async (cluster) => { const log: string[] = []; - const rootNodes = cluster._options.rootNodes.length; - const nodeConnect = rootNodes; - const nodeReady = nodeConnect + rootNodes; + const { numberOfMasters } = GLOBAL.CLUSTERS.WITH_REPLICAS; + const nodeConnect = numberOfMasters; + const nodeReady = nodeConnect + numberOfMasters; const connect = nodeReady + 1; - const nodeDisconnect = connect + rootNodes; + const nodeDisconnect = connect + numberOfMasters; const disconnect = nodeDisconnect + 1; cluster @@ -363,38 +363,37 @@ describe('Cluster', () => { await cluster.connect(); cluster.destroy(); - assert.strictEqual(log.length, disconnect); + assert.equal(log.length, disconnect); - assert.deepStrictEqual( + assert.deepEqual( log.slice(0, nodeConnect), - new Array(rootNodes).fill('node-connect'), + new Array(numberOfMasters).fill('node-connect'), ); - assert.deepStrictEqual( + assert.deepEqual( log.slice(nodeConnect, nodeReady), - new Array(rootNodes).fill('node-ready'), + new Array(numberOfMasters).fill('node-ready'), ); - assert.deepStrictEqual( + assert.deepEqual( log.slice(nodeReady, connect), new Array(1).fill('connect'), ); - assert.deepStrictEqual( + assert.deepEqual( log.slice(connect, nodeDisconnect), - new Array(rootNodes).fill('node-disconnect'), + new Array(numberOfMasters).fill('node-disconnect'), ); - assert.deepStrictEqual( + assert.deepEqual( log.slice(nodeDisconnect, disconnect), new Array(1).fill('disconnect'), ); - assert.strictEqual(log.includes('error'), false); - assert.strictEqual(log.includes('node-error'), false); - assert.strictEqual(log.includes('node-reconnecting'), false); + assert.equal(log.includes('error'), false); + assert.equal(log.includes('node-error'), false); + assert.equal(log.includes('node-reconnecting'), false); }, { ...GLOBAL.CLUSTERS.OPEN, - disableClusterSetup: true - } as any); - + disableClusterSetup: true, + }); }); }); From 993a1557f0af6f06d602b767741f4ad45e16e12c Mon Sep 17 00:00:00 2001 From: Trofymenko Vladyslav Date: Tue, 7 Oct 2025 00:23:26 +0300 Subject: [PATCH 6/6] cluster events tests adding exact masters amount --- packages/client/lib/cluster/index.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index e336f7d310..e32bf4fa85 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -343,7 +343,7 @@ describe('Cluster', () => { describe('clusterEvents', () => { testUtils.testWithCluster('should fire events', async (cluster) => { const log: string[] = []; - const { numberOfMasters } = GLOBAL.CLUSTERS.WITH_REPLICAS; + const numberOfMasters = 2; const nodeConnect = numberOfMasters; const nodeReady = nodeConnect + numberOfMasters; const connect = nodeReady + 1; @@ -393,6 +393,8 @@ describe('Cluster', () => { }, { ...GLOBAL.CLUSTERS.OPEN, disableClusterSetup: true, + numberOfMasters: 2, + numberOfReplicas: 1, }); });