diff --git a/package-lock.json b/package-lock.json index a9f846e7a8..6d2460ed19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8520,7 +8520,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8529,12 +8529,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/client": { "name": "@redis/client", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -8550,7 +8550,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -8569,7 +8569,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/entraid/node_modules/@types/node": { @@ -8606,7 +8606,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8615,18 +8615,18 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/redis": { - "version": "5.0.1", + "version": "5.1.1", "license": "MIT", "dependencies": { - "@redis/bloom": "5.1.0", - "@redis/client": "5.1.0", - "@redis/json": "5.1.0", - "@redis/search": "5.1.0", - "@redis/time-series": "5.1.0" + "@redis/bloom": "5.1.1", + "@redis/client": "5.1.1", + "@redis/json": "5.1.1", + "@redis/search": "5.1.1", + "@redis/time-series": "5.1.1" }, "engines": { "node": ">= 18" @@ -8634,7 +8634,7 @@ }, "packages/search": { "name": "@redis/search", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8643,7 +8643,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } }, "packages/test-utils": { @@ -8712,7 +8712,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8721,7 +8721,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" } } } diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 3c8e37c5b7..547e5ee64e 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index cc052dd5b5..4f752210db 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -89,8 +89,8 @@ describe('Client', () => { && expected?.credentialsProvider?.type === 'async-credentials-provider') { // Compare the actual output of the credentials functions - const resultCreds = await result.credentialsProvider.credentials(); - const expectedCreds = await expected.credentialsProvider.credentials(); + const resultCreds = await result.credentialsProvider?.credentials(); + const expectedCreds = await expected.credentialsProvider?.credentials(); assert.deepEqual(resultCreds, expectedCreds); } else { assert.fail('Credentials provider type mismatch'); diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 8d98aa8ed2..a446ad8e75 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -4,7 +4,7 @@ import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsP import RedisCommandsQueue, { CommandOptions } from './commands-queue'; import { EventEmitter } from 'node:events'; import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; -import { ClientClosedError, ClientOfflineError, DisconnectsClientError, SimpleError, WatchError } from '../errors'; +import { ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchError } from '../errors'; import { URL } from 'node:url'; import { TcpSocketConnectOpts } from 'node:net'; import { PUBSUB_TYPE, PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub'; @@ -626,14 +626,10 @@ export default class RedisClient< if (!this.#options?.disableClientInfo) { commands.push({ cmd: ['CLIENT', 'SETINFO', 'LIB-VER', version], - errorHandler: (err: Error) => { - // Only throw if not a SimpleError - unknown subcommand - // Client libraries are expected to ignore failures - // of type SimpleError - unknown subcommand, which are - // expected from older servers ( < v7 ) - if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) { - throw err; - } + errorHandler: () => { + // Client libraries are expected to pipeline this command + // after authentication on all connections and ignore failures + // since they could be connected to an older version that doesn't support them. } }); @@ -646,14 +642,10 @@ export default class RedisClient< ? `node-redis(${this.#options.clientInfoTag})` : 'node-redis' ], - errorHandler: (err: Error) => { - // Only throw if not a SimpleError - unknown subcommand - // Client libraries are expected to ignore failures - // of type SimpleError - unknown subcommand, which are - // expected from older servers ( < v7 ) - if (!(err instanceof SimpleError) || !err.isUnknownSubcommand()) { - throw err; - } + errorHandler: () => { + // Client libraries are expected to pipeline this command + // after authentication on all connections and ignore failures + // since they could be connected to an older version that doesn't support them. } }); } diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index f738562926..c2c251810e 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -188,7 +188,7 @@ export default class RedisCluster< const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, command.IS_READ_ONLY, this._commandOptions, @@ -204,7 +204,7 @@ export default class RedisCluster< const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, command.IS_READ_ONLY, this._self._commandOptions, @@ -222,7 +222,7 @@ export default class RedisCluster< parser.push(...prefix); fn.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, fn.IS_READ_ONLY, this._self._commandOptions, @@ -240,7 +240,7 @@ export default class RedisCluster< parser.push(...prefix); script.parseCommand(parser, ...args); - return this._self.#execute( + return this._self._execute( parser.firstKey, script.IS_READ_ONLY, this._commandOptions, @@ -293,9 +293,9 @@ export default class RedisCluster< return RedisCluster.factory(options)(options); } - readonly #options: RedisClusterOptions; + readonly _options: RedisClusterOptions; - readonly #slots: RedisClusterSlots; + readonly _slots: RedisClusterSlots; private _self = this; private _commandOptions?: ClusterCommandOptions; @@ -305,11 +305,11 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica). */ get slots() { - return this._self.#slots.slots; + return this._self._slots.slots; } get clientSideCache() { - return this._self.#slots.clientSideCache; + return this._self._slots.clientSideCache; } /** @@ -317,7 +317,7 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific master node. */ get masters() { - return this._self.#slots.masters; + return this._self._slots.masters; } /** @@ -325,7 +325,7 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific replica node. */ get replicas() { - return this._self.#slots.replicas; + return this._self._slots.replicas; } /** @@ -333,25 +333,25 @@ export default class RedisCluster< * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica). */ get nodeByAddress() { - return this._self.#slots.nodeByAddress; + return this._self._slots.nodeByAddress; } /** * The current pub/sub node. */ get pubSubNode() { - return this._self.#slots.pubSubNode; + return this._self._slots.pubSubNode; } get isOpen() { - return this._self.#slots.isOpen; + return this._self._slots.isOpen; } constructor(options: RedisClusterOptions) { super(); - this.#options = options; - this.#slots = new RedisClusterSlots(options, this.emit.bind(this)); + this._options = options; + this._slots = new RedisClusterSlots(options, this.emit.bind(this)); if (options?.commandOptions) { this._commandOptions = options.commandOptions; @@ -366,14 +366,14 @@ export default class RedisCluster< _TYPE_MAPPING extends TypeMapping = TYPE_MAPPING >(overrides?: Partial>) { return new (Object.getPrototypeOf(this).constructor)({ - ...this._self.#options, + ...this._self._options, commandOptions: this._commandOptions, ...overrides }) as RedisClusterType<_M, _F, _S, _RESP, _TYPE_MAPPING>; } async connect() { - await this._self.#slots.connect(); + await this._self._slots.connect(); return this as unknown as RedisClusterType; } @@ -429,7 +429,7 @@ export default class RedisCluster< // return this._commandOptionsProxy('policies', policies); // } - #handleAsk( + _handleAsk( fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ) { return async (client: RedisClientType, options?: ClusterCommandOptions) => { @@ -450,14 +450,14 @@ export default class RedisCluster< }; } - async #execute( + async _execute( firstKey: RedisArgument | undefined, isReadonly: boolean | undefined, options: ClusterCommandOptions | undefined, fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ): Promise { - const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16; - let client = await this.#slots.getClient(firstKey, isReadonly); + const maxCommandRedirections = this._options.maxCommandRedirections ?? 16; + let client = await this._slots.getClient(firstKey, isReadonly); let i = 0; let myFn = fn; @@ -475,10 +475,10 @@ export default class RedisCluster< if (err.message.startsWith('ASK')) { const address = err.message.substring(err.message.lastIndexOf(' ') + 1); - let redirectTo = await this.#slots.getMasterByAddress(address); + let redirectTo = await this._slots.getMasterByAddress(address); if (!redirectTo) { - await this.#slots.rediscover(client); - redirectTo = await this.#slots.getMasterByAddress(address); + await this._slots.rediscover(client); + redirectTo = await this._slots.getMasterByAddress(address); } if (!redirectTo) { @@ -486,13 +486,13 @@ export default class RedisCluster< } client = redirectTo; - myFn = this.#handleAsk(fn); + myFn = this._handleAsk(fn); continue; } if (err.message.startsWith('MOVED')) { - await this.#slots.rediscover(client); - client = await this.#slots.getClient(firstKey, isReadonly); + await this._slots.rediscover(client); + client = await this._slots.getClient(firstKey, isReadonly); continue; } @@ -508,7 +508,7 @@ export default class RedisCluster< options?: ClusterCommandOptions, // defaultPolicies?: CommandPolicies ): Promise { - return this._self.#execute( + return this._self._execute( firstKey, isReadonly, options, @@ -520,11 +520,11 @@ export default class RedisCluster< type Multi = new (...args: ConstructorParameters) => RedisClusterMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>; return new ((this as any).Multi as Multi)( async (firstKey, isReadonly, commands) => { - const client = await this._self.#slots.getClient(firstKey, isReadonly); + const client = await this._self._slots.getClient(firstKey, isReadonly); return client._executeMulti(commands); }, async (firstKey, isReadonly, commands) => { - const client = await this._self.#slots.getClient(firstKey, isReadonly); + const client = await this._self._slots.getClient(firstKey, isReadonly); return client._executePipeline(commands); }, routing, @@ -539,7 +539,7 @@ export default class RedisCluster< listener: PubSubListener, bufferMode?: T ) { - return (await this._self.#slots.getPubSubClient()) + return (await this._self._slots.getPubSubClient()) .SUBSCRIBE(channels, listener, bufferMode); } @@ -550,7 +550,7 @@ export default class RedisCluster< listener?: PubSubListener, bufferMode?: T ) { - return this._self.#slots.executeUnsubscribeCommand(client => + return this._self._slots.executeUnsubscribeCommand(client => client.UNSUBSCRIBE(channels, listener, bufferMode) ); } @@ -562,7 +562,7 @@ export default class RedisCluster< listener: PubSubListener, bufferMode?: T ) { - return (await this._self.#slots.getPubSubClient()) + return (await this._self._slots.getPubSubClient()) .PSUBSCRIBE(patterns, listener, bufferMode); } @@ -573,7 +573,7 @@ export default class RedisCluster< listener?: PubSubListener, bufferMode?: T ) { - return this._self.#slots.executeUnsubscribeCommand(client => + return this._self._slots.executeUnsubscribeCommand(client => client.PUNSUBSCRIBE(patterns, listener, bufferMode) ); } @@ -585,9 +585,9 @@ export default class RedisCluster< listener: PubSubListener, bufferMode?: T ) { - const maxCommandRedirections = this._self.#options.maxCommandRedirections ?? 16, + const maxCommandRedirections = this._self._options.maxCommandRedirections ?? 16, firstChannel = Array.isArray(channels) ? channels[0] : channels; - let client = await this._self.#slots.getShardedPubSubClient(firstChannel); + let client = await this._self._slots.getShardedPubSubClient(firstChannel); for (let i = 0; ; i++) { try { return await client.SSUBSCRIBE(channels, listener, bufferMode); @@ -597,8 +597,8 @@ export default class RedisCluster< } if (err.message.startsWith('MOVED')) { - await this._self.#slots.rediscover(client); - client = await this._self.#slots.getShardedPubSubClient(firstChannel); + await this._self._slots.rediscover(client); + client = await this._self._slots.getShardedPubSubClient(firstChannel); continue; } @@ -614,7 +614,7 @@ export default class RedisCluster< listener?: PubSubListener, bufferMode?: T ) { - return this._self.#slots.executeShardedUnsubscribeCommand( + return this._self._slots.executeShardedUnsubscribeCommand( Array.isArray(channels) ? channels[0] : channels, client => client.SUNSUBSCRIBE(channels, listener, bufferMode) ); @@ -626,28 +626,28 @@ export default class RedisCluster< * @deprecated Use `close` instead. */ quit() { - return this._self.#slots.quit(); + return this._self._slots.quit(); } /** * @deprecated Use `destroy` instead. */ disconnect() { - return this._self.#slots.disconnect(); + return this._self._slots.disconnect(); } close() { - this._self.#slots.clientSideCache?.onPoolClose(); - return this._self.#slots.close(); + this._self._slots.clientSideCache?.onPoolClose(); + return this._self._slots.close(); } destroy() { - this._self.#slots.clientSideCache?.onPoolClose(); - return this._self.#slots.destroy(); + this._self._slots.clientSideCache?.onPoolClose(); + return this._self._slots.destroy(); } nodeClient(node: ShardNode) { - return this._self.#slots.nodeClient(node); + return this._self._slots.nodeClient(node); } /** @@ -655,7 +655,7 @@ export default class RedisCluster< * Userful for running "forward" commands (like PUBLISH) on a random node. */ getRandomNode() { - return this._self.#slots.getRandomNode(); + return this._self._slots.getRandomNode(); } /** @@ -663,7 +663,7 @@ export default class RedisCluster< * Useful for running readonly commands on a slot. */ getSlotRandomNode(slot: number) { - return this._self.#slots.getSlotRandomNode(slot); + return this._self._slots.getSlotRandomNode(slot); } /** diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts index 4f05f62ac4..db37ec1a9b 100644 --- a/packages/client/lib/errors.ts +++ b/packages/client/lib/errors.ts @@ -70,11 +70,7 @@ export class ErrorReply extends Error { } } -export class SimpleError extends ErrorReply { - isUnknownSubcommand(): boolean { - return this.message.toLowerCase().indexOf('err unknown subcommand') !== -1; - } -} +export class SimpleError extends ErrorReply {} export class BlobError extends ErrorReply {} diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index a2e52b774b..a9bdc8cc95 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -197,7 +197,6 @@ describe(`test with scripts`, () => { }, GLOBAL.SENTINEL.WITH_SCRIPT) }); - describe(`test with functions`, () => { testUtils.testWithClientSentinel('with function', async sentinel => { await sentinel.functionLoad( @@ -377,12 +376,9 @@ describe(`test with masterPoolSize 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) { + // wait a bit to ensure that sentinels are seeing eachother + await setTimeout(2000) let checkedMaster = false; let checkedReplicas = false; 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 frame = new SentinelFramework(config); let tracer = new Array(); @@ -439,42 +435,30 @@ describe.skip('legacy tests', () => { let longestTestDelta = 0; let last: number; - before(async function () { - this.timeout(15000); - - last = Date.now(); - - function deltaMeasurer() { - const delta = Date.now() - last; - if (delta > longestDelta) { - longestDelta = delta; - } - if (delta > longestTestDelta) { - longestTestDelta = delta; - } - if (!stopMeasuringBlocking) { - last = Date.now(); - setImmediate(deltaMeasurer); - } - } - setImmediate(deltaMeasurer); - await frame.spawnRedisSentinel(); - }); - - after(async function () { - this.timeout(15000); - - stopMeasuringBlocking = true; - - await frame.cleanup(); - }) describe('Sentinel Client', function () { let sentinel: RedisSentinelType | undefined; beforeEach(async function () { - this.timeout(0); - + this.timeout(15000); + + last = Date.now(); + + function deltaMeasurer() { + const delta = Date.now() - last; + if (delta > longestDelta) { + longestDelta = delta; + } + if (delta > longestTestDelta) { + longestTestDelta = delta; + } + if (!stopMeasuringBlocking) { + last = Date.now(); + setImmediate(deltaMeasurer); + } + } + setImmediate(deltaMeasurer); + await frame.spawnRedisSentinel(); await frame.getAllRunning(); await steadyState(frame); longestTestDelta = 0; @@ -522,6 +506,10 @@ describe.skip('legacy tests', () => { await sentinel.destroy(); sentinel = undefined; } + + stopMeasuringBlocking = true; + + await frame.cleanup(); }) it('use', async function () { @@ -863,7 +851,6 @@ describe.skip('legacy tests', () => { it('shutdown sentinel node', async function () { this.timeout(60000); - sentinel = frame.getSentinelClient(); sentinel.setTracer(tracer); sentinel.on("error", () => { }); @@ -1020,7 +1007,7 @@ describe.skip('legacy tests', () => { this.timeout(30000); 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.set('x', 1); diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 86bc5b3178..60c1a59689 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -4,12 +4,13 @@ import { once } from 'node:events'; import { promisify } from 'node:util'; import { exec } from 'node:child_process'; import { RedisSentinelOptions, RedisSentinelType } from './types'; -import RedisClient from '../client'; +import RedisClient, {RedisClientType} from '../client'; import RedisSentinel from '.'; import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; const execAsync = promisify(exec); import RedisSentinelModule from './module' - +import TestUtils from '@redis/test-utils'; +import { DEBUG_MODE_ARGS } from '../test-utils' interface ErrorWithCode extends Error { code: string; } @@ -125,7 +126,6 @@ export interface RedisSentinelConfig { sentinelServerArgument?: Array sentinelName: string; - sentinelQuorum?: number; password?: string; } @@ -151,6 +151,7 @@ export interface SentinelController { } export class SentinelFramework extends DockerBase { + #testUtils: TestUtils; #nodeList: Awaited> = []; /* port -> docker info/client */ #nodeMap: Map>>>; @@ -170,7 +171,11 @@ export class SentinelFramework extends DockerBase { super(); this.config = config; - + this.#testUtils = TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M05-pre' + }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); } @@ -190,7 +195,7 @@ export class SentinelFramework extends DockerBase { const options: RedisSentinelOptions = { ...opts, 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 } @@ -218,11 +223,11 @@ export class SentinelFramework extends DockerBase { throw new Error("inconsistent state with partial setup"); } - this.#nodeList = await this.spawnRedisSentinelNodes(); - this.#nodeList.map((value) => this.#nodeMap.set(value.docker.port.toString(), value)); + this.#nodeList = await this.spawnRedisSentinelNodes(2); + this.#nodeList.map((value) => this.#nodeMap.set(value.port.toString(), value)); - this.#sentinelList = await this.spawnRedisSentinelSentinels(); - this.#sentinelList.map((value) => this.#sentinelMap.set(value.docker.port.toString(), value)); + this.#sentinelList = await this.spawnRedisSentinelSentinels(this.#nodeList[0].port, 3) + this.#sentinelList.map((value) => this.#sentinelMap.set(value.port.toString(), value)); this.#spawned = true; } @@ -234,11 +239,8 @@ export class SentinelFramework extends DockerBase { return Promise.all( [...this.#nodeMap!.values(), ...this.#sentinelMap!.values()].map( - async ({ docker, client }) => { - if (client.isOpen) { - client.destroy(); - } - this.dockerRemove(docker.dockerId); + async ({ dockerId }) => { + this.dockerRemove(dockerId); } ) ).finally(async () => { @@ -248,112 +250,33 @@ export class SentinelFramework extends DockerBase { }); } - protected async spawnRedisSentinelNodeDocker() { - const imageInfo: RedisServerDockerConfig = this.config.nodeDockerConfig ?? { image: "redis/redis-stack-server", version: "latest" }; - const serverArguments: Array = 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 client = await RedisClient.create({ - password: this.config.password, - socket: { - port: docker.port - } - }).on("error", () => { }).connect(); - - return { - docker, - client - }; - } - - protected async spawnRedisSentinelNodes() { - const master = await this.spawnRedisSentinelNodeDocker(); + protected async spawnRedisSentinelNodes(replicasCount: number) { + const master = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) + + const replicas: Array = [] + for (let i = 0; i < replicasCount; i++) { + const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) + replicas.push(replica) - const promises: Array> = []; + const client = RedisClient.create({ + socket: { + port: replica.port + } + }) - 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 [ master, - ...await Promise.all(promises) - ]; - } - - protected async spawnRedisSentinelSentinelDocker() { - const imageInfo: RedisServerDockerConfig = this.config.sentinelDockerConfig ?? { image: "redis", version: "latest" } - let serverArguments: Array; - 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 - }; + ...replicas + ] } - protected async spawnRedisSentinelSentinels() { - const quorum = this.config.sentinelQuorum?.toString() ?? "2"; - const node = this.#nodeList[0]; - - const promises: Array> = []; - - 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() { @@ -384,90 +307,71 @@ export class SentinelFramework extends DockerBase { } async addSentinel() { - const quorum = this.config.sentinelQuorum?.toString() ?? "2"; - const node = this.#nodeList[0]; - const sentinel = await this.spawnRedisSentinelSentinelDocker(); - - 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); + const nodes = await this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, this.#nodeList[0].port, this.config.sentinelName, 1) + this.#sentinelList.push(nodes[0]); + this.#sentinelMap.set(nodes[0].port.toString(), nodes[0]); } async addNode() { 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) { - await newNode.client.configSet({'masterauth': this.config.password}) - } - await newNode.client.replicaOf('127.0.0.1', masterPort); + const client = RedisClient.create({ + socket: { + port: replica.port + } + }) + + await client.connect(); + await client.replicaOf("127.0.0.1", masterPort); + await client.close(); + - this.#nodeList.push(newNode); - this.#nodeMap.set(newNode.docker.port.toString(), newNode); + this.#nodeList.push(replica); + this.#nodeMap.set(replica.port.toString(), replica); } async getMaster(tracer?: Array): Promise { - for (const sentinel of this.#sentinelMap!.values()) { - let info; - - try { - if (!sentinel.client.isReady) { - continue; - } - - info = await sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); - if (tracer) { - tracer.push('getMaster: master data returned from sentinel'); - tracer.push(JSON.stringify(info, undefined, '\t')) - } - } catch (err) { - console.log("getMaster: sentinelMaster call failed: " + err); - continue; - } - - const master = this.#nodeMap.get(info.port); - if (master === undefined) { - throw new Error(`couldn't find master node for ${info.port}`); - } - - if (tracer) { - tracer.push(`getMaster: master port is either ${info.port} or ${master.docker.port}`); - } + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const info = await client.sentinel.sentinelMaster(this.config.sentinelName); + await client.close() - if (!master.client.isOpen) { - throw new Error(`Sentinel's expected master node (${info.port}) is now down`); - } + const master = this.#nodeMap.get(info.port); + if (master === undefined) { + throw new Error(`couldn't find master node for ${info.port}`); + } - return info.port; + if (tracer) { + tracer.push(`getMaster: master port is either ${info.port} or ${master.port}`); } - throw new Error("Couldn't get master"); + return info.port; } async getMasterPort(tracer?: Array): Promise { const data = await this.getMaster(tracer) - return this.#nodeMap.get(data!)!.docker.port; + return this.#nodeMap.get(data!)!.port; } 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 { const masterPort = await this.getMasterPort(); while (true) { const node = this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)]; - if (node.docker.port != masterPort) { - return node.docker.port.toString(); + if (node.port != masterPort) { + return node.port.toString(); } } } @@ -479,11 +383,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - if (node.client.isOpen) { - node.client.destroy(); - } - - return await this.dockerStop(node.docker.dockerId); + return await this.dockerStop(node.dockerId); } async restartNode(id: string) { @@ -492,15 +392,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - await this.dockerStart(node.docker.dockerId); - if (!node.client.isOpen) { - node.client = await RedisClient.create({ - password: this.config.password, - socket: { - port: node.docker.port - } - }).on("error", () => { }).connect(); - } + await this.dockerStart(node.dockerId); } async stopSentinel(id: string) { @@ -509,11 +401,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - if (sentinel.client.isOpen) { - sentinel.client.destroy(); - } - - return await this.dockerStop(sentinel.docker.dockerId); + return await this.dockerStop(sentinel.dockerId); } async restartSentinel(id: string) { @@ -522,16 +410,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - await this.dockerStart(sentinel.docker.dockerId); - if (!sentinel.client.isOpen) { - sentinel.client = await RedisClient.create({ - modules: RedisSentinelModule, - password: this.config.password, - socket: { - port: sentinel.docker.port - } - }).on("error", () => { }).connect(); - } + await this.dockerStart(sentinel.dockerId); } getNodePort(id: string) { @@ -540,13 +419,13 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - return node.docker.port; + return node.port; } getAllNodesPort() { let ports: Array = []; for (const node of this.#nodeList) { - ports.push(node.docker.port); + ports.push(node.port); } return ports @@ -555,7 +434,7 @@ export class SentinelFramework extends DockerBase { getAllDockerIds() { let ids = new Map(); for (const node of this.#nodeList) { - ids.set(node.docker.dockerId, node.docker.port); + ids.set(node.dockerId, node.port); } return ids; @@ -567,43 +446,67 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - return sentinel.docker.port; + return sentinel.port; } getAllSentinelsPort() { let ports: Array = []; for (const sentinel of this.#sentinelList) { - ports.push(sentinel.docker.port); + ports.push(sentinel.port); } return ports } getSetinel(i: number): string { - return this.#sentinelList[i].docker.port.toString(); + return this.#sentinelList[i].port.toString(); } - sentinelSentinels() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelSentinels(this.config.sentinelName); - } - } + async sentinelSentinels() { + const client = RedisClient.create({ + name: 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() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); - } - } + async sentinelMaster() { + const client = RedisClient.create({ + name: 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() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelReplicas(this.config.sentinelName); - } - } + async sentinelReplicas() { + const client = RedisClient.create({ + name: 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 } } \ No newline at end of file diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 63f43ba5e6..19bbafc66e 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -14,7 +14,7 @@ const utils = TestUtils.createFromConfig({ export default utils; -const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? +export const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? ['--enable-debug-command', 'yes'] : []; diff --git a/packages/client/package.json b/packages/client/package.json index dd41dc53e0..a6d44451a6 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/entraid/package.json b/packages/entraid/package.json index ba44351b3b..43f26d1e03 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -21,7 +21,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@types/express": "^4.17.21", diff --git a/packages/json/lib/commands/helpers.ts b/packages/json/lib/commands/helpers.ts index 26ff12f683..99579ce81c 100644 --- a/packages/json/lib/commands/helpers.ts +++ b/packages/json/lib/commands/helpers.ts @@ -2,7 +2,6 @@ import { isNullReply } from "@redis/client/dist/lib/commands/generic-transformer import { BlobStringReply, NullReply, UnwrapReply } from "@redis/client/dist/lib/RESP/types"; export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { - console.log('transformRedisJsonNullReply', json) return isNullReply(json) ? json : transformRedisJsonReply(json); } @@ -17,6 +16,5 @@ export function transformRedisJsonArgument(json: RedisJSON): string { export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { const res = JSON.parse((json as unknown as UnwrapReply).toString()); - console.log('transformRedisJsonReply', json, res) return res; } diff --git a/packages/json/package.json b/packages/json/package.json index fd0b3c768f..3b473dfce0 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/redis/README.md b/packages/redis/README.md index d04a19b0d7..ab6b4707e6 100644 --- a/packages/redis/README.md +++ b/packages/redis/README.md @@ -45,13 +45,13 @@ npm install redis | Name | Description | | ---------------------------------------------- | ------------------------------------------------------------------------------------------- | -| [`redis`](../redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | -| [`@redis/client`](../client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | -| [`@redis/bloom`](../bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | -| [`@redis/json`](../json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | -| [`@redis/search`](../search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | -| [`@redis/time-series`](../time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | -| [`@redis/entraid`](../entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | +| [`redis`](https://github.com/redis/node-redis/tree/master/packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | +| [`@redis/client`](https://github.com/redis/node-redis/tree/master/packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | +| [`@redis/bloom`](https://github.com/redis/node-redis/tree/master/packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | +| [`@redis/json`](https://github.com/redis/node-redis/tree/master/packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | +| [`@redis/search`](https://github.com/redis/node-redis/tree/master/packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | +| [`@redis/time-series`](https://github.com/redis/node-redis/tree/master/packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | +| [`@redis/entraid`](https://github.com/redis/node-redis/tree/master/packages/entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | > Looking for a high-level library to handle object mapping? > See [redis-om-node](https://github.com/redis/redis-om-node)! @@ -83,7 +83,7 @@ createClient({ ``` You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in -the [client configuration guide](../../docs/client-configuration.md). +the [client configuration guide](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md). To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean. `client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it @@ -188,7 +188,7 @@ await pool.ping(); ### Pub/Sub -See the [Pub/Sub overview](../../docs/pub-sub.md). +See the [Pub/Sub overview](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md). ### Scan Iterator @@ -250,7 +250,7 @@ const client = createClient({ }); ``` -See the [V5 documentation](../../docs/v5.md#client-side-caching) for more details and advanced usage. +See the [V5 documentation](https://github.com/redis/node-redis/blob/master/docs/v5.md#client-side-caching) for more details and advanced usage. ### Auto-Pipelining @@ -274,11 +274,11 @@ await Promise.all([ ### Programmability -See the [Programmability overview](../../docs/programmability.md). +See the [Programmability overview](https://github.com/redis/node-redis/blob/master/docs/programmability.md). ### Clustering -Check out the [Clustering Guide](../../docs/clustering.md) when using Node Redis to connect to a Redis Cluster. +Check out the [Clustering Guide](https://github.com/redis/node-redis/blob/master/docs/clustering.md) when using Node Redis to connect to a Redis Cluster. ### Events @@ -291,12 +291,12 @@ The Node Redis client class is an Nodejs EventEmitter and it emits an event each | `end` | Connection has been closed (via `.disconnect()`) | _No arguments_ | | `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` | | `reconnecting` | Client is trying to reconnect to the server | _No arguments_ | -| `sharded-channel-moved` | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | +| `sharded-channel-moved` | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) | See [here](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md#sharded-channel-moved-event) | > :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and > an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details. -> The client will not emit [any other events](../../docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. +> The client will not emit [any other events](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. ## Supported Redis versions @@ -313,13 +313,13 @@ Node Redis is supported with the following versions of Redis: ## Migration -- [From V3 to V4](../../docs/v3-to-v4.md) -- [From V4 to V5](../../docs/v4-to-v5.md) -- [V5](../../docs/v5.md) +- [From V3 to V4](https://github.com/redis/node-redis/blob/master/docs/v3-to-v4.md) +- [From V4 to V5](https://github.com/redis/node-redis/blob/master/docs/v4-to-v5.md) +- [V5](https://github.com/redis/node-redis/blob/master/docs/v5.md) ## Contributing -If you'd like to contribute, check out the [contributing guide](../../CONTRIBUTING.md). +If you'd like to contribute, check out the [contributing guide](https://github.com/redis/node-redis/blob/master/CONTRIBUTING.md). Thank you to all the people who already contributed to Node Redis! @@ -327,4 +327,4 @@ Thank you to all the people who already contributed to Node Redis! ## License -This repository is licensed under the "MIT" license. See [LICENSE](../../LICENSE). +This repository is licensed under the "MIT" license. See [LICENSE](https://github.com/redis/node-redis/blob/master/LICENSE). diff --git a/packages/redis/package.json b/packages/redis/package.json index b6c3f409a6..05fb70cdd1 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -10,11 +10,11 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { - "@redis/bloom": "5.1.0", - "@redis/client": "5.1.0", - "@redis/json": "5.1.0", - "@redis/search": "5.1.0", - "@redis/time-series": "5.1.0" + "@redis/bloom": "5.1.1", + "@redis/client": "5.1.1", + "@redis/json": "5.1.1", + "@redis/search": "5.1.1", + "@redis/time-series": "5.1.1" }, "engines": { "node": ">= 18" diff --git a/packages/search/package.json b/packages/search/package.json index ac56b5ff5d..7cb73dfc0a 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index 3814a80923..47257964f6 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -62,7 +62,7 @@ export interface RedisServerDocker { dockerId: string; } -async function spawnRedisServerDocker( +export async function spawnRedisServerDocker( options: RedisServerDockerOptions, serverArguments: Array): Promise { let port; if (options.mode == "sentinel") { @@ -374,35 +374,16 @@ export async function spawnRedisSentinel( 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); - })()); + sentinelPromises.push( + spawnSentinelNode( + dockerConfigs, + serverArguments, + master.port, + "mymaster", + path.join(tmpDir, i.toString()), + password, + ), + ) } const sentinelNodes = await Promise.all(sentinelPromises); @@ -424,3 +405,43 @@ after(() => { }) ); }); + + +export async function spawnSentinelNode( + dockerConfigs: RedisServerDockerOptions, + serverArguments: Array, + 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, + ); +} \ No newline at end of file diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index d92c5c9e3d..a41f970e0c 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -19,10 +19,13 @@ import { RedisClusterType } from '@redis/client/index'; 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 { hideBin } from 'yargs/helpers'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; interface TestUtilsConfig { /** @@ -395,19 +398,19 @@ export default class TestUtils { S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} ->( - range: ([minVersion: Array, maxVersion: Array] | [minVersion: Array, 'LATEST']), - title: string, - fn: (sentinel: RedisSentinelType) => unknown, - options: SentinelTestOptions -): 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'}`) + >( + range: ([minVersion: Array, maxVersion: Array] | [minVersion: Array, 'LATEST']), + title: string, + fn: (sentinel: RedisSentinelType) => unknown, + options: SentinelTestOptions + ): 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< M extends RedisModules = {}, @@ -541,4 +544,46 @@ export default class TestUtils { this.testWithClient(`client.${title}`, fn, options.client); 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 + ): Promise { + 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, + masterPort: number, + sentinelName: string, + count: number + ): Promise> { + const sentinels: Array = []; + 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 + } } diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 466c53df22..888f0f317e 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.1.0" + "@redis/client": "^5.1.1" }, "devDependencies": { "@redis/test-utils": "*"