1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-07 13:22:56 +03:00

CLUSERT [INFO|NODES|REPLICAS]

This commit is contained in:
Leibale
2023-09-12 16:02:12 -04:00
parent 0e8a0b9dd0
commit 17cf320651
7 changed files with 80 additions and 358 deletions

View File

@@ -3,53 +3,18 @@ import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments, transformReply } from './CLUSTER_INFO';
describe('CLUSTER INFO', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['CLUSTER', 'INFO']
);
});
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['CLUSTER', 'INFO']
);
});
it('transformReply', () => {
assert.deepEqual(
transformReply([
'cluster_state:ok',
'cluster_slots_assigned:16384',
'cluster_slots_ok:16384',
'cluster_slots_pfail:0',
'cluster_slots_fail:0',
'cluster_known_nodes:6',
'cluster_size:3',
'cluster_current_epoch:6',
'cluster_my_epoch:2',
'cluster_stats_messages_sent:1483972',
'cluster_stats_messages_received:1483968'
].join('\r\n')),
{
state: 'ok',
slots: {
assigned: 16384,
ok: 16384,
pfail: 0,
fail: 0
},
knownNodes: 6,
size: 3,
currentEpoch: 6,
myEpoch: 2,
stats: {
messagesSent: 1483972,
messagesReceived: 1483968
}
}
);
});
testUtils.testWithCluster('clusterNode.clusterInfo', async cluster => {
const client = await cluster.nodeClient(cluster.masters[0]);
assert.notEqual(
await client.clusterInfo(),
null
);
}, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('clusterNode.clusterInfo', async cluster => {
const client = await cluster.nodeClient(cluster.masters[0]);
assert.equal(
typeof await client.clusterInfo(),
'string'
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -1,47 +1,10 @@
export function transformArguments(): Array<string> {
import { VerbatimStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['CLUSTER', 'INFO'];
}
interface ClusterInfoReply {
state: string;
slots: {
assigned: number;
ok: number;
pfail: number;
fail: number;
};
knownNodes: number;
size: number;
currentEpoch: number;
myEpoch: number;
stats: {
messagesSent: number;
messagesReceived: number;
};
}
export function transformReply(reply: string): ClusterInfoReply {
const lines = reply.split('\r\n');
return {
state: extractLineValue(lines[0]),
slots: {
assigned: Number(extractLineValue(lines[1])),
ok: Number(extractLineValue(lines[2])),
pfail: Number(extractLineValue(lines[3])),
fail: Number(extractLineValue(lines[4]))
},
knownNodes: Number(extractLineValue(lines[5])),
size: Number(extractLineValue(lines[6])),
currentEpoch: Number(extractLineValue(lines[7])),
myEpoch: Number(extractLineValue(lines[8])),
stats: {
messagesSent: Number(extractLineValue(lines[9])),
messagesReceived: Number(extractLineValue(lines[10]))
}
};
}
export function extractLineValue(line: string): string {
return line.substring(line.indexOf(':') + 1);
}
},
transformReply: undefined as unknown as () => VerbatimStringReply
} as const satisfies Command;

View File

@@ -1,145 +1,20 @@
import { strict as assert } from 'assert';
import { RedisClusterNodeLinkStates, transformArguments, transformReply } from './CLUSTER_NODES';
import testUtils, { GLOBAL } from '../test-utils';
import CLUSTER_NODES from './CLUSTER_NODES';
describe('CLUSTER NODES', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['CLUSTER', 'NODES']
);
});
it('transformArguments', () => {
assert.deepEqual(
CLUSTER_NODES.transformArguments(),
['CLUSTER', 'NODES']
);
});
describe('transformReply', () => {
it('simple', () => {
assert.deepEqual(
transformReply([
'master 127.0.0.1:30001@31001 myself,master - 0 0 1 connected 0-16384',
'slave 127.0.0.1:30002@31002 slave master 0 0 1 connected',
''
].join('\n')),
[{
id: 'master',
address: '127.0.0.1:30001@31001',
host: '127.0.0.1',
port: 30001,
cport: 31001,
flags: ['myself', 'master'],
pingSent: 0,
pongRecv: 0,
configEpoch: 1,
linkState: RedisClusterNodeLinkStates.CONNECTED,
slots: [{
from: 0,
to: 16384
}],
replicas: [{
id: 'slave',
address: '127.0.0.1:30002@31002',
host: '127.0.0.1',
port: 30002,
cport: 31002,
flags: ['slave'],
pingSent: 0,
pongRecv: 0,
configEpoch: 1,
linkState: RedisClusterNodeLinkStates.CONNECTED
}]
}]
);
});
it('should support addresses without cport', () => {
assert.deepEqual(
transformReply(
'id 127.0.0.1:30001 master - 0 0 0 connected 0-16384\n'
),
[{
id: 'id',
address: '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('should support ipv6 addresses', () => {
assert.deepEqual(
transformReply(
'id 2a02:6b8:c21:330d:0:1589:ebbe:b1a0:6379@16379 master - 0 0 0 connected 0-549\n'
),
[{
id: 'id',
address: '2a02:6b8:c21:330d:0:1589:ebbe:b1a0:6379@16379',
host: '2a02:6b8:c21:330d:0:1589:ebbe:b1a0',
port: 6379,
cport: 16379,
flags: ['master'],
pingSent: 0,
pongRecv: 0,
configEpoch: 0,
linkState: RedisClusterNodeLinkStates.CONNECTED,
slots: [{
from: 0,
to: 549
}],
replicas: []
}]
);
});
it.skip('with importing slots', () => {
assert.deepEqual(
transformReply(
'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0-<-16384\n'
),
[{
id: 'id',
address: '127.0.0.1:30001@31001',
host: '127.0.0.1',
port: 30001,
cport: 31001,
flags: ['master'],
pingSent: 0,
pongRecv: 0,
configEpoch: 0,
linkState: RedisClusterNodeLinkStates.CONNECTED,
slots: [], // TODO
replicas: []
}]
);
});
it.skip('with migrating slots', () => {
assert.deepEqual(
transformReply(
'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0->-16384\n'
),
[{
id: 'id',
address: '127.0.0.1:30001@31001',
host: '127.0.0.1',
port: 30001,
cport: 31001,
flags: ['master'],
pingSent: 0,
pongRecv: 0,
configEpoch: 0,
linkState: RedisClusterNodeLinkStates.CONNECTED,
slots: [], // TODO
replicas: []
}]
);
});
});
testUtils.testWithCluster('clusterNode.clusterNodes', async cluster => {
const client = await cluster.nodeClient(cluster.masters[0]);
assert.equal(
typeof await client.clusterNodes(),
'string'
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -1,105 +1,10 @@
export function transformArguments(): Array<string> {
import { VerbatimStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['CLUSTER', 'NODES'];
}
export enum RedisClusterNodeLinkStates {
CONNECTED = 'connected',
DISCONNECTED = 'disconnected'
}
interface RedisClusterNodeAddress {
host: string;
port: number;
cport: number | null;
}
export interface RedisClusterReplicaNode extends RedisClusterNodeAddress {
id: string;
address: string;
flags: Array<string>;
pingSent: number;
pongRecv: number;
configEpoch: number;
linkState: RedisClusterNodeLinkStates;
}
export interface RedisClusterMasterNode extends RedisClusterReplicaNode {
slots: Array<{
from: number;
to: number;
}>;
replicas: Array<RedisClusterReplicaNode>;
}
export function transformReply(reply: string): Array<RedisClusterMasterNode> {
const lines = reply.split('\n');
lines.pop(); // last line is empty
const mastersMap = new Map<string, RedisClusterMasterNode>(),
replicasMap = new Map<string, Array<RedisClusterReplicaNode>>();
for (const line of lines) {
const [id, address, flags, masterId, pingSent, pongRecv, configEpoch, linkState, ...slots] = line.split(' '),
node = {
id,
address,
...transformNodeAddress(address),
flags: flags.split(','),
pingSent: Number(pingSent),
pongRecv: Number(pongRecv),
configEpoch: Number(configEpoch),
linkState: (linkState as RedisClusterNodeLinkStates)
};
if (masterId === '-') {
let replicas = replicasMap.get(id);
if (!replicas) {
replicas = [];
replicasMap.set(id, replicas);
}
mastersMap.set(id, {
...node,
slots: slots.map(slot => {
// TODO: importing & exporting (https://redis.io/commands/cluster-nodes#special-slot-entries)
const [fromString, toString] = slot.split('-', 2),
from = Number(fromString);
return {
from,
to: toString ? Number(toString) : from
};
}),
replicas
});
} else {
const replicas = replicasMap.get(masterId);
if (!replicas) {
replicasMap.set(masterId, [node]);
} else {
replicas.push(node);
}
}
}
return [...mastersMap.values()];
}
function transformNodeAddress(address: string): RedisClusterNodeAddress {
const indexOfColon = address.lastIndexOf(':'),
indexOfAt = address.indexOf('@', indexOfColon),
host = address.substring(0, indexOfColon);
if (indexOfAt === -1) {
return {
host,
port: Number(address.substring(indexOfColon + 1)),
cport: null
};
}
return {
host: address.substring(0, indexOfColon),
port: Number(address.substring(indexOfColon + 1, indexOfAt)),
cport: Number(address.substring(indexOfAt + 1))
};
}
},
transformReply: undefined as unknown as () => VerbatimStringReply
} as const satisfies Command;

View File

@@ -1,11 +1,20 @@
import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_REPLICAS';
import testUtils, { GLOBAL } from '../test-utils';
import CLUSTER_REPLICAS from './CLUSTER_REPLICAS';
describe('CLUSTER REPLICAS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('0'),
['CLUSTER', 'REPLICAS', '0']
);
});
it('transformArguments', () => {
assert.deepEqual(
CLUSTER_REPLICAS.transformArguments('0'),
['CLUSTER', 'REPLICAS', '0']
);
});
testUtils.testWithCluster('clusterNode.clusterReplicas', async cluster => {
const client = await cluster.nodeClient(cluster.masters[0]);
assert.equal(
typeof await client.clusterReplicas(cluster.masters[0].id),
'string'
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -1,5 +1,10 @@
export function transformArguments(nodeId: string): Array<string> {
return ['CLUSTER', 'REPLICAS', nodeId];
}
import { RedisArgument, VerbatimStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export { transformReply } from './CLUSTER_NODES';
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(nodeId: RedisArgument) {
return ['CLUSTER', 'REPLICAS', nodeId];
},
transformReply: undefined as unknown as () => VerbatimStringReply
} as const satisfies Command;

View File

@@ -54,13 +54,13 @@ import CLUSTER_FAILOVER from './CLUSTER_FAILOVER';
import CLUSTER_FLUSHSLOTS from './CLUSTER_FLUSHSLOTS';
import CLUSTER_FORGET from './CLUSTER_FORGET';
import CLUSTER_GETKEYSINSLOT from './CLUSTER_GETKEYSINSLOT';
// import CLUSTER_INFO from './CLUSTER_INFO';
import CLUSTER_INFO from './CLUSTER_INFO';
import CLUSTER_KEYSLOT from './CLUSTER_KEYSLOT';
import CLUSTER_LINKS from './CLUSTER_LINKS';
import CLUSTER_MEET from './CLUSTER_MEET';
import CLUSTER_MYID from './CLUSTER_MYID';
// import CLUSTER_NODES from './CLUSTER_NODES';
// import CLUSTER_REPLICAS from './CLUSTER_REPLICAS';
import CLUSTER_NODES from './CLUSTER_NODES';
import CLUSTER_REPLICAS from './CLUSTER_REPLICAS';
import CLUSTER_REPLICATE from './CLUSTER_REPLICATE';
import CLUSTER_RESET from './CLUSTER_RESET';
import CLUSTER_SAVECONFIG from './CLUSTER_SAVECONFIG';
@@ -437,8 +437,8 @@ export default {
clusterForget: CLUSTER_FORGET,
CLUSTER_GETKEYSINSLOT,
clusterGetKeysInSlot: CLUSTER_GETKEYSINSLOT,
// CLUSTER_INFO,
// clusterInfo: CLUSTER_INFO,
CLUSTER_INFO,
clusterInfo: CLUSTER_INFO,
CLUSTER_KEYSLOT,
clusterKeySlot: CLUSTER_KEYSLOT,
CLUSTER_LINKS,
@@ -447,10 +447,10 @@ export default {
clusterMeet: CLUSTER_MEET,
CLUSTER_MYID,
clusterMyId: CLUSTER_MYID,
// CLUSTER_NODES,
// clusterNodes: CLUSTER_NODES,
// CLUSTER_REPLICAS,
// clusterReplicas: CLUSTER_REPLICAS,
CLUSTER_NODES,
clusterNodes: CLUSTER_NODES,
CLUSTER_REPLICAS,
clusterReplicas: CLUSTER_REPLICAS,
CLUSTER_REPLICATE,
clusterReplicate: CLUSTER_REPLICATE,
CLUSTER_RESET,