Skip to content

Commit bd11e38

Browse files
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>
1 parent d6d8d8e commit bd11e38

File tree

4 files changed

+86
-12
lines changed

4 files changed

+86
-12
lines changed

docs/clustering.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,24 @@ createCluster({
120120

121121
> 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.
122122
123+
### Events
124+
125+
The Node Redis Cluster class extends Node.js’s EventEmitter and emits the following events:
126+
127+
| Name | When | Listener arguments |
128+
| ----------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------- |
129+
| `connect` | The cluster has successfully connected and is ready to us | _No arguments_ |
130+
| `disconnect` | The cluster has disconnected | _No arguments_ |
131+
| `error` | The cluster has errored | `(error: Error)` |
132+
| `node-ready` | A cluster node is ready to establish a connection | `(node: { host: string, port: number })` |
133+
| `node-connect` | A cluster node has connected | `(node: { host: string, port: number })` |
134+
| `node-reconnecting` | A cluster node is attempting to reconnect after an error | `(node: { host: string, port: number })` |
135+
| `node-disconnect` | A cluster node has disconnected | `(node: { host: string, port: number })` |
136+
| `node-error` | A cluster node has has errored (usually during TCP connection) | `(error: Error, node: { host: string, port: number })` |
137+
138+
> :warning: You **MUST** listen to `error` events. If a cluster doesn't have at least one `error` listener registered and
139+
> 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.
140+
123141
## Command Routing
124142

125143
### Commands that operate on Redis Keys

packages/client/lib/cluster/cluster-slots.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ type PubSubNode<
8080
RESP extends RespVersions,
8181
TYPE_MAPPING extends TypeMapping
8282
> = (
83-
Omit<Node<M, F, S, RESP, TYPE_MAPPING>, 'client'> &
84-
Required<Pick<Node<M, F, S, RESP, TYPE_MAPPING>, 'client'>>
85-
);
83+
Omit<Node<M, F, S, RESP, TYPE_MAPPING>, 'client'> &
84+
Required<Pick<Node<M, F, S, RESP, TYPE_MAPPING>, 'client'>>
85+
);
8686

8787
type PubSubToResubscribe = Record<
8888
PUBSUB_TYPE['CHANNELS'] | PUBSUB_TYPE['PATTERNS'],
@@ -153,6 +153,7 @@ export default class RedisClusterSlots<
153153
this.#isOpen = true;
154154
try {
155155
await this.#discoverWithRootNodes();
156+
this.#emit('connect');
156157
} catch (err) {
157158
this.#isOpen = false;
158159
throw err;
@@ -333,17 +334,26 @@ export default class RedisClusterSlots<
333334
}
334335

335336
#createClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>, readonly = node.readonly) {
337+
const socket =
338+
this.#getNodeAddress(node.address) ??
339+
{ host: node.host, port: node.port, };
340+
const client = Object.freeze({
341+
host: socket.host,
342+
port: socket.port,
343+
});
344+
const emit = this.#emit;
336345
return this.#clientFactory(
337346
this.#clientOptionsDefaults({
338347
clientSideCache: this.clientSideCache,
339348
RESP: this.#options.RESP,
340-
socket: this.#getNodeAddress(node.address) ?? {
341-
host: node.host,
342-
port: node.port
343-
},
344-
readonly
345-
})
346-
).on('error', err => console.error(err));
349+
socket,
350+
readonly,
351+
}))
352+
.on('error', error => emit('node-error', error, client))
353+
.on('reconnecting', () => emit('node-reconnecting', client))
354+
.once('ready', () => emit('node-ready', client))
355+
.once('connect', () => emit('node-connect', client))
356+
.once('end', () => emit('node-disconnect', client));
347357
}
348358

349359
#createNodeClient(node: ShardNode<M, F, S, RESP, TYPE_MAPPING>, readonly?: boolean) {
@@ -406,6 +416,7 @@ export default class RedisClusterSlots<
406416

407417
this.#resetSlots();
408418
this.nodeByAddress.clear();
419+
this.#emit('disconnect');
409420
}
410421

411422
*#clients() {
@@ -443,6 +454,7 @@ export default class RedisClusterSlots<
443454
this.nodeByAddress.clear();
444455

445456
await Promise.allSettled(promises);
457+
this.#emit('disconnect');
446458
}
447459

448460
getClient(
@@ -542,7 +554,7 @@ export default class RedisClusterSlots<
542554
node = index < this.masters.length ?
543555
this.masters[index] :
544556
this.replicas[index - this.masters.length],
545-
client = this.#createClient(node, false);
557+
client = this.#createClient(node, false);
546558

547559
this.pubSubNode = {
548560
address: node.address,

packages/client/lib/cluster/index.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,4 +339,43 @@ describe('Cluster', () => {
339339
minimumDockerVersion: [7]
340340
});
341341
});
342+
343+
describe('clusterEvents', () => {
344+
testUtils.testWithCluster('should fire events', async (cluster) => {
345+
const log: string[] = [];
346+
347+
cluster
348+
.on('connect', () => log.push('connect'))
349+
.on('disconnect', () => log.push('disconnect'))
350+
.on('error', () => log.push('error'))
351+
.on('node-error', () => log.push('node-error'))
352+
.on('node-reconnecting', () => log.push('node-reconnecting'))
353+
.on('node-ready', () => log.push('node-ready'))
354+
.on('node-connect', () => log.push('node-connect'))
355+
.on('node-disconnect', () => log.push('node-disconnect'))
356+
357+
await cluster.connect();
358+
cluster.destroy();
359+
360+
assert.deepEqual(log, [
361+
'node-connect',
362+
'node-connect',
363+
'node-ready',
364+
'node-ready',
365+
'connect',
366+
'node-disconnect',
367+
'node-disconnect',
368+
'disconnect',
369+
]);
370+
}, {
371+
...GLOBAL.CLUSTERS.OPEN,
372+
disableClusterSetup: true,
373+
numberOfMasters: 2,
374+
numberOfReplicas: 1,
375+
clusterConfiguration: {
376+
minimizeConnections: false
377+
}
378+
});
379+
});
380+
342381
});

packages/test-utils/lib/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ interface ClusterTestOptions<
116116
clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>>;
117117
numberOfMasters?: number;
118118
numberOfReplicas?: number;
119+
disableClusterSetup?: boolean;
119120
}
120121

121122
interface AllTestOptions<
@@ -554,10 +555,14 @@ export default class TestUtils {
554555
port
555556
}
556557
})),
557-
minimizeConnections: true,
558+
minimizeConnections: options.clusterConfiguration?.minimizeConnections ?? true,
558559
...options.clusterConfiguration
559560
});
560561

562+
if(options.disableClusterSetup) {
563+
return fn(cluster);
564+
}
565+
561566
await cluster.connect();
562567

563568
try {

0 commit comments

Comments
 (0)