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

fix(sentinel): Migrated to the new testing framework, fixed issues that were discovered during transition

* [CAE-342] Fix a couple of bugs

* Fixed issue with nodes masterauth persistency, changed docker container

* [CAE-342] Fixed a couple of sentinel issues, enabled most tests

* [CAE-342] Added comment

* [CAE-342] Migrate majority of tests to testUtils

* [CAE-342] Minor refactor

* .

* [CAE-342] Using cae containers for sentinel

* [CAE-342] Improved resiliency of the legacy tests, added TSdoc comment

* [CAE-342] Some extra logging, removed unneeded changes

* [CAE-342] Moved docker env as optional part of redisserverdockerconfig

* [CAE-342] Move password to serverArguments

* [CAE-342] Moved ts-node to devDependencies

* [CAE-342] Reverted legacy testing framework improvements
This commit is contained in:
Hristo Temelski
2025-04-30 15:56:29 +03:00
committed by GitHub
parent 048df302e4
commit 10ff6debab
9 changed files with 1459 additions and 1253 deletions

203
package-lock.json generated
View File

@@ -8,6 +8,9 @@
"workspaces": [
"./packages/*"
],
"dependencies": {
"ts-node": "^10.9.2"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@types/mocha": "^10.0.6",
@@ -664,6 +667,28 @@
"node": ">=6.9.0"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.19.12",
"cpu": [
@@ -804,7 +829,6 @@
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -820,7 +844,6 @@
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@@ -1067,10 +1090,6 @@
"resolved": "packages/entraid",
"link": true
},
"node_modules/@redis/graph": {
"resolved": "packages/graph",
"link": true
},
"node_modules/@redis/json": {
"resolved": "packages/json",
"link": true
@@ -1164,6 +1183,30 @@
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"license": "MIT"
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -1247,7 +1290,6 @@
},
"node_modules/@types/node": {
"version": "20.11.16",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
@@ -1330,6 +1372,30 @@
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "7.1.0",
"license": "MIT",
@@ -1448,6 +1514,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"dev": true,
@@ -2315,6 +2387,12 @@
}
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"dev": true,
@@ -5088,6 +5166,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"license": "ISC"
},
"node_modules/marked": {
"version": "4.3.0",
"dev": true,
@@ -7576,6 +7660,58 @@
"node": ">=0.8.0"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/ts-node/node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/tslib": {
"version": "2.6.2",
"license": "0BSD"
@@ -7741,7 +7877,6 @@
},
"node_modules/typescript": {
"version": "5.3.3",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -7780,7 +7915,6 @@
},
"node_modules/undici-types": {
"version": "5.26.5",
"dev": true,
"license": "MIT"
},
"node_modules/unicorn-magic": {
@@ -7956,6 +8090,12 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"license": "MIT"
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -8324,6 +8464,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"dev": true,
@@ -8353,7 +8502,7 @@
},
"packages/bloom": {
"name": "@redis/bloom",
"version": "5.0.0-next.6",
"version": "5.0.0-next.7",
"license": "MIT",
"devDependencies": {
"@redis/test-utils": "*"
@@ -8362,12 +8511,12 @@
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.0.0-next.6"
"@redis/client": "^5.0.0-next.7"
}
},
"packages/client": {
"name": "@redis/client",
"version": "5.0.0-next.6",
"version": "5.0.0-next.7",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2"
@@ -8383,7 +8532,7 @@
},
"packages/entraid": {
"name": "@redis/entraid",
"version": "5.0.0-next.6",
"version": "5.0.0-next.7",
"license": "MIT",
"dependencies": {
"@azure/identity": "4.7.0",
@@ -8402,7 +8551,7 @@
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.0.0-next.6"
"@redis/client": "^5.0.0-next.7"
}
},
"packages/entraid/node_modules/@types/node": {
@@ -8425,6 +8574,7 @@
"packages/graph": {
"name": "@redis/graph",
"version": "5.0.0-next.6",
"extraneous": true,
"license": "MIT",
"devDependencies": {
"@redis/test-utils": "*"
@@ -8438,7 +8588,7 @@
},
"packages/json": {
"name": "@redis/json",
"version": "5.0.0-next.6",
"version": "5.0.0-next.7",
"license": "MIT",
"devDependencies": {
"@redis/test-utils": "*"
@@ -8447,19 +8597,18 @@
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.0.0-next.6"
"@redis/client": "^5.0.0-next.7"
}
},
"packages/redis": {
"version": "5.0.0-next.6",
"version": "5.0.0-next.7",
"license": "MIT",
"dependencies": {
"@redis/bloom": "5.0.0-next.6",
"@redis/client": "5.0.0-next.6",
"@redis/graph": "5.0.0-next.6",
"@redis/json": "5.0.0-next.6",
"@redis/search": "5.0.0-next.6",
"@redis/time-series": "5.0.0-next.6"
"@redis/bloom": "5.0.0-next.7",
"@redis/client": "5.0.0-next.7",
"@redis/json": "5.0.0-next.7",
"@redis/search": "5.0.0-next.7",
"@redis/time-series": "5.0.0-next.7"
},
"engines": {
"node": ">= 18"
@@ -8467,7 +8616,7 @@
},
"packages/search": {
"name": "@redis/search",
"version": "5.0.0-next.6",
"version": "5.0.0-next.7",
"license": "MIT",
"devDependencies": {
"@redis/test-utils": "*"
@@ -8476,7 +8625,7 @@
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.0.0-next.6"
"@redis/client": "^5.0.0-next.7"
}
},
"packages/test-utils": {
@@ -8545,7 +8694,7 @@
},
"packages/time-series": {
"name": "@redis/time-series",
"version": "5.0.0-next.6",
"version": "5.0.0-next.7",
"license": "MIT",
"devDependencies": {
"@redis/test-utils": "*"
@@ -8554,7 +8703,7 @@
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.0.0-next.6"
"@redis/client": "^5.0.0-next.7"
}
}
}

View File

@@ -21,6 +21,7 @@
"release-it": "^17.0.3",
"tsx": "^4.7.0",
"typedoc": "^0.25.7",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"ts-node": "^10.9.2"
}
}

