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

Adapt legacy sentinel tests to use the new test utils (#2976)

* modified legacy sentinel tests

* Adapt legacy sentinel tests to use the new test utils

* modify tmpdir creation

* reduced sentinel config timeouts, removed unneeded comment

---------

Co-authored-by: H. Temelski <hristo.temelski@redis.com>
This commit is contained in:
Hristo Temelski
2025-05-27 14:21:22 +03:00
committed by GitHub
parent 065eb5e914
commit f346bad64e
6 changed files with 264 additions and 308 deletions

View File

@@ -89,8 +89,8 @@ describe('Client', () => {
&& expected?.credentialsProvider?.type === 'async-credentials-provider') { && expected?.credentialsProvider?.type === 'async-credentials-provider') {
// Compare the actual output of the credentials functions // Compare the actual output of the credentials functions
const resultCreds = await result.credentialsProvider.credentials(); const resultCreds = await result.credentialsProvider?.credentials();
const expectedCreds = await expected.credentialsProvider.credentials(); const expectedCreds = await expected.credentialsProvider?.credentials();
assert.deepEqual(resultCreds, expectedCreds); assert.deepEqual(resultCreds, expectedCreds);
} else { } else {
assert.fail('Credentials provider type mismatch'); assert.fail('Credentials provider type mismatch');

View File

@@ -197,7 +197,6 @@ describe(`test with scripts`, () => {
}, GLOBAL.SENTINEL.WITH_SCRIPT) }, GLOBAL.SENTINEL.WITH_SCRIPT)
}); });
describe(`test with functions`, () => { describe(`test with functions`, () => {
testUtils.testWithClientSentinel('with function', async sentinel => { testUtils.testWithClientSentinel('with function', async sentinel => {
await sentinel.functionLoad( await sentinel.functionLoad(
@@ -377,12 +376,9 @@ describe(`test with masterPoolSize 2`, () => {
}, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2);
}); });
// TODO: Figure out how to modify the test utils
// so it would have fine grained controll over
// sentinel
// it should somehow replicate the `SentinelFramework` object functionallities
async function steadyState(frame: SentinelFramework) { async function steadyState(frame: SentinelFramework) {
// wait a bit to ensure that sentinels are seeing eachother
await setTimeout(2000)
let checkedMaster = false; let checkedMaster = false;
let checkedReplicas = false; let checkedReplicas = false;
while (!checkedMaster || !checkedReplicas) { while (!checkedMaster || !checkedReplicas) {
@@ -430,7 +426,7 @@ async function steadyState(frame: SentinelFramework) {
} }
} }
describe.skip('legacy tests', () => { describe('legacy tests', () => {
const config: RedisSentinelConfig = { sentinelName: "test", numberOfNodes: 3, password: undefined }; const config: RedisSentinelConfig = { sentinelName: "test", numberOfNodes: 3, password: undefined };
const frame = new SentinelFramework(config); const frame = new SentinelFramework(config);
let tracer = new Array<string>(); let tracer = new Array<string>();
@@ -439,7 +435,11 @@ describe.skip('legacy tests', () => {
let longestTestDelta = 0; let longestTestDelta = 0;
let last: number; let last: number;
before(async function () {
describe('Sentinel Client', function () {
let sentinel: RedisSentinelType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> | undefined;
beforeEach(async function () {
this.timeout(15000); this.timeout(15000);
last = Date.now(); last = Date.now();
@@ -459,22 +459,6 @@ describe.skip('legacy tests', () => {
} }
setImmediate(deltaMeasurer); setImmediate(deltaMeasurer);
await frame.spawnRedisSentinel(); await frame.spawnRedisSentinel();
});
after(async function () {
this.timeout(15000);
stopMeasuringBlocking = true;
await frame.cleanup();
})
describe('Sentinel Client', function () {
let sentinel: RedisSentinelType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> | undefined;
beforeEach(async function () {
this.timeout(0);
await frame.getAllRunning(); await frame.getAllRunning();
await steadyState(frame); await steadyState(frame);
longestTestDelta = 0; longestTestDelta = 0;
@@ -522,6 +506,10 @@ describe.skip('legacy tests', () => {
await sentinel.destroy(); await sentinel.destroy();
sentinel = undefined; sentinel = undefined;
} }
stopMeasuringBlocking = true;
await frame.cleanup();
}) })
it('use', async function () { it('use', async function () {
@@ -863,7 +851,6 @@ describe.skip('legacy tests', () => {
it('shutdown sentinel node', async function () { it('shutdown sentinel node', async function () {
this.timeout(60000); this.timeout(60000);
sentinel = frame.getSentinelClient(); sentinel = frame.getSentinelClient();
sentinel.setTracer(tracer); sentinel.setTracer(tracer);
sentinel.on("error", () => { }); sentinel.on("error", () => { });
@@ -1020,7 +1007,7 @@ describe.skip('legacy tests', () => {
this.timeout(30000); this.timeout(30000);
const csc = new BasicPooledClientSideCache(); const csc = new BasicPooledClientSideCache();
sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3}, clientSideCache: csc, masterPoolSize: 5}); sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3 as const}, RESP: 3 as const, clientSideCache: csc, masterPoolSize: 5});
await sentinel.connect(); await sentinel.connect();
await sentinel.set('x', 1); await sentinel.set('x', 1);

View File

@@ -4,12 +4,13 @@ import { once } from 'node:events';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { exec } from 'node:child_process'; import { exec } from 'node:child_process';
import { RedisSentinelOptions, RedisSentinelType } from './types'; import { RedisSentinelOptions, RedisSentinelType } from './types';
import RedisClient from '../client'; import RedisClient, {RedisClientType} from '../client';
import RedisSentinel from '.'; import RedisSentinel from '.';
import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types';
const execAsync = promisify(exec); const execAsync = promisify(exec);
import RedisSentinelModule from './module' import RedisSentinelModule from './module'
import TestUtils from '@redis/test-utils';
import { DEBUG_MODE_ARGS } from '../test-utils'
interface ErrorWithCode extends Error { interface ErrorWithCode extends Error {
code: string; code: string;
} }
@@ -125,7 +126,6 @@ export interface RedisSentinelConfig {
sentinelServerArgument?: Array<string> sentinelServerArgument?: Array<string>
sentinelName: string; sentinelName: string;
sentinelQuorum?: number;
password?: string; password?: string;
} }
@@ -151,6 +151,7 @@ export interface SentinelController {
} }
export class SentinelFramework extends DockerBase { export class SentinelFramework extends DockerBase {
#testUtils: TestUtils;
#nodeList: Awaited<ReturnType<SentinelFramework['spawnRedisSentinelNodes']>> = []; #nodeList: Awaited<ReturnType<SentinelFramework['spawnRedisSentinelNodes']>> = [];
/* port -> docker info/client */ /* port -> docker info/client */
#nodeMap: Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelNodes']>>>>; #nodeMap: Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelNodes']>>>>;
@@ -170,7 +171,11 @@ export class SentinelFramework extends DockerBase {
super(); super();
this.config = config; this.config = config;
this.#testUtils = TestUtils.createFromConfig({
dockerImageName: 'redislabs/client-libs-test',
dockerImageVersionArgument: 'redis-version',
defaultDockerVersion: '8.0-M05-pre'
});
this.#nodeMap = new Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelNodes']>>>>(); this.#nodeMap = new Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelNodes']>>>>();
this.#sentinelMap = new Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelSentinels']>>>>(); this.#sentinelMap = new Map<string, ArrayElement<Awaited<ReturnType<SentinelFramework['spawnRedisSentinelSentinels']>>>>();
} }
@@ -190,7 +195,7 @@ export class SentinelFramework extends DockerBase {
const options: RedisSentinelOptions<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> = { const options: RedisSentinelOptions<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> = {
...opts, ...opts,
name: this.config.sentinelName, name: this.config.sentinelName,
sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.docker.port } }), sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.port } }),
passthroughClientErrorEvents: errors passthroughClientErrorEvents: errors
} }
@@ -218,11 +223,11 @@ export class SentinelFramework extends DockerBase {
throw new Error("inconsistent state with partial setup"); throw new Error("inconsistent state with partial setup");
} }
this.#nodeList = await this.spawnRedisSentinelNodes(); this.#nodeList = await this.spawnRedisSentinelNodes(2);
this.#nodeList.map((value) => this.#nodeMap.set(value.docker.port.toString(), value)); this.#nodeList.map((value) => this.#nodeMap.set(value.port.toString(), value));
this.#sentinelList = await this.spawnRedisSentinelSentinels(); this.#sentinelList = await this.spawnRedisSentinelSentinels(this.#nodeList[0].port, 3)
this.#sentinelList.map((value) => this.#sentinelMap.set(value.docker.port.toString(), value)); this.#sentinelList.map((value) => this.#sentinelMap.set(value.port.toString(), value));
this.#spawned = true; this.#spawned = true;
} }
@@ -234,11 +239,8 @@ export class SentinelFramework extends DockerBase {
return Promise.all( return Promise.all(
[...this.#nodeMap!.values(), ...this.#sentinelMap!.values()].map( [...this.#nodeMap!.values(), ...this.#sentinelMap!.values()].map(
async ({ docker, client }) => { async ({ dockerId }) => {
if (client.isOpen) { this.dockerRemove(dockerId);
client.destroy();
}
this.dockerRemove(docker.dockerId);
} }
) )
).finally(async () => { ).finally(async () => {
@@ -248,114 +250,35 @@ export class SentinelFramework extends DockerBase {
}); });
} }
protected async spawnRedisSentinelNodeDocker() { protected async spawnRedisSentinelNodes(replicasCount: number) {
const imageInfo: RedisServerDockerConfig = this.config.nodeDockerConfig ?? { image: "redis/redis-stack-server", version: "latest" }; const master = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS})
const serverArguments: Array<string> = this.config.nodeServerArguments ?? [];
let environment;
if (this.config.password !== undefined) {
environment = `REDIS_ARGS="{port} --requirepass ${this.config.password}"`;
} else {
environment = 'REDIS_ARGS="{port}"';
}
const docker = await this.spawnRedisServerDocker(imageInfo, serverArguments, environment); const replicas: Array<RedisServerDocker> = []
const client = await RedisClient.create({ for (let i = 0; i < replicasCount; i++) {
password: this.config.password, const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS})
replicas.push(replica)
const client = RedisClient.create({
socket: { socket: {
port: docker.port port: replica.port
} }
}).on("error", () => { }).connect();
return {
docker,
client
};
}
protected async spawnRedisSentinelNodes() {
const master = await this.spawnRedisSentinelNodeDocker();
const promises: Array<ReturnType<SentinelFramework['spawnRedisSentinelNodeDocker']>> = [];
for (let i = 0; i < (this.config.numberOfNodes ?? 0) - 1; i++) {
promises.push(
this.spawnRedisSentinelNodeDocker().then(async node => {
if (this.config.password !== undefined) {
await node.client.configSet({'masterauth': this.config.password})
}
await node.client.replicaOf('127.0.0.1', master.docker.port);
return node;
}) })
);
await client.connect();
await client.replicaOf("127.0.0.1", master.port);
await client.close();
} }
return [ return [
master, master,
...await Promise.all(promises) ...replicas
];
}
protected async spawnRedisSentinelSentinelDocker() {
const imageInfo: RedisServerDockerConfig = this.config.sentinelDockerConfig ?? { image: "redis", version: "latest" }
let serverArguments: Array<string>;
if (this.config.password === undefined) {
serverArguments = this.config.sentinelServerArgument ??
[
"/bin/bash",
"-c",
"\"touch /tmp/sentinel.conf ; /usr/local/bin/redis-sentinel /tmp/sentinel.conf {port} \""
];
} else {
serverArguments = this.config.sentinelServerArgument ??
[
"/bin/bash",
"-c",
`"touch /tmp/sentinel.conf ; /usr/local/bin/redis-sentinel /tmp/sentinel.conf {port} --requirepass ${this.config.password}"`
];
}
const docker = await this.spawnRedisServerDocker(imageInfo, serverArguments);
const client = await RedisClient.create({
modules: RedisSentinelModule,
password: this.config.password,
socket: {
port: docker.port
}
}).on("error", () => { }).connect();
return {
docker,
client
};
}
protected async spawnRedisSentinelSentinels() {
const quorum = this.config.sentinelQuorum?.toString() ?? "2";
const node = this.#nodeList[0];
const promises: Array<ReturnType<SentinelFramework['spawnRedisSentinelSentinelDocker']>> = [];
for (let i = 0; i < (this.config.numberOfSentinels ?? 3); i++) {
promises.push(
this.spawnRedisSentinelSentinelDocker().then(async sentinel => {
await sentinel.client.sentinel.sentinelMonitor(this.config.sentinelName, '127.0.0.1', node.docker.port.toString(), quorum);
const options: Array<{option: RedisArgument, value: RedisArgument}> = [];
options.push({ option: "down-after-milliseconds", value: "100" });
options.push({ option: "failover-timeout", value: "5000" });
if (this.config.password !== undefined) {
options.push({ option: "auth-pass", value: this.config.password });
}
await sentinel.client.sentinel.sentinelSet(this.config.sentinelName, options)
return sentinel;
})
);
}
return [
...await Promise.all(promises)
] ]
} }
protected async spawnRedisSentinelSentinels(masterPort: number, sentinels: number) {
return this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, masterPort, this.config.sentinelName, sentinels)
}
async getAllRunning() { async getAllRunning() {
for (const port of this.getAllNodesPort()) { for (const port of this.getAllNodesPort()) {
let first = true; let first = true;
@@ -384,54 +307,42 @@ export class SentinelFramework extends DockerBase {
} }
async addSentinel() { async addSentinel() {
const quorum = this.config.sentinelQuorum?.toString() ?? "2"; const nodes = await this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, this.#nodeList[0].port, this.config.sentinelName, 1)
const node = this.#nodeList[0]; this.#sentinelList.push(nodes[0]);
const sentinel = await this.spawnRedisSentinelSentinelDocker(); this.#sentinelMap.set(nodes[0].port.toString(), nodes[0]);
await sentinel.client.sentinel.sentinelMonitor(this.config.sentinelName, '127.0.0.1', node.docker.port.toString(), quorum);
const options: Array<{option: RedisArgument, value: RedisArgument}> = [];
options.push({ option: "down-after-milliseconds", value: "100" });
options.push({ option: "failover-timeout", value: "5000" });
if (this.config.password !== undefined) {
options.push({ option: "auth-pass", value: this.config.password });
}
await sentinel.client.sentinel.sentinelSet(this.config.sentinelName, options);
this.#sentinelList.push(sentinel);
this.#sentinelMap.set(sentinel.docker.port.toString(), sentinel);
} }
async addNode() { async addNode() {
const masterPort = await this.getMasterPort(); const masterPort = await this.getMasterPort();
const newNode = await this.spawnRedisSentinelNodeDocker(); const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS})
if (this.config.password !== undefined) { const client = RedisClient.create({
await newNode.client.configSet({'masterauth': this.config.password}) socket: {
port: replica.port
} }
await newNode.client.replicaOf('127.0.0.1', masterPort); })
this.#nodeList.push(newNode); await client.connect();
this.#nodeMap.set(newNode.docker.port.toString(), newNode); await client.replicaOf("127.0.0.1", masterPort);
await client.close();
this.#nodeList.push(replica);
this.#nodeMap.set(replica.port.toString(), replica);
} }
async getMaster(tracer?: Array<string>): Promise<string | undefined> { async getMaster(tracer?: Array<string>): Promise<string | undefined> {
for (const sentinel of this.#sentinelMap!.values()) { const client = RedisClient.create({
let info; name: this.config.sentinelName,
socket: {
try { host: "127.0.0.1",
if (!sentinel.client.isReady) { port: this.#sentinelList[0].port,
continue; },
} modules: RedisSentinelModule,
});
info = await sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); await client.connect()
if (tracer) { const info = await client.sentinel.sentinelMaster(this.config.sentinelName);
tracer.push('getMaster: master data returned from sentinel'); await client.close()
tracer.push(JSON.stringify(info, undefined, '\t'))
}
} catch (err) {
console.log("getMaster: sentinelMaster call failed: " + err);
continue;
}
const master = this.#nodeMap.get(info.port); const master = this.#nodeMap.get(info.port);
if (master === undefined) { if (master === undefined) {
@@ -439,35 +350,28 @@ export class SentinelFramework extends DockerBase {
} }
if (tracer) { if (tracer) {
tracer.push(`getMaster: master port is either ${info.port} or ${master.docker.port}`); tracer.push(`getMaster: master port is either ${info.port} or ${master.port}`);
}
if (!master.client.isOpen) {
throw new Error(`Sentinel's expected master node (${info.port}) is now down`);
} }
return info.port; return info.port;
} }
throw new Error("Couldn't get master");
}
async getMasterPort(tracer?: Array<string>): Promise<number> { async getMasterPort(tracer?: Array<string>): Promise<number> {
const data = await this.getMaster(tracer) const data = await this.getMaster(tracer)
return this.#nodeMap.get(data!)!.docker.port; return this.#nodeMap.get(data!)!.port;
} }
getRandomNode() { getRandomNode() {
return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].docker.port.toString(); return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].port.toString();
} }
async getRandonNonMasterNode(): Promise<string> { async getRandonNonMasterNode(): Promise<string> {
const masterPort = await this.getMasterPort(); const masterPort = await this.getMasterPort();
while (true) { while (true) {
const node = this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)]; const node = this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)];
if (node.docker.port != masterPort) { if (node.port != masterPort) {
return node.docker.port.toString(); return node.port.toString();
} }
} }
} }
@@ -479,11 +383,7 @@ export class SentinelFramework extends DockerBase {
throw new Error("unknown node: " + id); throw new Error("unknown node: " + id);
} }
if (node.client.isOpen) { return await this.dockerStop(node.dockerId);
node.client.destroy();
}
return await this.dockerStop(node.docker.dockerId);
} }
async restartNode(id: string) { async restartNode(id: string) {
@@ -492,15 +392,7 @@ export class SentinelFramework extends DockerBase {
throw new Error("unknown node: " + id); throw new Error("unknown node: " + id);
} }
await this.dockerStart(node.docker.dockerId); await this.dockerStart(node.dockerId);
if (!node.client.isOpen) {
node.client = await RedisClient.create({
password: this.config.password,
socket: {
port: node.docker.port
}
}).on("error", () => { }).connect();
}
} }
async stopSentinel(id: string) { async stopSentinel(id: string) {
@@ -509,11 +401,7 @@ export class SentinelFramework extends DockerBase {
throw new Error("unknown sentinel: " + id); throw new Error("unknown sentinel: " + id);
} }
if (sentinel.client.isOpen) { return await this.dockerStop(sentinel.dockerId);
sentinel.client.destroy();
}
return await this.dockerStop(sentinel.docker.dockerId);
} }
async restartSentinel(id: string) { async restartSentinel(id: string) {
@@ -522,16 +410,7 @@ export class SentinelFramework extends DockerBase {
throw new Error("unknown sentinel: " + id); throw new Error("unknown sentinel: " + id);
} }
await this.dockerStart(sentinel.docker.dockerId); await this.dockerStart(sentinel.dockerId);
if (!sentinel.client.isOpen) {
sentinel.client = await RedisClient.create({
modules: RedisSentinelModule,
password: this.config.password,
socket: {
port: sentinel.docker.port
}
}).on("error", () => { }).connect();
}
} }
getNodePort(id: string) { getNodePort(id: string) {
@@ -540,13 +419,13 @@ export class SentinelFramework extends DockerBase {
throw new Error("unknown node: " + id); throw new Error("unknown node: " + id);
} }
return node.docker.port; return node.port;
} }
getAllNodesPort() { getAllNodesPort() {
let ports: Array<number> = []; let ports: Array<number> = [];
for (const node of this.#nodeList) { for (const node of this.#nodeList) {
ports.push(node.docker.port); ports.push(node.port);
} }
return ports return ports
@@ -555,7 +434,7 @@ export class SentinelFramework extends DockerBase {
getAllDockerIds() { getAllDockerIds() {
let ids = new Map<string, number>(); let ids = new Map<string, number>();
for (const node of this.#nodeList) { for (const node of this.#nodeList) {
ids.set(node.docker.dockerId, node.docker.port); ids.set(node.dockerId, node.port);
} }
return ids; return ids;
@@ -567,43 +446,67 @@ export class SentinelFramework extends DockerBase {
throw new Error("unknown sentinel: " + id); throw new Error("unknown sentinel: " + id);
} }
return sentinel.docker.port; return sentinel.port;
} }
getAllSentinelsPort() { getAllSentinelsPort() {
let ports: Array<number> = []; let ports: Array<number> = [];
for (const sentinel of this.#sentinelList) { for (const sentinel of this.#sentinelList) {
ports.push(sentinel.docker.port); ports.push(sentinel.port);
} }
return ports return ports
} }
getSetinel(i: number): string { getSetinel(i: number): string {
return this.#sentinelList[i].docker.port.toString(); return this.#sentinelList[i].port.toString();
} }
sentinelSentinels() { async sentinelSentinels() {
for (const sentinel of this.#sentinelList) { const client = RedisClient.create({
if (sentinel.client.isReady) { name: this.config.sentinelName,
return sentinel.client.sentinel.sentinelSentinels(this.config.sentinelName); socket: {
} host: "127.0.0.1",
} port: this.#sentinelList[0].port,
},
modules: RedisSentinelModule,
});
await client.connect()
const sentinels = client.sentinel.sentinelSentinels(this.config.sentinelName)
await client.close()
return sentinels
} }
sentinelMaster() { async sentinelMaster() {
for (const sentinel of this.#sentinelList) { const client = RedisClient.create({
if (sentinel.client.isReady) { name: this.config.sentinelName,
return sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); socket: {
} host: "127.0.0.1",
} port: this.#sentinelList[0].port,
},
modules: RedisSentinelModule,
});
await client.connect()
const master = client.sentinel.sentinelMaster(this.config.sentinelName)
await client.close()
return master
} }
sentinelReplicas() { async sentinelReplicas() {
for (const sentinel of this.#sentinelList) { const client = RedisClient.create({
if (sentinel.client.isReady) { name: this.config.sentinelName,
return sentinel.client.sentinel.sentinelReplicas(this.config.sentinelName); socket: {
} host: "127.0.0.1",
} port: this.#sentinelList[0].port,
},
modules: RedisSentinelModule,
});
await client.connect()
const replicas = client.sentinel.sentinelReplicas(this.config.sentinelName)
await client.close()
return replicas
} }
} }

