You've already forked node-redis
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:
@@ -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');
|
||||||
|
@@ -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);
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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'] :
|
||||||
[];
|
[];
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user