View File

@@ -299,6 +299,9 @@ export default class RedisClient<
#monitorCallback?: MonitorCallback<TYPE_MAPPING>;
private _self = this;
private _commandOptions?: CommandOptions<TYPE_MAPPING>;
// flag used to annotate that the client
// was in a watch transaction when
// a topology change occured
#dirtyWatch?: string;
#epoch: number;
#watchEpoch?: number;
@@ -325,6 +328,20 @@ export default class RedisClient<
return this._self.#watchEpoch !== undefined;
}
/**
* Indicates whether the client's WATCH command has been invalidated by a topology change.
* When this returns true, any transaction using WATCH will fail with a WatchError.
* @returns true if the watched keys have been modified, false otherwise
*/
get isDirtyWatch(): boolean {
return this._self.#dirtyWatch !== undefined
}
/**
* Marks the client's WATCH command as invalidated due to a topology change.
* This will cause any subsequent EXEC in a transaction to fail with a WatchError.
* @param msg - The error message explaining why the WATCH is dirty
*/
setDirtyWatch(msg: string) {
this._self.#dirtyWatch = msg;
}

File diff suppressed because it is too large Load Diff

View File

@@ -345,9 +345,12 @@ export default class RedisSentinel<
key: K,
value: V
) {
const proxy = Object.create(this._self);
proxy._commandOptions = Object.create(this._self.#commandOptions ?? null);
proxy._commandOptions[key] = value;
const proxy = Object.create(this);
// Create new commandOptions object with the inherited properties
proxy._self.#commandOptions = {
...(this._self.#commandOptions || {}),
[key]: value
};
return proxy as RedisSentinelType<
M,
F,
@@ -685,6 +688,7 @@ class RedisSentinelInternal<
while (true) {
this.#trace("starting connect loop");
count+=1;
if (this.#destroy) {
this.#trace("in #connect and want to destroy")
return;
@@ -1109,7 +1113,7 @@ class RedisSentinelInternal<
this.#trace(`transform: destroying old masters if open`);
for (const client of this.#masterClients) {
masterWatches.push(client.isWatching);
masterWatches.push(client.isWatching || client.isDirtyWatch);
if (client.isOpen) {
client.destroy()

View File

@@ -2,9 +2,10 @@ import TestUtils from '@redis/test-utils';
import { SinonSpy } from 'sinon';
import { setTimeout } from 'node:timers/promises';
import { CredentialsProvider } from './authx';
import { Command } from './RESP/types';
import { BasicCommandParser } from './client/parser';
import { Command, NumberReply } from './RESP/types';
import { BasicCommandParser, CommandParser } from './client/parser';
import { defineScript } from './lua-script';
import RedisBloomModules from '@redis/bloom';
const utils = TestUtils.createFromConfig({
dockerImageName: 'redislabs/client-libs-test',
dockerImageVersionArgument: 'redis-version',
@@ -42,6 +43,45 @@ const streamingCredentialsProvider: CredentialsProvider =
} as const;
const SQUARE_SCRIPT = defineScript({
SCRIPT:
`local number = redis.call('GET', KEYS[1])
return number * number`,
NUMBER_OF_KEYS: 1,
FIRST_KEY_INDEX: 0,
parseCommand(parser: CommandParser, key: string) {
parser.pushKey(key);
},
transformReply: undefined as unknown as () => NumberReply
});
export const MATH_FUNCTION = {
name: 'math',
engine: 'LUA',
code:
`#!LUA name=math
redis.register_function {
function_name = "square",
callback = function(keys, args)
local number = redis.call('GET', keys[1])
return number * number
end,
flags = { "no-writes" }
}`,
library: {
square: {
NAME: 'square',
IS_READ_ONLY: true,
NUMBER_OF_KEYS: 1,
FIRST_KEY_INDEX: 0,
parseCommand(parser: CommandParser, key: string) {
parser.pushKey(key);
},
transformReply: undefined as unknown as () => NumberReply
}
}
};
export const GLOBAL = {
SERVERS: {
OPEN: {
@@ -86,6 +126,43 @@ export const GLOBAL = {
useReplicas: true
}
}
},
SENTINEL: {
OPEN: {
serverArguments: [...DEBUG_MODE_ARGS],
},
PASSWORD: {
serverArguments: ['--requirepass', 'test_password', ...DEBUG_MODE_ARGS],
},
WITH_SCRIPT: {
serverArguments: [...DEBUG_MODE_ARGS],
scripts: {
square: SQUARE_SCRIPT,
},
},
WITH_FUNCTION: {
serverArguments: [...DEBUG_MODE_ARGS],
functions: {
math: MATH_FUNCTION.library,
},
},
WITH_MODULE: {
serverArguments: [...DEBUG_MODE_ARGS],
modules: RedisBloomModules,
},
WITH_REPLICA_POOL_SIZE_1: {
serverArguments: [...DEBUG_MODE_ARGS],
replicaPoolSize: 1,
},
WITH_RESERVE_CLIENT_MASTER_POOL_SIZE_2: {
serverArguments: [...DEBUG_MODE_ARGS],
masterPoolSize: 2,
reserveClient: true,
},
WITH_MASTER_POOL_SIZE_2: {
serverArguments: [...DEBUG_MODE_ARGS],
masterPoolSize: 2,
}
}
};

View File

@@ -4,9 +4,11 @@ import { once } from 'node:events';
import { createClient } from '@redis/client/index';
import { setTimeout } from 'node:timers/promises';
// import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS';
import { execFile as execFileCallback } from 'node:child_process';
import { promisify } from 'node:util';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
const execAsync = promisify(execFileCallback);
@@ -38,37 +40,64 @@ const portIterator = (async function* (): AsyncIterableIterator<number> {
throw new Error('All ports are in use');
})();
export interface RedisServerDockerConfig {
interface RedisServerDockerConfig {
image: string;
version: string;
}
interface SentinelConfig {
mode: "sentinel";
mounts: Array<string>;
port: number;
}
interface ServerConfig {
mode: "server";
}
export type RedisServerDockerOptions = RedisServerDockerConfig & (SentinelConfig | ServerConfig)
export interface RedisServerDocker {
port: number;
dockerId: string;
}
async function spawnRedisServerDocker({
image,
version
}: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
const port = (await portIterator.next()).value;
async function spawnRedisServerDocker(
options: RedisServerDockerOptions, serverArguments: Array<string>): Promise<RedisServerDocker> {
let port;
if (options.mode == "sentinel") {
port = options.port;
} else {
port = (await portIterator.next()).value;
}
const portStr = port.toString();
const dockerArgs = [
'run',
'-e', `PORT=${portStr}`,
'-d',
'--network', 'host',
`${image}:${version}`,
'--port', portStr
'--init',
'-e', `PORT=${portStr}`
];
if (serverArguments.length > 0) {
dockerArgs.push(...serverArguments);
if (options.mode == "sentinel") {
options.mounts.forEach(mount => {
dockerArgs.push('-v', mount);
});
}
console.log(`[Docker] Spawning Redis container - Image: ${image}:${version}, Port: ${port}`);
dockerArgs.push(
'-d',
'--network', 'host',
`${options.image}:${options.version}`
);
if (serverArguments.length > 0) {
for (let i = 0; i < serverArguments.length; i++) {
dockerArgs.push(serverArguments[i])
}
}
console.log(`[Docker] Spawning Redis container - Image: ${options.image}:${options.version}, Port: ${port}, Mode: ${options.mode}`);
const { stdout, stderr } = await execAsync('docker', dockerArgs);
@@ -87,7 +116,7 @@ async function spawnRedisServerDocker({
}
const RUNNING_SERVERS = new Map<Array<string>, ReturnType<typeof spawnRedisServerDocker>>();
export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
export function spawnRedisServer(dockerConfig: RedisServerDockerOptions, serverArguments: Array<string>): Promise<RedisServerDocker> {
const runningServer = RUNNING_SERVERS.get(serverArguments);
if (runningServer) {
return runningServer;
@@ -113,7 +142,7 @@ after(() => {
);
});
export interface RedisClusterDockersConfig extends RedisServerDockerConfig {
export type RedisClusterDockersConfig = RedisServerDockerOptions & {
numberOfMasters?: number;
numberOfReplicas?: number;
}
@@ -178,7 +207,7 @@ async function spawnRedisClusterNodeDockers(
}
async function spawnRedisClusterNodeDocker(
dockersConfig: RedisClusterDockersConfig,
dockersConfig: RedisServerDockerOptions,
serverArguments: Array<string>,
clientConfig?: Partial<RedisClusterClientOptions>
) {
@@ -291,3 +320,107 @@ after(() => {
})
);
});
const RUNNING_NODES = new Map<Array<string>, Array<RedisServerDocker>>();
const RUNNING_SENTINELS = new Map<Array<string>, Array<RedisServerDocker>>();
export async function spawnRedisSentinel(
dockerConfigs: RedisServerDockerOptions,
serverArguments: Array<string>,
): Promise<Array<RedisServerDocker>> {
const runningNodes = RUNNING_SENTINELS.get(serverArguments);
if (runningNodes) {
return runningNodes;
}
const passIndex = serverArguments.indexOf('--requirepass')+1;
let password: string | undefined = undefined;
if (passIndex != 0) {
password = serverArguments[passIndex];
}
const master = await spawnRedisServerDocker(dockerConfigs, serverArguments);
const redisNodes: Array<RedisServerDocker> = [master];
const replicaPromises: Array<Promise<RedisServerDocker>> = [];
const replicasCount = 2;
for (let i = 0; i < replicasCount; i++) {
replicaPromises.push((async () => {
const replica = await spawnRedisServerDocker(dockerConfigs, serverArguments);
const client = createClient({
socket: {
port: replica.port
},
password: password
});
await client.connect();
await client.replicaOf("127.0.0.1", master.port);
await client.close();
return replica;
})());
}
const replicas = await Promise.all(replicaPromises);
redisNodes.push(...replicas);
RUNNING_NODES.set(serverArguments, redisNodes);
const sentinelPromises: Array<Promise<RedisServerDocker>> = [];
const sentinelCount = 3;
const appPrefix = 'sentinel-config-dir';
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix));
for (let i = 0; i < sentinelCount; i++) {
sentinelPromises.push((async () => {
const port = (await portIterator.next()).value;
let sentinelConfig = `port ${port}
sentinel monitor mymaster 127.0.0.1 ${master.port} 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 6000
`;
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);
RUNNING_SENTINELS.set(serverArguments, sentinelNodes);
if (tmpDir) {
fs.rmSync(tmpDir, { recursive: true });
}
return sentinelNodes;
}
after(() => {
return Promise.all(
[...RUNNING_NODES.values(), ...RUNNING_SENTINELS.values()].map(async dockersPromise => {
return Promise.all(
dockersPromise.map(({ dockerId }) => dockerRemove(dockerId))
);
})
);
});

View File

@@ -6,8 +6,11 @@ import {
TypeMapping,
// CommandPolicies,
createClient,
createSentinel,
RedisClientOptions,
RedisClientType,
RedisSentinelOptions,
RedisSentinelType,
RedisPoolOptions,
RedisClientPoolType,
createClientPool,
@@ -15,7 +18,8 @@ import {
RedisClusterOptions,
RedisClusterType
} from '@redis/client/index';
import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers';
import { RedisNode } from '@redis/client/lib/sentinel/types'
import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions } from './dockers';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
@@ -68,6 +72,24 @@ interface ClientTestOptions<
disableClientSetup?: boolean;
}
interface SentinelTestOptions<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
> extends CommonTestOptions {
sentinelOptions?: Partial<RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>>;
clientOptions?: Partial<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>>;
scripts?: S;
functions?: F;
modules?: M;
disableClientSetup?: boolean;
replicaPoolSize?: number;
masterPoolSize?: number;
reserveClient?: boolean;
}
interface ClientPoolTestOptions<
M extends RedisModules,
F extends RedisFunctions,
@@ -148,13 +170,14 @@ export default class TestUtils {
}
readonly #VERSION_NUMBERS: Array<number>;
readonly #DOCKER_IMAGE: RedisServerDockerConfig;
readonly #DOCKER_IMAGE: RedisServerDockerOptions;
constructor({ string, numbers }: Version, dockerImageName: string) {
this.#VERSION_NUMBERS = numbers;
this.#DOCKER_IMAGE = {
image: dockerImageName,
version: string
version: string,
mode: "server"
};
}
@@ -179,7 +202,6 @@ export default class TestUtils {
}
isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {
const isVersionGreaterThanHook = this.isVersionGreaterThan.bind(this);
const versionNumber = this.#VERSION_NUMBERS.join('.');
const minimumVersionString = minimumVersion?.join('.');
@@ -272,6 +294,81 @@ export default class TestUtils {
});
}
testWithClientSentinel<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
>(
title: string,
fn: (sentinel: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
options: SentinelTestOptions<M, F, S, RESP, TYPE_MAPPING>
): void {
let dockerPromises: ReturnType<typeof spawnRedisSentinel>;
const passIndex = options.serverArguments.indexOf('--requirepass')+1;
let password: string | undefined = undefined;
if (passIndex != 0) {
password = options.serverArguments[passIndex];
}
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
const dockerImage = this.#DOCKER_IMAGE;
before(function () {
this.timeout(30000);
dockerPromises = spawnRedisSentinel(dockerImage, options.serverArguments);
return dockerPromises;
});
}
it(title, async function () {
this.timeout(30000);
if (options.skipTest) return this.skip();
if (!dockerPromises) return this.skip();
const promises = await dockerPromises;
const rootNodes: Array<RedisNode> = promises.map(promise => ({
host: "127.0.0.1",
port: promise.port
}));
const sentinel = createSentinel({
name: 'mymaster',
sentinelRootNodes: rootNodes,
nodeClientOptions: {
password: password || undefined,
},
sentinelClientOptions: {
password: password || undefined,
},
replicaPoolSize: options?.replicaPoolSize || 0,
scripts: options?.scripts || {},
modules: options?.modules || {},
functions: options?.functions || {},
masterPoolSize: options?.masterPoolSize || undefined,
reserveClient: options?.reserveClient || false,
}) as RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>;
if (options.disableClientSetup) {
return fn(sentinel);
}
await sentinel.connect();
try {
await sentinel.flushAll();
await fn(sentinel);
} finally {
if (sentinel.isOpen) {
await sentinel.flushAll();
sentinel.destroy();
}
}
});
}
testWithClientIfVersionWithinRange<
M extends RedisModules = {},
F extends RedisFunctions = {},
@@ -290,7 +387,26 @@ export default class TestUtils {
} 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'}`)
}
}
testWithClienSentineltIfVersionWithinRange<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
>(
range: ([minVersion: Array<number>, maxVersion: Array<number>] | [minVersion: Array<number>, 'LATEST']),
title: string,
fn: (sentinel: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
options: SentinelTestOptions<M, F, S, RESP, TYPE_MAPPING>
): void {
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)
} 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'}`)
}
}
testWithClientPool<