View File

@@ -14,7 +14,7 @@ const utils = TestUtils.createFromConfig({
export default utils; export default utils;
const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? export const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ?
['--enable-debug-command', 'yes'] : ['--enable-debug-command', 'yes'] :
[]; [];

View File

@@ -62,7 +62,7 @@ export interface RedisServerDocker {
dockerId: string; dockerId: string;
} }
async function spawnRedisServerDocker( export async function spawnRedisServerDocker(
options: RedisServerDockerOptions, serverArguments: Array<string>): Promise<RedisServerDocker> { options: RedisServerDockerOptions, serverArguments: Array<string>): Promise<RedisServerDocker> {
let port; let port;
if (options.mode == "sentinel") { if (options.mode == "sentinel") {
@@ -374,35 +374,16 @@ export async function spawnRedisSentinel(
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix));
for (let i = 0; i < sentinelCount; i++) { for (let i = 0; i < sentinelCount; i++) {
sentinelPromises.push((async () => { sentinelPromises.push(
const port = (await portIterator.next()).value; spawnSentinelNode(
dockerConfigs,
let sentinelConfig = `port ${port} serverArguments,
sentinel monitor mymaster 127.0.0.1 ${master.port} 2 master.port,
sentinel down-after-milliseconds mymaster 5000 "mymaster",
sentinel failover-timeout mymaster 6000 path.join(tmpDir, i.toString()),
`; password,
if (password !== undefined) { ),
sentinelConfig += `requirepass ${password}\n`; )
sentinelConfig += `sentinel auth-pass mymaster ${password}\n`;
}
const dir = fs.mkdtempSync(path.join(tmpDir, i.toString()));
fs.writeFile(`${dir}/redis.conf`, sentinelConfig, err => {
if (err) {
console.error("failed to create temporary config file", err);
}
});
return await spawnRedisServerDocker(
{
image: dockerConfigs.image,
version: dockerConfigs.version,
mode: "sentinel",
mounts: [`${dir}/redis.conf:/redis/config/node-sentinel-1/redis.conf`],
port: port,
}, serverArguments);
})());
} }
const sentinelNodes = await Promise.all(sentinelPromises); const sentinelNodes = await Promise.all(sentinelPromises);
@@ -424,3 +405,43 @@ after(() => {
}) })
); );
}); });
export async function spawnSentinelNode(
dockerConfigs: RedisServerDockerOptions,
serverArguments: Array<string>,
masterPort: number,
sentinelName: string,
tmpDir: string,
password?: string,
) {
const port = (await portIterator.next()).value;
let sentinelConfig = `port ${port}
sentinel monitor ${sentinelName} 127.0.0.1 ${masterPort} 2
sentinel down-after-milliseconds ${sentinelName} 500
sentinel failover-timeout ${sentinelName} 1000
`;
if (password !== undefined) {
sentinelConfig += `requirepass ${password}\n`;
sentinelConfig += `sentinel auth-pass ${sentinelName} ${password}\n`;
}
const dir = fs.mkdtempSync(tmpDir);
fs.writeFile(`${dir}/redis.conf`, sentinelConfig, err => {
if (err) {
console.error("failed to create temporary config file", err);
}
});
return await spawnRedisServerDocker(
{
image: dockerConfigs.image,
version: dockerConfigs.version,
mode: "sentinel",
mounts: [`${dir}/redis.conf:/redis/config/node-sentinel-1/redis.conf`],
port: port,
},
serverArguments,
);
}

