1
0
mirror of https://github.com/redis/node-redis.git synced 2025-12-11 09:22:35 +03:00

feat: add cluster/node events (#1855) (#3083)

* add cluster/node events
* add test for cluster events positive branch
* add cluster events docs section

fixes: #1855

---------

Co-authored-by: Nikolay Karadzhov <nkaradzhov89@gmail.com>
This commit is contained in:
Trofymenko Vladyslav
2025-10-09 16:06:57 +03:00
committed by GitHub
parent d6d8d8e8ed
commit bd11e382d0
4 changed files with 86 additions and 12 deletions

View File

@@ -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. > 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 extends Node.jss EventEmitter and emits the following events:
| Name | When | Listener 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-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.
## Command Routing ## Command Routing
### Commands that operate on Redis Keys ### Commands that operate on Redis Keys

View File

@@ -153,6 +153,7 @@ export default class RedisClusterSlots<
this.#isOpen = true; this.#isOpen = true;
try { try {
await this.#discoverWithRootNodes(); await this.#discoverWithRootNodes();
this.#emit('connect');
} catch (err) { } catch (err) {
this.#isOpen = false; this.#isOpen = false;
throw err; throw err;
@@ -333,17 +334,26 @@ export default class RedisClusterSlots<
} }
#createClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>, readonly = node.readonly) { #createClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>, 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( return this.#clientFactory(
this.#clientOptionsDefaults({ this.#clientOptionsDefaults({
clientSideCache: this.clientSideCache, clientSideCache: this.clientSideCache,
RESP: this.#options.RESP, RESP: this.#options.RESP,
socket: this.#getNodeAddress(node.address) ?? { socket,
host: node.host, readonly,
port: node.port }))
}, .on('error', error => emit('node-error', error, client))
readonly .on('reconnecting', () => emit('node-reconnecting', client))
}) .once('ready', () => emit('node-ready', client))
).on('error', err => console.error(err)); .once('connect', () => emit('node-connect', client))
.once('end', () => emit('node-disconnect', client));
} }
#createNodeClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>, readonly?: boolean) { #createNodeClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>, readonly?: boolean) {
@@ -406,6 +416,7 @@ export default class RedisClusterSlots<
this.#resetSlots(); this.#resetSlots();
this.nodeByAddress.clear(); this.nodeByAddress.clear();
this.#emit('disconnect');
} }
*#clients() { *#clients() {
@@ -443,6 +454,7 @@ export default class RedisClusterSlots<
this.nodeByAddress.clear(); this.nodeByAddress.clear();
await Promise.allSettled(promises); await Promise.allSettled(promises);
this.#emit('disconnect');
} }
getClient( getClient(

View File

@@ -339,4 +339,43 @@ describe('Cluster', () => {
minimumDockerVersion: [7] 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();
assert.deepEqual(log, [
'node-connect',
'node-connect',
'node-ready',
'node-ready',
'connect',
'node-disconnect',
'node-disconnect',
'disconnect',
]);
}, {
...GLOBAL.CLUSTERS.OPEN,
disableClusterSetup: true,
numberOfMasters: 2,
numberOfReplicas: 1,
clusterConfiguration: {
minimizeConnections: false
}
});
});
}); });

View File

@@ -116,6 +116,7 @@ interface ClusterTestOptions<
clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>>; clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>>;
numberOfMasters?: number; numberOfMasters?: number;
numberOfReplicas?: number; numberOfReplicas?: number;
disableClusterSetup?: boolean;
} }
interface AllTestOptions< interface AllTestOptions<
@@ -554,10 +555,14 @@ export default class TestUtils {
port port
} }
})), })),
minimizeConnections: true, minimizeConnections: options.clusterConfiguration?.minimizeConnections ?? true,
...options.clusterConfiguration ...options.clusterConfiguration
}); });
if(options.disableClusterSetup) {
return fn(cluster);
}
await cluster.connect(); await cluster.connect();
try { try {