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

V5 bringing RESP3, Sentinel and TypeMapping to node-redis

RESP3 Support
   - Some commands responses in RESP3 aren't stable yet and therefore return an "untyped" ReplyUnion.
 
Sentinel

TypeMapping

Correctly types Multi commands

Note: some API changes to be further documented in v4-to-v5.md
This commit is contained in:
Shaya Potter
2024-10-15 17:46:52 +03:00
committed by GitHub
parent 2fc79bdfb3
commit b2d35c5286
1174 changed files with 45931 additions and 36274 deletions

View File

@@ -1,260 +1,256 @@
import { createConnection } from 'net';
import { once } from 'events';
import RedisClient from '@redis/client/dist/lib/client';
import { promiseTimeout } from '@redis/client/dist/lib/utils';
import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS';
import * as path from 'path';
import { promisify } from 'util';
import { exec } from 'child_process';
import { createConnection } from 'node:net';
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 { promisify } from 'node:util';
import { exec } from 'node:child_process';
const execAsync = promisify(exec);
interface ErrorWithCode extends Error {
code: string;
code: string;
}
async function isPortAvailable(port: number): Promise<boolean> {
try {
const socket = createConnection({ port });
await once(socket, 'connect');
socket.end();
} catch (err) {
if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') {
return true;
}
try {
const socket = createConnection({ port });
await once(socket, 'connect');
socket.end();
} catch (err) {
if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') {
return true;
}
}
return false;
return false;
}
const portIterator = (async function*(): AsyncIterableIterator<number> {
for (let i = 6379; i < 65535; i++) {
if (await isPortAvailable(i)) {
yield i;
}
const portIterator = (async function* (): AsyncIterableIterator<number> {
for (let i = 6379; i < 65535; i++) {
if (await isPortAvailable(i)) {
yield i;
}
}
throw new Error('All ports are in use');
throw new Error('All ports are in use');
})();
export interface RedisServerDockerConfig {
image: string;
version: string;
image: string;
version: string;
}
export interface RedisServerDocker {
port: number;
dockerId: string;
port: number;
dockerId: string;
}
// ".." cause it'll be in `./dist`
const DOCKER_FODLER_PATH = path.join(__dirname, '../docker');
async function spawnRedisServerDocker({ image, version }: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
const port = (await portIterator.next()).value,
{ stdout, stderr } = await execAsync(
'docker run -d --network host $(' +
`docker build ${DOCKER_FODLER_PATH} -q ` +
`--build-arg IMAGE=${image}:${version} ` +
`--build-arg REDIS_ARGUMENTS="--save '' --port ${port.toString()} ${serverArguments.join(' ')}"` +
')'
);
const port = (await portIterator.next()).value,
{ stdout, stderr } = await execAsync(
`docker run -e REDIS_ARGS="--port ${port.toString()} ${serverArguments.join(' ')}" -d --network host ${image}:${version}`
);
if (!stdout) {
throw new Error(`docker run error - ${stderr}`);
}
if (!stdout) {
throw new Error(`docker run error - ${stderr}`);
}
while (await isPortAvailable(port)) {
await promiseTimeout(50);
}
while (await isPortAvailable(port)) {
await setTimeout(50);
}
return {
port,
dockerId: stdout.trim()
};
return {
port,
dockerId: stdout.trim()
};
}
const RUNNING_SERVERS = new Map<Array<string>, ReturnType<typeof spawnRedisServerDocker>>();
export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
const runningServer = RUNNING_SERVERS.get(serverArguments);
if (runningServer) {
return runningServer;
}
const runningServer = RUNNING_SERVERS.get(serverArguments);
if (runningServer) {
return runningServer;
}
const dockerPromise = spawnRedisServerDocker(dockerConfig, serverArguments);
RUNNING_SERVERS.set(serverArguments, dockerPromise);
return dockerPromise;
const dockerPromise = spawnRedisServerDocker(dockerConfig, serverArguments);
RUNNING_SERVERS.set(serverArguments, dockerPromise);
return dockerPromise;
}
async function dockerRemove(dockerId: string): Promise<void> {
const { stderr } = await execAsync(`docker rm -f ${dockerId}`);
if (stderr) {
throw new Error(`docker rm error - ${stderr}`);
}
const { stderr } = await execAsync(`docker rm -f ${dockerId}`);
if (stderr) {
throw new Error(`docker rm error - ${stderr}`);
}
}
after(() => {
return Promise.all(
[...RUNNING_SERVERS.values()].map(async dockerPromise =>
await dockerRemove((await dockerPromise).dockerId)
)
);
return Promise.all(
[...RUNNING_SERVERS.values()].map(async dockerPromise =>
await dockerRemove((await dockerPromise).dockerId)
)
);
});
export interface RedisClusterDockersConfig extends RedisServerDockerConfig {
numberOfMasters?: number;
numberOfReplicas?: number;
numberOfMasters?: number;
numberOfReplicas?: number;
}
async function spawnRedisClusterNodeDockers(
dockersConfig: RedisClusterDockersConfig,
serverArguments: Array<string>,
fromSlot: number,
toSlot: number
dockersConfig: RedisClusterDockersConfig,
serverArguments: Array<string>,
fromSlot: number,
toSlot: number
) {
const range: Array<number> = [];
for (let i = fromSlot; i < toSlot; i++) {
range.push(i);
}
const range: Array<number> = [];
for (let i = fromSlot; i < toSlot; i++) {
range.push(i);
}
const master = await spawnRedisClusterNodeDocker(
dockersConfig,
serverArguments
);
const master = await spawnRedisClusterNodeDocker(
dockersConfig,
serverArguments
);
await master.client.clusterAddSlots(range);
await master.client.clusterAddSlots(range);
if (!dockersConfig.numberOfReplicas) return [master];
const replicasPromises: Array<ReturnType<typeof spawnRedisClusterNodeDocker>> = [];
for (let i = 0; i < (dockersConfig.numberOfReplicas ?? 0); i++) {
replicasPromises.push(
spawnRedisClusterNodeDocker(dockersConfig, [
...serverArguments,
'--cluster-enabled',
'yes',
'--cluster-node-timeout',
'5000'
]).then(async replica => {
await replica.client.clusterMeet('127.0.0.1', master.docker.port);
if (!dockersConfig.numberOfReplicas) return [master];
while ((await replica.client.clusterSlots()).length === 0) {
await promiseTimeout(50);
}
const replicasPromises: Array<ReturnType<typeof spawnRedisClusterNodeDocker>> = [];
for (let i = 0; i < (dockersConfig.numberOfReplicas ?? 0); i++) {
replicasPromises.push(
spawnRedisClusterNodeDocker(dockersConfig, [
...serverArguments,
'--cluster-enabled',
'yes',
'--cluster-node-timeout',
'5000'
]).then(async replica => {
await replica.client.clusterMeet('127.0.0.1', master.docker.port);
await replica.client.clusterReplicate(
await master.client.clusterMyId()
);
while ((await replica.client.clusterSlots()).length === 0) {
await setTimeout(50);
}
return replica;
})
await replica.client.clusterReplicate(
await master.client.clusterMyId()
);
}
return [
master,
...await Promise.all(replicasPromises)
];
return replica;
})
);
}
return [
master,
...await Promise.all(replicasPromises)
];
}
async function spawnRedisClusterNodeDocker(
dockersConfig: RedisClusterDockersConfig,
serverArguments: Array<string>
dockersConfig: RedisClusterDockersConfig,
serverArguments: Array<string>
) {
const docker = await spawnRedisServerDocker(dockersConfig, [
...serverArguments,
'--cluster-enabled',
'yes',
'--cluster-node-timeout',
'5000'
]),
client = RedisClient.create({
socket: {
port: docker.port
}
});
const docker = await spawnRedisServerDocker(dockersConfig, [
...serverArguments,
'--cluster-enabled',
'yes',
'--cluster-node-timeout',
'5000'
]),
client = createClient({
socket: {
port: docker.port
}
});
await client.connect();
await client.connect();
return {
docker,
client
};
return {
docker,
client
};
}
const SLOTS = 16384;
async function spawnRedisClusterDockers(
dockersConfig: RedisClusterDockersConfig,
serverArguments: Array<string>
dockersConfig: RedisClusterDockersConfig,
serverArguments: Array<string>
): Promise<Array<RedisServerDocker>> {
const numberOfMasters = dockersConfig.numberOfMasters ?? 2,
slotsPerNode = Math.floor(SLOTS / numberOfMasters),
spawnPromises: Array<ReturnType<typeof spawnRedisClusterNodeDockers>> = [];
for (let i = 0; i < numberOfMasters; i++) {
const fromSlot = i * slotsPerNode,
toSlot = i === numberOfMasters - 1 ? SLOTS : fromSlot + slotsPerNode;
spawnPromises.push(
spawnRedisClusterNodeDockers(
dockersConfig,
serverArguments,
fromSlot,
toSlot
)
);
}
const nodes = (await Promise.all(spawnPromises)).flat(),
meetPromises: Array<Promise<unknown>> = [];
for (let i = 1; i < nodes.length; i++) {
meetPromises.push(
nodes[i].client.clusterMeet('127.0.0.1', nodes[0].docker.port)
);
}
await Promise.all(meetPromises);
await Promise.all(
nodes.map(async ({ client }) => {
while (totalNodes(await client.clusterSlots()) !== nodes.length) {
await promiseTimeout(50);
}
return client.disconnect();
})
const numberOfMasters = dockersConfig.numberOfMasters ?? 2,
slotsPerNode = Math.floor(SLOTS / numberOfMasters),
spawnPromises: Array<ReturnType<typeof spawnRedisClusterNodeDockers>> = [];
for (let i = 0; i < numberOfMasters; i++) {
const fromSlot = i * slotsPerNode,
toSlot = i === numberOfMasters - 1 ? SLOTS : fromSlot + slotsPerNode;
spawnPromises.push(
spawnRedisClusterNodeDockers(
dockersConfig,
serverArguments,
fromSlot,
toSlot
)
);
}
return nodes.map(({ docker }) => docker);
const nodes = (await Promise.all(spawnPromises)).flat(),
meetPromises: Array<Promise<unknown>> = [];
for (let i = 1; i < nodes.length; i++) {
meetPromises.push(
nodes[i].client.clusterMeet('127.0.0.1', nodes[0].docker.port)
);
}
await Promise.all(meetPromises);
await Promise.all(
nodes.map(async ({ client }) => {
while (
totalNodes(await client.clusterSlots()) !== nodes.length ||
!(await client.sendCommand<string>(['CLUSTER', 'INFO'])).startsWith('cluster_state:ok') // TODO
) {
await setTimeout(50);
}
client.destroy();
})
);
return nodes.map(({ docker }) => docker);
}
function totalNodes(slots: ClusterSlotsReply) {
let total = slots.length;
for (const slot of slots) {
total += slot.replicas.length;
}
// TODO: type ClusterSlotsReply
function totalNodes(slots: any) {
let total = slots.length;
for (const slot of slots) {
total += slot.replicas.length;
}
return total;
return total;
}
const RUNNING_CLUSTERS = new Map<Array<string>, ReturnType<typeof spawnRedisClusterDockers>>();
export function spawnRedisCluster(dockersConfig: RedisClusterDockersConfig, serverArguments: Array<string>): Promise<Array<RedisServerDocker>> {
const runningCluster = RUNNING_CLUSTERS.get(serverArguments);
if (runningCluster) {
return runningCluster;
}
const runningCluster = RUNNING_CLUSTERS.get(serverArguments);
if (runningCluster) {
return runningCluster;
}
const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments);
RUNNING_CLUSTERS.set(serverArguments, dockersPromise);
return dockersPromise;
const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments);
RUNNING_CLUSTERS.set(serverArguments, dockersPromise);
return dockersPromise;
}
after(() => {
return Promise.all(
[...RUNNING_CLUSTERS.values()].map(async dockersPromise => {
return Promise.all(
(await dockersPromise).map(({ dockerId }) => dockerRemove(dockerId))
);
})
);
return Promise.all(
[...RUNNING_CLUSTERS.values()].map(async dockersPromise => {
return Promise.all(
(await dockersPromise).map(({ dockerId }) => dockerRemove(dockerId))
);
})
);
});

View File

@@ -1,226 +1,339 @@
import { RedisModules, RedisFunctions, RedisScripts } from '@redis/client/lib/commands';
import RedisClient, { RedisClientOptions, RedisClientType } from '@redis/client/lib/client';
import RedisCluster, { RedisClusterOptions, RedisClusterType } from '@redis/client/lib/cluster';
import { RedisSocketCommonOptions } from '@redis/client/lib/client/socket';
import {
RedisModules,
RedisFunctions,
RedisScripts,
RespVersions,
TypeMapping,
// CommandPolicies,
createClient,
RedisClientOptions,
RedisClientType,
RedisPoolOptions,
RedisClientPoolType,
createClientPool,
createCluster,
RedisClusterOptions,
RedisClusterType
} from '@redis/client/index';
import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
interface TestUtilsConfig {
dockerImageName: string;
dockerImageVersionArgument: string;
defaultDockerVersion?: string;
dockerImageName: string;
dockerImageVersionArgument: string;
defaultDockerVersion?: string;
}
interface CommonTestOptions {
minimumDockerVersion?: Array<number>;
serverArguments: Array<string>;
minimumDockerVersion?: Array<number>;
}
interface ClientTestOptions<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
> extends CommonTestOptions {
serverArguments: Array<string>;
clientOptions?: Partial<Omit<RedisClientOptions<M, F, S>, 'socket'> & { socket: RedisSocketCommonOptions }>;
disableClientSetup?: boolean;
clientOptions?: Partial<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>>;
disableClientSetup?: boolean;
}
interface ClientPoolTestOptions<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
> extends CommonTestOptions {
clientOptions?: Partial<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>>;
poolOptions?: RedisPoolOptions;
}
interface ClusterTestOptions<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
// POLICIES extends CommandPolicies
> extends CommonTestOptions {
serverArguments: Array<string>;
clusterConfiguration?: Partial<RedisClusterOptions<M, F, S>>;
numberOfMasters?: number;
numberOfReplicas?: number;
clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>>;
numberOfMasters?: number;
numberOfReplicas?: number;
}
interface AllTestOptions<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
// POLICIES extends CommandPolicies
> {
client: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>;
cluster: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>;
}
interface Version {
string: string;
numbers: Array<number>;
string: string;
numbers: Array<number>;
}
export default class TestUtils {
static #parseVersionNumber(version: string): Array<number> {
if (version === 'latest' || version === 'edge') return [Infinity];
static #parseVersionNumber(version: string): Array<number> {
if (version === 'latest' || version === 'edge') return [Infinity];
const dashIndex = version.indexOf('-');
return (dashIndex === -1 ? version : version.substring(0, dashIndex))
.split('.')
.map(x => {
const value = Number(x);
if (Number.isNaN(value)) {
throw new TypeError(`${version} is not a valid redis version`);
}
const dashIndex = version.indexOf('-');
return (dashIndex === -1 ? version : version.substring(0, dashIndex))
.split('.')
.map(x => {
const value = Number(x);
if (Number.isNaN(value)) {
throw new TypeError(`${version} is not a valid redis version`);
}
return value;
});
}
return value;
});
}
static #getVersion(argumentName: string, defaultVersion = 'latest'): Version {
return yargs(hideBin(process.argv))
.option(argumentName, {
type: 'string',
default: defaultVersion
})
.coerce(argumentName, (version: string) => {
return {
string: version,
numbers: TestUtils.#parseVersionNumber(version)
};
})
.demandOption(argumentName)
.parseSync()[argumentName];
}
readonly #VERSION_NUMBERS: Array<number>;
readonly #DOCKER_IMAGE: RedisServerDockerConfig;
constructor(config: TestUtilsConfig) {
const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion);
this.#VERSION_NUMBERS = numbers;
this.#DOCKER_IMAGE = {
image: config.dockerImageName,
version: string
static #getVersion(argumentName: string, defaultVersion = 'latest'): Version {
return yargs(hideBin(process.argv))
.option(argumentName, {
type: 'string',
default: defaultVersion
})
.coerce(argumentName, (version: string) => {
return {
string: version,
numbers: TestUtils.#parseVersionNumber(version)
};
})
.demandOption(argumentName)
.parseSync()[argumentName];
}
readonly #VERSION_NUMBERS: Array<number>;
readonly #DOCKER_IMAGE: RedisServerDockerConfig;
constructor(config: TestUtilsConfig) {
const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion);
this.#VERSION_NUMBERS = numbers;
this.#DOCKER_IMAGE = {
image: config.dockerImageName,
version: string
};
}
isVersionGreaterThan(minimumVersion: Array<number> | undefined): boolean {
if (minimumVersion === undefined) return true;
const lastIndex = Math.min(this.#VERSION_NUMBERS.length, minimumVersion.length) - 1;
for (let i = 0; i < lastIndex; i++) {
if (this.#VERSION_NUMBERS[i] > minimumVersion[i]) {
return true;
} else if (minimumVersion[i] > this.#VERSION_NUMBERS[i]) {
return false;
}
}
isVersionGreaterThan(minimumVersion: Array<number> | undefined): boolean {
if (minimumVersion === undefined) return true;
return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex];
}
const lastIndex = Math.min(this.#VERSION_NUMBERS.length, minimumVersion.length) - 1;
for (let i = 0; i < lastIndex; i++) {
if (this.#VERSION_NUMBERS[i] > minimumVersion[i]) {
return true;
} else if (minimumVersion[i] > this.#VERSION_NUMBERS[i]) {
return false;
}
isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {
const isVersionGreaterThan = this.isVersionGreaterThan.bind(this);
before(function () {
if (!isVersionGreaterThan(minimumVersion)) {
return this.skip();
}
});
}
testWithClient<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
>(
title: string,
fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
options: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>
): void {
let dockerPromise: ReturnType<typeof spawnRedisServer>;
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
const dockerImage = this.#DOCKER_IMAGE;
before(function () {
this.timeout(30000);
dockerPromise = spawnRedisServer(dockerImage, options.serverArguments);
return dockerPromise;
});
}
it(title, async function () {
if (!dockerPromise) return this.skip();
const client = createClient({
...options.clientOptions,
socket: {
...options.clientOptions?.socket,
// TODO
// @ts-ignore
port: (await dockerPromise).port
}
});
return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex];
}
if (options.disableClientSetup) {
return fn(client);
}
isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {
const isVersionGreaterThan = this.isVersionGreaterThan.bind(this);
before(function () {
if (!isVersionGreaterThan(minimumVersion)) {
return this.skip();
}
});
}
await client.connect();
testWithClient<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(
title: string,
fn: (client: RedisClientType<M, F, S>) => unknown,
options: ClientTestOptions<M, F, S>
): void {
let dockerPromise: ReturnType<typeof spawnRedisServer>;
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
const dockerImage = this.#DOCKER_IMAGE;
before(function () {
this.timeout(30000);
dockerPromise = spawnRedisServer(dockerImage, options.serverArguments);
return dockerPromise;
});
try {
await client.flushAll();
await fn(client);
} finally {
if (client.isOpen) {
await client.flushAll();
client.destroy();
}
}
});
}
it(title, async function() {
if (!dockerPromise) return this.skip();
testWithClientPool<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
>(
title: string,
fn: (client: RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
options: ClientPoolTestOptions<M, F, S, RESP, TYPE_MAPPING>
): void {
let dockerPromise: ReturnType<typeof spawnRedisServer>;
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
const dockerImage = this.#DOCKER_IMAGE;
before(function () {
this.timeout(30000);
const client = RedisClient.create({
...options?.clientOptions,
socket: {
...options?.clientOptions?.socket,
port: (await dockerPromise).port
}
});
if (options.disableClientSetup) {
return fn(client);
}
await client.connect();
try {
await client.flushAll();
await fn(client);
} finally {
if (client.isOpen) {
await client.flushAll();
await client.disconnect();
}
}
});
dockerPromise = spawnRedisServer(dockerImage, options.serverArguments);
return dockerPromise;
});
}
static async #clusterFlushAll<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(cluster: RedisClusterType<M, F, S>): Promise<unknown> {
return Promise.all(
cluster.masters.map(async ({ client }) => {
if (client) {
await (await client).flushAll();
}
})
);
}
it(title, async function () {
if (!dockerPromise) return this.skip();
testWithCluster<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(
title: string,
fn: (cluster: RedisClusterType<M, F, S>) => unknown,
options: ClusterTestOptions<M, F, S>
): void {
let dockersPromise: ReturnType<typeof spawnRedisCluster>;
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
const dockerImage = this.#DOCKER_IMAGE;
before(function () {
this.timeout(30000);
dockersPromise = spawnRedisCluster({
...dockerImage,
numberOfMasters: options?.numberOfMasters,
numberOfReplicas: options?.numberOfReplicas
}, options.serverArguments);
return dockersPromise;
});
const pool = createClientPool({
...options.clientOptions,
socket: {
...options.clientOptions?.socket,
// TODO
// @ts-ignore
port: (await dockerPromise).port
}
}, options.poolOptions);
it(title, async function () {
if (!dockersPromise) return this.skip();
await pool.connect();
const dockers = await dockersPromise,
cluster = RedisCluster.create({
rootNodes: dockers.map(({ port }) => ({
socket: {
port
}
})),
minimizeConnections: true,
...options.clusterConfiguration
});
try {
await pool.flushAll();
await fn(pool);
} finally {
await pool.flushAll();
pool.destroy();
}
});
}
await cluster.connect();
static async #clusterFlushAll<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
// POLICIES extends CommandPolicies
>(cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>): Promise<unknown> {
return Promise.all(
cluster.masters.map(async master => {
if (master.client) {
await (await cluster.nodeClient(master)).flushAll();
}
})
);
}
try {
await TestUtils.#clusterFlushAll(cluster);
await fn(cluster);
} finally {
await TestUtils.#clusterFlushAll(cluster);
await cluster.disconnect();
}
});
testWithCluster<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
// POLICIES extends CommandPolicies = {}
>(
title: string,
fn: (cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) => unknown,
options: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>
): void {
let dockersPromise: ReturnType<typeof spawnRedisCluster>;
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
const dockerImage = this.#DOCKER_IMAGE;
before(function () {
this.timeout(30000);
dockersPromise = spawnRedisCluster({
...dockerImage,
numberOfMasters: options.numberOfMasters,
numberOfReplicas: options.numberOfReplicas
}, options.serverArguments);
return dockersPromise;
});
}
it(title, async function () {
if (!dockersPromise) return this.skip();
const dockers = await dockersPromise,
cluster = createCluster({
rootNodes: dockers.map(({ port }) => ({
socket: {
port
}
})),
minimizeConnections: true,
...options.clusterConfiguration
});
await cluster.connect();
try {
await TestUtils.#clusterFlushAll(cluster);
await fn(cluster);
} finally {
await TestUtils.#clusterFlushAll(cluster);
cluster.destroy();
}
});
}
testAll<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
// POLICIES extends CommandPolicies = {}
>(
title: string,
fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING> | RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) => unknown,
options: AllTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>
) {
this.testWithClient(`client.${title}`, fn, options.client);
this.testWithCluster(`cluster.${title}`, fn, options.cluster);
}
}