View File

@@ -19,10 +19,13 @@ import {
RedisClusterType RedisClusterType
} from '@redis/client/index'; } from '@redis/client/index';
import { RedisNode } from '@redis/client/lib/sentinel/types' import { RedisNode } from '@redis/client/lib/sentinel/types'
import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions } from './dockers'; import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions, RedisServerDocker, spawnSentinelNode, spawnRedisServerDocker } from './dockers';
import yargs from 'yargs'; import yargs from 'yargs';
import { hideBin } from 'yargs/helpers'; import { hideBin } from 'yargs/helpers';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
interface TestUtilsConfig { interface TestUtilsConfig {
/** /**
@@ -395,19 +398,19 @@ export default class TestUtils {
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2, RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {} TYPE_MAPPING extends TypeMapping = {}
>( >(
range: ([minVersion: Array<number>, maxVersion: Array<number>] | [minVersion: Array<number>, 'LATEST']), range: ([minVersion: Array<number>, maxVersion: Array<number>] | [minVersion: Array<number>, 'LATEST']),
title: string, title: string,
fn: (sentinel: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>) => unknown, fn: (sentinel: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
options: SentinelTestOptions<M, F, S, RESP, TYPE_MAPPING> options: SentinelTestOptions<M, F, S, RESP, TYPE_MAPPING>
): void { ): void {
if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) { if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) {
return this.testWithClientSentinel(`${title} [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options) return this.testWithClientSentinel(`${title} [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options)
} else { } else {
console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`) console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`)
} }
} }
testWithClientPool< testWithClientPool<
M extends RedisModules = {}, M extends RedisModules = {},
@@ -541,4 +544,46 @@ export default class TestUtils {
this.testWithClient(`client.${title}`, fn, options.client); this.testWithClient(`client.${title}`, fn, options.client);
this.testWithCluster(`cluster.${title}`, fn, options.cluster); this.testWithCluster(`cluster.${title}`, fn, options.cluster);
} }
spawnRedisServer<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
// POLICIES extends CommandPolicies = {}
>(
options: ClientPoolTestOptions<M, F, S, RESP, TYPE_MAPPING>
): Promise<RedisServerDocker> {
return spawnRedisServerDocker(this.#DOCKER_IMAGE, options.serverArguments)
}
async spawnRedisSentinels<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
// POLICIES extends CommandPolicies = {}
>(
options: ClientPoolTestOptions<M, F, S, RESP, TYPE_MAPPING>,
masterPort: number,
sentinelName: string,
count: number
): Promise<Array<RedisServerDocker>> {
const sentinels: Array<RedisServerDocker> = [];
for (let i = 0; i < count; i++) {
const appPrefix = 'sentinel-config-dir';
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix));
sentinels.push(await spawnSentinelNode(this.#DOCKER_IMAGE, options.serverArguments, masterPort, sentinelName, tmpDir))
if (tmpDir) {
fs.rmSync(tmpDir, { recursive: true });
}
}
return sentinels
}
} }