1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-04 15:02:09 +03:00

enhance cluster reshard handling

This commit is contained in:
leibale
2021-11-22 17:42:41 -05:00
parent 6946e36ba0
commit 42e36dfbb1
4 changed files with 76 additions and 28 deletions

View File

@@ -42,20 +42,8 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
throw new Error('None of the root nodes is available'); throw new Error('None of the root nodes is available');
} }
async discover(startWith: RedisClientType<M, S>): Promise<void> {
if (await this.#discoverNodes(startWith.options)) return;
for (const { client } of this.#nodeByUrl.values()) {
if (client === startWith) continue;
if (await this.#discoverNodes(client.options)) return;
}
throw new Error('None of the cluster nodes is available');
}
async #discoverNodes(clientOptions?: RedisClusterClientOptions): Promise<boolean> { async #discoverNodes(clientOptions?: RedisClusterClientOptions): Promise<boolean> {
const client = new this.#Client(clientOptions); const client = this.#initiateClient(clientOptions);
await client.connect(); await client.connect();
@@ -72,6 +60,29 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
} }
} }
#runningRediscoverPromise?: Promise<void>;
async rediscover(startWith: RedisClientType<M, S>): Promise<void> {
if (!this.#runningRediscoverPromise) {
this.#runningRediscoverPromise = this.#rediscover(startWith)
.finally(() => this.#runningRediscoverPromise = undefined);
}
return this.#runningRediscoverPromise;
}
async #rediscover(startWith: RedisClientType<M, S>): Promise<void> {
if (await this.#discoverNodes(startWith.options)) return;
for (const { client } of this.#nodeByUrl.values()) {
if (client === startWith) continue;
if (await this.#discoverNodes(client.options)) return;
}
throw new Error('None of the cluster nodes is available');
}
async #reset(masters: Array<RedisClusterMasterNode>): Promise<void> { async #reset(masters: Array<RedisClusterMasterNode>): Promise<void> {
// Override this.#slots and add not existing clients to this.#nodeByUrl // Override this.#slots and add not existing clients to this.#nodeByUrl
const promises: Array<Promise<void>> = [], const promises: Array<Promise<void>> = [],
@@ -103,18 +114,23 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
await Promise.all(promises); await Promise.all(promises);
} }
#clientOptionsDefaults(options: RedisClusterClientOptions): RedisClusterClientOptions { #clientOptionsDefaults(options?: RedisClusterClientOptions): RedisClusterClientOptions | undefined {
if (!this.#options.defaults) return options; if (!this.#options.defaults) return options;
const merged = Object.assign({}, this.#options.defaults, options); const merged = Object.assign({}, this.#options.defaults, options);
if (options.socket && this.#options.defaults.socket) { if (options?.socket && this.#options.defaults.socket) {
Object.assign({}, this.#options.defaults.socket, options.socket); Object.assign({}, this.#options.defaults.socket, options.socket);
} }
return merged; return merged;
} }
#initiateClient(options?: RedisClusterClientOptions): RedisClientType<M, S> {
return new this.#Client(this.#clientOptionsDefaults(options))
.on('error', this.#onError);
}
#initiateClientForNode(nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set<string>, promises: Array<Promise<void>>): ClusterNode<M, S> { #initiateClientForNode(nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set<string>, promises: Array<Promise<void>>): ClusterNode<M, S> {
const url = `${nodeData.host}:${nodeData.port}`; const url = `${nodeData.host}:${nodeData.port}`;
clientsInUse.add(url); clientsInUse.add(url);
@@ -123,15 +139,13 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
if (!node) { if (!node) {
node = { node = {
id: nodeData.id, id: nodeData.id,
client: new this.#Client( client: this.#initiateClient({
this.#clientOptionsDefaults({
socket: { socket: {
host: nodeData.host, host: nodeData.host,
port: nodeData.port port: nodeData.port
}, },
readonly readonly
}) })
)
}; };
promises.push(node.client.connect()); promises.push(node.client.connect());
this.#nodeByUrl.set(url, node); this.#nodeByUrl.set(url, node);

View File

@@ -157,7 +157,7 @@ export default class RedisCluster<M extends RedisModules = Record<string, never>
const url = err.message.substring(err.message.lastIndexOf(' ') + 1); const url = err.message.substring(err.message.lastIndexOf(' ') + 1);
let node = this.#slots.getNodeByUrl(url); let node = this.#slots.getNodeByUrl(url);
if (!node) { if (!node) {
await this.#slots.discover(client); await this.#slots.rediscover(client);
node = this.#slots.getNodeByUrl(url); node = this.#slots.getNodeByUrl(url);
if (!node) { if (!node) {
@@ -168,7 +168,7 @@ export default class RedisCluster<M extends RedisModules = Record<string, never>
await node.client.asking(); await node.client.asking();
return node.client; return node.client;
} else if (err.message.startsWith('MOVED')) { } else if (err.message.startsWith('MOVED')) {
await this.#slots.discover(client); await this.#slots.rediscover(client);
return true; return true;
} }

View File

@@ -48,6 +48,31 @@ describe('CLUSTER NODES', () => {
); );
}); });
it('should support urls without cport', () => {
assert.deepEqual(
transformReply(
'id 127.0.0.1:30001 master - 0 0 0 connected 0-16384\n'
),
[{
id: 'id',
url: '127.0.0.1:30001',
host: '127.0.0.1',
port: 30001,
cport: null,
flags: ['master'],
pingSent: 0,
pongRecv: 0,
configEpoch: 0,
linkState: RedisClusterNodeLinkStates.CONNECTED,
slots: [{
from: 0,
to: 16384
}],
replicas: []
}]
);
});
it.skip('with importing slots', () => { it.skip('with importing slots', () => {
assert.deepEqual( assert.deepEqual(
transformReply( transformReply(

View File

@@ -10,7 +10,7 @@ export enum RedisClusterNodeLinkStates {
interface RedisClusterNodeTransformedUrl { interface RedisClusterNodeTransformedUrl {
host: string; host: string;
port: number; port: number;
cport: number; cport: number | null;
} }
export interface RedisClusterReplicaNode extends RedisClusterNodeTransformedUrl { export interface RedisClusterReplicaNode extends RedisClusterNodeTransformedUrl {
@@ -86,7 +86,16 @@ export function transformReply(reply: string): Array<RedisClusterMasterNode> {
function transformNodeUrl(url: string): RedisClusterNodeTransformedUrl { function transformNodeUrl(url: string): RedisClusterNodeTransformedUrl {
const indexOfColon = url.indexOf(':'), const indexOfColon = url.indexOf(':'),
indexOfAt = url.indexOf('@', indexOfColon); indexOfAt = url.indexOf('@', indexOfColon),
host = url.substring(0, indexOfColon);
if (indexOfAt === -1) {
return {
host,
port: Number(url.substring(indexOfColon + 1)),
cport: null
};
}
return { return {
host: url.substring(0, indexOfColon), host: url.substring(0, indexOfColon),