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

fix(ssubscribe): properly resubscribe in case of shard failover (#3098)

* fix(ssubscribe): properly resubscribe in case of shard failover

1) when RE failover happens, there is a disconnect
2) affected Client reconnects and tries to resubscribe all existing listeners
ISSUE #1: CROSSSLOT Error - client was doing ssubscribe ch1 ch2.. chN which, after the failover could result in CROSSSLOT ( naturally, becasuse now some slots could be owned by other shards )
FIX: send one ssubscribe command per channel instead of one ssubscribe for all channels
ISSUE #2: MOVED Error - some/all of the channels might be moved somewhere else
FIX: 1: Propagate the error to the Cluster. 2: Cluster rediscovers topology.
3: Cluster resubscribes all listeners of the failed client ( possibly some/all of those will end up in a different client after the rediscovery ) 

fixes: #2902
This commit is contained in:
Nikolay Karadzhov
2025-10-13 11:59:08 +03:00
committed by GitHub
parent bd11e382d0
commit 96d6445d66
9 changed files with 828 additions and 25 deletions

View File

@@ -6,7 +6,7 @@ import { EventEmitter } from 'node:events';
import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
import RedisClusterSlots, { NodeAddressMap, ShardNode } from './cluster-slots';
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
import { PubSubListener } from '../client/pub-sub';
import { PubSubListener, PubSubListeners } from '../client/pub-sub';
import { ErrorReply } from '../errors';
import { RedisTcpSocketOptions } from '../client/socket';
import { ClientSideCacheConfig, PooledClientSideCacheProvider } from '../client/cache';
@@ -310,6 +310,7 @@ export default class RedisCluster<
this._options = options;
this._slots = new RedisClusterSlots(options, this.emit.bind(this));
this.on('__resubscribeAllPubSubListeners', this.resubscribeAllPubSubListeners.bind(this));
if (options?.commandOptions) {
this._commandOptions = options.commandOptions;
@@ -584,6 +585,33 @@ export default class RedisCluster<
);
}
resubscribeAllPubSubListeners(allListeners: PubSubListeners) {
for(const [channel, listeners] of allListeners.CHANNELS) {
listeners.buffers.forEach(bufListener => {
this.subscribe(channel, bufListener, true);
});
listeners.strings.forEach(strListener => {
this.subscribe(channel, strListener);
});
};
for (const [channel, listeners] of allListeners.PATTERNS) {
listeners.buffers.forEach(bufListener => {
this.pSubscribe(channel, bufListener, true);
});
listeners.strings.forEach(strListener => {
this.pSubscribe(channel, strListener);
});
};
for (const [channel, listeners] of allListeners.SHARDED) {
listeners.buffers.forEach(bufListener => {
this.sSubscribe(channel, bufListener, true);
});
listeners.strings.forEach(strListener => {
this.sSubscribe(channel, strListener);
});
};
}
sUnsubscribe = this.SUNSUBSCRIBE;
/**