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

client pool

This commit is contained in:
Leibale
2023-09-04 17:26:48 -04:00
parent c7a03acfd3
commit 3895eb5926
15 changed files with 565 additions and 352 deletions

View File

@@ -19,7 +19,7 @@ redis.register_function {
Here is the same example, but in a format that can be pasted into the `redis-cli`. Here is the same example, but in a format that can be pasted into the `redis-cli`.
``` ```
FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name=\"add\", callback=function(keys, args) return redis.call('GET', keys[1])+args[1] end, flags={\"no-writes\"}}" FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name='add', callback=function(keys, args) return redis.call('GET', keys[1])+args[1] end, flags={'no-writes'}}"
``` ```
Load the prior redis function on the _redis server_ before running the example below. Load the prior redis function on the _redis server_ before running the example below.

View File

@@ -4,11 +4,15 @@ export { VerbatimString } from './lib/RESP/verbatim-string';
export { defineScript } from './lib/lua-script'; export { defineScript } from './lib/lua-script';
// export * from './lib/errors'; // export * from './lib/errors';
import RedisClient, { RedisClientType, RedisClientOptions } from './lib/client'; import RedisClient, { RedisClientOptions, RedisClientType } from './lib/client';
export { RedisClientType, RedisClientOptions }; export { RedisClientOptions, RedisClientType };
export const createClient = RedisClient.create; export const createClient = RedisClient.create;
import RedisCluster, { RedisClusterType, RedisClusterOptions } from './lib/cluster'; import { RedisClientPool, RedisPoolOptions, RedisClientPoolType } from './lib/client/pool';
export { RedisClientPoolType, RedisPoolOptions };
export const createClientPool = RedisClientPool.create;
import RedisCluster, { RedisClusterOptions, RedisClusterType } from './lib/cluster';
export { RedisClusterType, RedisClusterOptions }; export { RedisClusterType, RedisClusterOptions };
export const createCluster = RedisCluster.create; export const createCluster = RedisCluster.create;

View File

@@ -111,7 +111,10 @@ export class Decoder {
case RESP_TYPES.NUMBER: case RESP_TYPES.NUMBER:
return this._handleDecodedValue( return this._handleDecodedValue(
this._config.onReply, this._config.onReply,
this._decodeNumber(chunk) this._decodeNumber(
this._config.getTypeMapping()[RESP_TYPES.NUMBER],
chunk
)
); );
case RESP_TYPES.BIG_NUMBER: case RESP_TYPES.BIG_NUMBER:
@@ -226,7 +229,11 @@ export class Decoder {
return boolean; return boolean;
} }
private _decodeNumber(chunk) { private _decodeNumber(type, chunk) {
if (type === String) {
return this._decodeSimpleString(String, chunk);
}
switch (chunk[this._cursor]) { switch (chunk[this._cursor]) {
case ASCII['+']: case ASCII['+']:
return this._maybeDecodeNumberValue(false, chunk); return this._maybeDecodeNumberValue(false, chunk);
@@ -675,7 +682,7 @@ export class Decoder {
return this._decodeBoolean(chunk); return this._decodeBoolean(chunk);
case RESP_TYPES.NUMBER: case RESP_TYPES.NUMBER:
return this._decodeNumber(chunk); return this._decodeNumber(typeMapping[RESP_TYPES.NUMBER], chunk);
case RESP_TYPES.BIG_NUMBER: case RESP_TYPES.BIG_NUMBER:
return this._decodeBigNumber(typeMapping[RESP_TYPES.BIG_NUMBER], chunk); return this._decodeBigNumber(typeMapping[RESP_TYPES.BIG_NUMBER], chunk);

View File

@@ -11,7 +11,7 @@ export interface CommandOptions<T = TypeMapping> {
asap?: boolean; asap?: boolean;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
/** /**
* Maps bettween RESP and JavaScript types * Maps between RESP and JavaScript types
*/ */
typeMapping?: T; typeMapping?: T;
} }

View File

@@ -7,13 +7,13 @@ import { ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchErr
import { URL } from 'url'; import { URL } from 'url';
import { TcpSocketConnectOpts } from 'net'; import { TcpSocketConnectOpts } from 'net';
import { PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub'; import { PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub';
import { Command, CommandArguments, CommandSignature, TypeMapping, CommanderConfig, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, RedisArgument } from '../RESP/types'; import { Command, CommandSignature, TypeMapping, CommanderConfig, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, RedisArgument } from '../RESP/types';
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command'; import { RedisMultiQueuedCommand } from '../multi-command';
import HELLO, { HelloOptions } from '../commands/HELLO'; import HELLO, { HelloOptions } from '../commands/HELLO';
import { ScanOptions, ScanCommonOptions } from '../commands/SCAN'; import { ScanOptions, ScanCommonOptions } from '../commands/SCAN';
import { RedisLegacyClient, RedisLegacyClientType } from './legacy-mode'; import { RedisLegacyClient, RedisLegacyClientType } from './legacy-mode';
// import { RedisClientPool } from './pool'; import { RedisPoolOptions, RedisClientPool } from './pool';
interface ClientCommander< interface ClientCommander<
M extends RedisModules, M extends RedisModules,
@@ -21,7 +21,7 @@ interface ClientCommander<
S extends RedisScripts, S extends RedisScripts,
RESP extends RespVersions, RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping TYPE_MAPPING extends TypeMapping
> extends CommanderConfig<M, F, S, RESP>{ > extends CommanderConfig<M, F, S, RESP> {
commandOptions?: CommandOptions<TYPE_MAPPING>; commandOptions?: CommandOptions<TYPE_MAPPING>;
} }
@@ -72,7 +72,7 @@ export interface RedisClientOptions<
readonly?: boolean; readonly?: boolean;
/** /**
* Send `PING` command at interval (in ms). * Send `PING` command at interval (in ms).
* Useful with Redis deployments that do not use TCP Keep-Alive. * Useful with Redis deployments that do not honor TCP Keep-Alive.
*/ */
pingInterval?: number; pingInterval?: number;
} }
@@ -194,13 +194,7 @@ export default class RedisClient<
return async function (this: ProxyClient, ...args: Array<unknown>) { return async function (this: ProxyClient, ...args: Array<unknown>) {
const scriptArgs = script.transformArguments(...args), const scriptArgs = script.transformArguments(...args),
redisArgs = prefix.concat(scriptArgs), redisArgs = prefix.concat(scriptArgs),
reply = await this.sendCommand(redisArgs, this._commandOptions).catch((err: unknown) => { reply = await this.executeScript(script, redisArgs, this._commandOptions);
if (!(err as Error)?.message?.startsWith?.('NOSCRIPT')) throw err;
redisArgs[0] = 'EVAL';
redisArgs[1] = script.SCRIPT;
return this.sendCommand(redisArgs, this._commandOptions);
});
return transformReply ? return transformReply ?
transformReply(reply, scriptArgs.preserve) : transformReply(reply, scriptArgs.preserve) :
reply; reply;
@@ -218,8 +212,8 @@ export default class RedisClient<
BaseClass: RedisClient, BaseClass: RedisClient,
commands: COMMANDS, commands: COMMANDS,
createCommand: RedisClient._createCommand, createCommand: RedisClient._createCommand,
createFunctionCommand: RedisClient._createFunctionCommand,
createModuleCommand: RedisClient._createModuleCommand, createModuleCommand: RedisClient._createModuleCommand,
createFunctionCommand: RedisClient._createFunctionCommand,
createScriptCommand: RedisClient._createScriptCommand, createScriptCommand: RedisClient._createScriptCommand,
config config
}); });
@@ -227,8 +221,7 @@ export default class RedisClient<
Client.prototype.Multi = RedisClientMultiCommand.extend(config); Client.prototype.Multi = RedisClientMultiCommand.extend(config);
return (options?: Omit<RedisClientOptions, keyof Exclude<typeof config, undefined>>) => { return (options?: Omit<RedisClientOptions, keyof Exclude<typeof config, undefined>>) => {
// returning a proxy of the client to prevent the namespaces.self to leak between proxies // returning a "proxy" to prevent the namespaces.self to leak between "proxies"
// namespaces will be bootstraped on first access per proxy
return Object.create(new Client(options)) as RedisClientType<M, F, S, RESP, TYPE_MAPPING>; return Object.create(new Client(options)) as RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
}; };
} }
@@ -527,13 +520,14 @@ export default class RedisClient<
} }
/** /**
* Create `RedisClientPool` using this client as a prototype * Create {@link RedisClientPool `RedisClientPool`} using this client as a prototype
*/ */
// pool() { pool(options?: Partial<RedisPoolOptions>) {
// return RedisClientPool.fromClient( return RedisClientPool.create(
// this as unknown as RedisClientType<M, F, S, RESP> this._options,
// ); options
// } );
}
duplicate< duplicate<
_M extends RedisModules = M, _M extends RedisModules = M,
@@ -549,12 +543,13 @@ export default class RedisClient<
}) as RedisClientType<_M, _F, _S, _RESP, _TYPE_MAPPING>; }) as RedisClientType<_M, _F, _S, _RESP, _TYPE_MAPPING>;
} }
connect() { async connect() {
return this._socket.connect(); await this._socket.connect();
return this as unknown as RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
} }
sendCommand<T = ReplyUnion>( sendCommand<T = ReplyUnion>(
args: CommandArguments, args: Array<RedisArgument>,
options?: CommandOptions options?: CommandOptions
): Promise<T> { ): Promise<T> {
if (!this._socket.isOpen) { if (!this._socket.isOpen) {
@@ -568,6 +563,22 @@ export default class RedisClient<
return promise; return promise;
} }
async executeScript(
script: RedisScript,
args: Array<RedisArgument>,
options?: CommandOptions
) {
try {
return await this.sendCommand(args, options);
} catch (err) {
if (!(err as Error)?.message?.startsWith?.('NOSCRIPT')) throw err;
args[0] = 'EVAL';
args[1] = script.SCRIPT;
return await this.sendCommand(args, options);
}
}
async SELECT(db: number): Promise<void> { async SELECT(db: number): Promise<void> {
await this.sendCommand(['SELECT', db.toString()]); await this.sendCommand(['SELECT', db.toString()]);
this._selectedDB = db; this._selectedDB = db;
@@ -728,7 +739,7 @@ export default class RedisClient<
/** /**
* @internal * @internal
*/ */
executePipeline(commands: Array<RedisMultiQueuedCommand>) { _executePipeline(commands: Array<RedisMultiQueuedCommand>) {
if (!this._socket.isOpen) { if (!this._socket.isOpen) {
return Promise.reject(new ClientClosedError()); return Promise.reject(new ClientClosedError());
} }
@@ -745,7 +756,7 @@ export default class RedisClient<
/** /**
* @internal * @internal
*/ */
async executeMulti( async _executeMulti(
commands: Array<RedisMultiQueuedCommand>, commands: Array<RedisMultiQueuedCommand>,
selectedDB?: number selectedDB?: number
) { ) {

View File

@@ -160,7 +160,7 @@ class LegacyMultiCommand {
} }
exec(cb?: (err: ErrorReply | null, replies?: Array<unknown>) => unknown) { exec(cb?: (err: ErrorReply | null, replies?: Array<unknown>) => unknown) {
const promise = this._client.executeMulti(this._multi.queue); const promise = this._client._executeMulti(this._multi.queue);
if (!cb) { if (!cb) {
promise.catch(err => this._client.emit('error', err)); promise.catch(err => this._client.emit('error', err));

View File

@@ -138,9 +138,26 @@ export class SinglyLinkedList<T> {
}; };
if (this._head === undefined) { if (this._head === undefined) {
this._head = this._tail = node; return this._head = this._tail = node;
}
return this._tail!.next = this._tail = node;
}
remove(node: SinglyLinkedNode<T>, parent: SinglyLinkedNode<T> | undefined) {
--this._length;
if (this._head === node) {
if (this._tail === node) {
this._head = this._tail = undefined;
} else {
this._head = node.next;
}
} else if (this._tail === node) {
this._tail = parent;
parent!.next = undefined;
} else { } else {
this._tail!.next = this._tail = node; parent!.next = node.next;
} }
} }

View File

@@ -173,7 +173,7 @@ export default class RedisClientMultiCommand<REPLIES = []> {
if (execAsPipeline) return this.execAsPipeline<T>(); if (execAsPipeline) return this.execAsPipeline<T>();
return this._multi.transformReplies( return this._multi.transformReplies(
await this._client.executeMulti(this._multi.queue, this._selectedDB) await this._client._executeMulti(this._multi.queue, this._selectedDB)
) as MultiReplyType<T, REPLIES>; ) as MultiReplyType<T, REPLIES>;
} }
@@ -187,7 +187,7 @@ export default class RedisClientMultiCommand<REPLIES = []> {
if (this._multi.queue.length === 0) return [] as MultiReplyType<T, REPLIES>; if (this._multi.queue.length === 0) return [] as MultiReplyType<T, REPLIES>;
return this._multi.transformReplies( return this._multi.transformReplies(
await this._client.executePipeline(this._multi.queue) await this._client._executePipeline(this._multi.queue)
) as MultiReplyType<T, REPLIES>; ) as MultiReplyType<T, REPLIES>;
} }

View File

@@ -1,242 +1,469 @@
// import COMMANDS from '../commands'; import COMMANDS from '../commands';
// import { RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; import { Command, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions, TypeMapping } from '../RESP/types';
// import RedisClient, { RedisClientType, RedisClientOptions, RedisClientExtensions } from '.'; import RedisClient, { RedisClientType, RedisClientOptions, RedisClientExtensions } from '.';
// import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
// import { DoublyLinkedNode, DoublyLinkedList, SinglyLinkedList } from './linked-list'; import { DoublyLinkedNode, DoublyLinkedList, SinglyLinkedList } from './linked-list';
import { TimeoutError } from '../errors';
import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
import { CommandOptions } from './commands-queue';
// export type RedisPoolOptions = typeof RedisClientPool['_DEFAULTS']; export interface RedisPoolOptions {
/**
* The minimum number of clients to keep in the pool (>= 1).
*/
minimum: number;
/**
* The maximum number of clients to keep in the pool (>= {@link RedisPoolOptions.minimum} >= 1).
*/
maximum: number;
/**
* The maximum time a task can wait for a client to become available (>= 0).
*/
acquireTimeout: number;
/**
* TODO
*/
cleanupDelay: number;
}
// export type PoolTask< export type PoolTask<
// M extends RedisModules, M extends RedisModules,
// F extends RedisFunctions, F extends RedisFunctions,
// S extends RedisScripts, S extends RedisScripts,
// RESP extends RespVersions, RESP extends RespVersions,
// TYPE_MAPPING extends TypeMapping, TYPE_MAPPING extends TypeMapping,
// T = unknown T = unknown
// > = (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => T; > = (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => T;
// export type RedisClientPoolType< export type RedisClientPoolType<
// M extends RedisModules = {}, M extends RedisModules = {},
// F extends RedisFunctions = {}, F extends RedisFunctions = {},
// S extends RedisScripts = {}, S extends RedisScripts = {},
// RESP extends RespVersions = 2, RESP extends RespVersions = 2,
// TYPE_MAPPING extends TypeMapping = {} TYPE_MAPPING extends TypeMapping = {}
// > = ( > = (
// RedisClientPool<M, F, S, RESP, TYPE_MAPPING> & RedisClientPool<M, F, S, RESP, TYPE_MAPPING> &
// RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING> RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING>
// ); );
// export class RedisClientPool< type ProxyPool = RedisClientPoolType<any, any, any, any, any>;
// M extends RedisModules = {},
// F extends RedisFunctions = {},
// S extends RedisScripts = {},
// RESP extends RespVersions = 2,
// TYPE_MAPPING extends TypeMapping = {}
// > extends EventEmitter {
// static fromClient<
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts,
// RESP extends RespVersions,
// TYPE_MAPPING extends TypeMapping = {}
// >(
// client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
// poolOptions: Partial<RedisPoolOptions>
// ) {
// return RedisClientPool.create(
// () => client.duplicate(),
// poolOptions
// );
// }
// static fromOptions< type NamespaceProxyPool = { self: ProxyPool };
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts,
// RESP extends RespVersions,
// TYPE_MAPPING extends TypeMapping = {}
// >(
// options: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>,
// poolOptions: Partial<RedisPoolOptions>
// ) {
// return RedisClientPool.create(
// RedisClient.factory(options),
// poolOptions
// );
// }
// static create< export class RedisClientPool<
// M extends RedisModules, M extends RedisModules = {},
// F extends RedisFunctions, F extends RedisFunctions = {},
// S extends RedisScripts, S extends RedisScripts = {},
// RESP extends RespVersions, RESP extends RespVersions = 2,
// TYPE_MAPPING extends TypeMapping = {} TYPE_MAPPING extends TypeMapping = {}
// >( > extends EventEmitter {
// clientFactory: () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>, private static _createCommand(command: Command, resp: RespVersions) {
// options?: Partial<RedisPoolOptions> const transformReply = getTransformReply(command, resp);
// ) { return async function (this: ProxyPool, ...args: Array<unknown>) {
// return new RedisClientPool( const redisArgs = command.transformArguments(...args),
// clientFactory, reply = await this.sendCommand(redisArgs, this._commandOptions);
// options return transformReply ?
// ) as RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>; transformReply(reply, redisArgs.preserve) :
// } reply;
};
}
// private static _DEFAULTS = { private static _createModuleCommand(command: Command, resp: RespVersions) {
// /** const transformReply = getTransformReply(command, resp);
// * The minimum number of clients to keep in the pool. return async function (this: NamespaceProxyPool, ...args: Array<unknown>) {
// */ const redisArgs = command.transformArguments(...args),
// minimum: 0, reply = await this.self.sendCommand(redisArgs, this.self._commandOptions);
// /** return transformReply ?
// * The maximum number of clients to keep in the pool. transformReply(reply, redisArgs.preserve) :
// */ reply;
// maximum: 1, };
// /** }
// * The maximum time a task can wait for a client to become available.
// */
// acquireTimeout: 3000,
// /**
// * When there are `> minimum && < maximum` clients in the pool, the pool will wait for `cleanupDelay` milliseconds before closing the extra clients.
// */
// cleanupDelay: 3000
// };
// private readonly _clientFactory: () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>; private static _createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
// private readonly _options: Required<RedisPoolOptions>; const prefix = functionArgumentsPrefix(name, fn),
// private readonly _idleClients = new SinglyLinkedList<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>(); transformReply = getTransformReply(fn, resp);
// private readonly _usedClients = new DoublyLinkedList<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>(); return async function (this: NamespaceProxyPool, ...args: Array<unknown>) {
// private readonly _tasksQueue = new SinglyLinkedList<{ const fnArgs = fn.transformArguments(...args),
// resolve: <T>(value: T | PromiseLike<T>) => void; reply = await this.self.sendCommand(
// reject: (reason?: unknown) => void; prefix.concat(fnArgs),
// fn: PoolTask<M, F, S, RESP, TYPE_MAPPING>; this.self._commandOptions
// }>(); );
return transformReply ?
transformReply(reply, fnArgs.preserve) :
reply;
};
}
// constructor( private static _createScriptCommand(script: RedisScript, resp: RespVersions) {
// clientFactory: () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>, const prefix = scriptArgumentsPrefix(script),
// options?: Partial<RedisPoolOptions> transformReply = getTransformReply(script, resp);
// ) { return async function (this: ProxyPool, ...args: Array<unknown>) {
// super(); const scriptArgs = script.transformArguments(...args),
redisArgs = prefix.concat(scriptArgs),
reply = await this.executeScript(script, redisArgs, this._commandOptions);
return transformReply ?
transformReply(reply, scriptArgs.preserve) :
reply;
};
}
// this._clientFactory = clientFactory; static create<
// this._options = { M extends RedisModules,
// ...RedisClientPool._DEFAULTS, F extends RedisFunctions,
// ...options S extends RedisScripts,
// }; RESP extends RespVersions,
// this._initate(); TYPE_MAPPING extends TypeMapping = {}
// } >(
// clientFactory: () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
clientOptions?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>,
options?: Partial<RedisPoolOptions>
) {
// @ts-ignore
const Pool = attachConfig({
BaseClass: RedisClientPool,
commands: COMMANDS,
createCommand: RedisClientPool._createCommand,
createModuleCommand: RedisClientPool._createModuleCommand,
createFunctionCommand: RedisClientPool._createFunctionCommand,
createScriptCommand: RedisClientPool._createScriptCommand,
config: clientOptions
});
// private async _initate() { // returning a "proxy" to prevent the namespaces.self to leak between "proxies"
// const promises = []; return Object.create(
// while (promises.length < this._options.minimum) { new Pool(
// promises.push(this._create()); RedisClient.factory(clientOptions).bind(undefined, clientOptions),
// } options
)
) as RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>;
}
// try { // TODO: defaults
// await Promise.all(promises); private static _DEFAULTS = {
// } catch (err) { minimum: 1,
// this.destroy(); maximum: 100,
// this.emit('error', err); acquireTimeout: 3000,
// } cleanupDelay: 3000
// } } satisfies RedisPoolOptions;
// private async _create() { private readonly _clientFactory: () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
// const client = this._clientFactory() private readonly _options: RedisPoolOptions;
// // TODO: more events?
// .on('error', (err: Error) => this.emit('error', err));
// const node = this._usedClients.push(client); private readonly _idleClients = new SinglyLinkedList<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>();
// await client.connect(); /**
* The number of idle clients.
*/
get idleClients() {
return this._idleClients.length;
}
// this._usedClients.remove(node); private readonly _clientsInUse = new DoublyLinkedList<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>();
// return client; /**
// } * The number of clients in use.
*/
get clientsInUse() {
return this._clientsInUse.length;
}
// execute<T>(fn: PoolTask<M, F, S, RESP, TYPE_MAPPING, T>): Promise<T> { private readonly _connectingClients = 0;
// return new Promise<T>((resolve, reject) => {
// let client = this._idleClients.shift();
// if (!client) {
// this._tasksQueue.push({
// // @ts-ignore
// resolve,
// reject,
// fn
// });
// if (this._idleClients.length + this._usedClients.length < this._options.maximum) { /**
// this._create(); * The number of clients that are currently connecting.
// } */
get connectingClients() {
return this._connectingClients;
}
// return; /**
// } * The total number of clients in the pool (including connecting, idle, and in use).
*/
get totalClients() {
return this._idleClients.length + this._clientsInUse.length;
}
// const node = this._usedClients.push(client); private readonly _tasksQueue = new SinglyLinkedList<{
// // @ts-ignore timeout: NodeJS.Timeout | undefined;
// this._executeTask(node, resolve, reject, fn); resolve: (value: unknown) => unknown;
// }); reject: (reason?: unknown) => unknown;
// } fn: PoolTask<M, F, S, RESP, TYPE_MAPPING>;
}>();
// private _executeTask( /**
// node: DoublyLinkedNode<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>, * The number of tasks waiting for a client to become available.
// resolve: <T>(value: T | PromiseLike<T>) => void, */
// reject: (reason?: unknown) => void, get tasksQueueLength() {
// fn: PoolTask<M, F, S, RESP, TYPE_MAPPING> return this._tasksQueue.length;
// ) { }
// const result = fn(node.value);
// if (result instanceof Promise) {
// result.then(resolve, reject);
// result.finally(() => this._returnClient(node))
// } else {
// resolve(result);
// this._returnClient(node);
// }
// }
// private _returnClient(node: DoublyLinkedListNode<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>) { private _isOpen = false;
// const task = this._tasksQueue.shift();
// if (task) {
// this._executeTask(node, task.resolve, task.reject, task.fn);
// return;
// }
// if (this._idleClients.length >= this._options.minimum) { /**
// node.client.destroy(); * Whether the pool is open (either connecting or connected).
// return; */
// } get isOpen() {
return this._isOpen;
}
// this._usedClients.remove(node); private _isClosing = false;
// this._idleClients.push(node.client);
// }
// async close() { /**
// const promises = []; * Whether the pool is closing (*not* closed).
*/
get isClosing() {
return this._isClosing;
}
// for (const client of this._idleClients) { /**
// promises.push(client.close()); * You are probably looking for {@link RedisClient.pool `RedisClient.pool`},
// } * {@link RedisClientPool.fromClient `RedisClientPool.fromClient`},
* or {@link RedisClientPool.fromOptions `RedisClientPool.fromOptions`}...
*/
constructor(
clientFactory: () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
options?: Partial<RedisPoolOptions>
) {
super();
// this._idleClients.reset(); this._clientFactory = clientFactory;
this._options = {
...RedisClientPool._DEFAULTS,
...options
};
}
// for (const client of this._usedClients) { private _self = this;
// promises.push(client.close()); private _commandOptions?: CommandOptions<TYPE_MAPPING>;
// }
// this._usedClients.reset(); withCommandOptions<
OPTIONS extends CommandOptions<TYPE_MAPPING>,
TYPE_MAPPING extends TypeMapping
>(options: OPTIONS) {
const proxy = Object.create(this._self);
proxy._commandOptions = options;
return proxy as RedisClientPoolType<
M,
F,
S,
RESP,
TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}
>;
}
// await Promise.all(promises); private _commandOptionsProxy<
// } K extends keyof CommandOptions,
V extends CommandOptions[K]
>(
key: K,
value: V
) {
const proxy = Object.create(this._self);
proxy._commandOptions = Object.create(this._commandOptions ?? null);
proxy._commandOptions[key] = value;
return proxy as RedisClientPoolType<
M,
F,
S,
RESP,
K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING
>;
}
// destroy() { /**
// for (const client of this._idleClients) { * Override the `typeMapping` command option
// client.destroy(); */
// } withTypeMapping<TYPE_MAPPING extends TypeMapping>(typeMapping: TYPE_MAPPING) {
return this._commandOptionsProxy('typeMapping', typeMapping);
}
// this._idleClients.reset(); /**
* Override the `abortSignal` command option
*/
withAbortSignal(abortSignal: AbortSignal) {
return this._commandOptionsProxy('abortSignal', abortSignal);
}
// for (const client of this._usedClients) { /**
// client.destroy(); * Override the `asap` command option to `true`
// } * TODO: remove?
*/
asap() {
return this._commandOptionsProxy('asap', true);
}
// this._usedClients.reset(); async connect() {
// } if (this._isOpen) return; // TODO: throw error?
// }
this._isOpen = true;
const promises = [];
while (promises.length < this._options.minimum) {
promises.push(this._create());
}
try {
await Promise.all(promises);
return this as unknown as RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>;
} catch (err) {
this.destroy();
throw err;
}
}
private async _create() {
const node = this._clientsInUse.push(
this._clientFactory()
.on('error', (err: Error) => this.emit('error', err))
);
try {
await node.value.connect();
} catch (err) {
this._clientsInUse.remove(node);
throw err;
}
this._returnClient(node);
}
execute<T>(fn: PoolTask<M, F, S, RESP, TYPE_MAPPING, T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
const client = this._idleClients.shift(),
{ tail } = this._tasksQueue;
if (!client) {
let timeout;
if (this._options.acquireTimeout > 0) {
timeout = setTimeout(
() => {
this._tasksQueue.remove(task, tail);
reject(new TimeoutError('Timeout waiting for a client')); // TODO: message
},
this._options.acquireTimeout
);
}
const task = this._tasksQueue.push({
timeout,
// @ts-ignore
resolve,
reject,
fn
});
if (this.totalClients < this._options.maximum) {
this._create();
}
return;
}
const node = this._clientsInUse.push(client);
// @ts-ignore
this._executeTask(node, resolve, reject, fn);
});
}
private _executeTask(
node: DoublyLinkedNode<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>,
resolve: <T>(value: T | PromiseLike<T>) => void,
reject: (reason?: unknown) => void,
fn: PoolTask<M, F, S, RESP, TYPE_MAPPING>
) {
const result = fn(node.value);
if (result instanceof Promise) {
result.then(resolve, reject);
result.finally(() => this._returnClient(node))
} else {
resolve(result);
this._returnClient(node);
}
}
private _returnClient(node: DoublyLinkedNode<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>) {
const task = this._tasksQueue.shift();
if (task) {
this._executeTask(node, task.resolve, task.reject, task.fn);
return;
}
this._clientsInUse.remove(node);
this._idleClients.push(node.value);
this._scheduleCleanup();
}
cleanupTimeout?: NodeJS.Timeout;
private _scheduleCleanup() {
if (this.totalClients <= this._options.minimum) return;
clearTimeout(this.cleanupTimeout);
this.cleanupTimeout = setTimeout(() => this._cleanup(), this._options.cleanupDelay);
}
private _cleanup() {
const toDestroy = Math.min(this._idleClients.length, this.totalClients - this._options.minimum);
for (let i = 0; i < toDestroy; i++) {
// TODO: shift vs pop
this._idleClients.shift()!.destroy();
}
}
sendCommand(
args: Array<RedisArgument>,
options?: CommandOptions
) {
return this.execute(client => client.sendCommand(args, options));
}
executeScript(
script: RedisScript,
args: Array<RedisArgument>,
options?: CommandOptions
) {
return this.execute(client => client.executeScript(script, args, options));
}
async close() {
if (this._isClosing) return; // TODO: throw err?
if (!this._isOpen) return; // TODO: throw err?
this._isClosing = true;
try {
const promises = [];
for (const client of this._idleClients) {
promises.push(client.close());
}
for (const client of this._clientsInUse) {
promises.push(client.close());
}
await Promise.all(promises);
this._idleClients.reset();
this._clientsInUse.reset();
} catch (err) {
} finally {
this._isClosing = false;
}
}
destroy() {
for (const client of this._idleClients) {
client.destroy();
}
this._idleClients.reset();
for (const client of this._clientsInUse) {
client.destroy();
}
this._clientsInUse.reset();
this._isOpen = false;
}
}

View File

@@ -1,4 +1,4 @@
import { RedisClientOptions } from '../client'; import { RedisClientOptions, RedisClientType } from '../client';
import { CommandOptions } from '../client/commands-queue'; import { CommandOptions } from '../client/commands-queue';
import { Command, CommandArguments, CommanderConfig, CommandPolicies, CommandWithPoliciesSignature, TypeMapping, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions } from '../RESP/types'; import { Command, CommandArguments, CommanderConfig, CommandPolicies, CommandWithPoliciesSignature, TypeMapping, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions } from '../RESP/types';
import COMMANDS from '../commands'; import COMMANDS from '../commands';
@@ -224,8 +224,8 @@ export default class RedisCluster<
BaseClass: RedisCluster, BaseClass: RedisCluster,
commands: COMMANDS, commands: COMMANDS,
createCommand: RedisCluster._createCommand, createCommand: RedisCluster._createCommand,
createFunctionCommand: RedisCluster._createFunctionCommand,
createModuleCommand: RedisCluster._createModuleCommand, createModuleCommand: RedisCluster._createModuleCommand,
createFunctionCommand: RedisCluster._createFunctionCommand,
createScriptCommand: RedisCluster._createScriptCommand, createScriptCommand: RedisCluster._createScriptCommand,
config config
}); });
@@ -233,8 +233,7 @@ export default class RedisCluster<
Cluster.prototype.Multi = RedisClusterMultiCommand.extend(config); Cluster.prototype.Multi = RedisClusterMultiCommand.extend(config);
return (options?: Omit<RedisClusterOptions, keyof Exclude<typeof config, undefined>>) => { return (options?: Omit<RedisClusterOptions, keyof Exclude<typeof config, undefined>>) => {
// returning a proxy of the client to prevent the namespaces.self to leak between proxies // returning a "proxy" to prevent the namespaces.self to leak between "proxies"
// namespaces will be bootstraped on first access per proxy
return Object.create(new Cluster(options)) as RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>; return Object.create(new Cluster(options)) as RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>;
}; };
} }
@@ -388,21 +387,17 @@ export default class RedisCluster<
return this._commandOptionsProxy('policies', policies); return this._commandOptionsProxy('policies', policies);
} }
async sendCommand<T = ReplyUnion>( async #execute<T>(
firstKey: RedisArgument | undefined, firstKey: RedisArgument | undefined,
isReadonly: boolean | undefined, isReadonly: boolean | undefined,
args: CommandArguments, fn: (client: RedisClientType<M, F, S, RESP>) => Promise<T>
options?: ClusterCommandOptions,
deafultPolicies?: CommandPolicies
): Promise<T> { ): Promise<T> {
// const requestPolicy = options?.policies?.request ?? deafultPolicies?.request,
// responsePolicy = options?.policies?.response ?? deafultPolicies?.response;
const maxCommandRedirections = this._options.maxCommandRedirections ?? 16; const maxCommandRedirections = this._options.maxCommandRedirections ?? 16;
let client = await this._slots.getClient(firstKey, isReadonly); let client = await this._slots.getClient(firstKey, isReadonly),
for (let i = 0; ; i++) { i = 0;
while (true) {
try { try {
return await client.sendCommand<T>(args, options); return await fn(client);
} catch (err) { } catch (err) {
// TODO: error class // TODO: error class
if (++i > maxCommandRedirections || !(err instanceof Error)) { if (++i > maxCommandRedirections || !(err instanceof Error)) {
@@ -424,39 +419,69 @@ export default class RedisCluster<
await redirectTo.asking(); await redirectTo.asking();
client = redirectTo; client = redirectTo;
continue; continue;
} else if (err.message.startsWith('MOVED')) { }
if (err.message.startsWith('MOVED')) {
await this._slots.rediscover(client); await this._slots.rediscover(client);
client = await this._slots.getClient(firstKey, isReadonly); client = await this._slots.getClient(firstKey, isReadonly);
continue; continue;
} }
throw err; throw err;
} }
} }
} }
/** async sendCommand<T = ReplyUnion>(
* @internal
*/
async executePipeline(
firstKey: RedisArgument | undefined, firstKey: RedisArgument | undefined,
isReadonly: boolean | undefined, isReadonly: boolean | undefined,
commands: Array<RedisMultiQueuedCommand> args: CommandArguments,
options?: ClusterCommandOptions,
defaultPolicies?: CommandPolicies
): Promise<T> {
return this.#execute(
firstKey,
isReadonly,
client => client.sendCommand(args, options)
);
}
executeScript(
script: RedisScript,
firstKey: RedisArgument | undefined,
isReadonly: boolean | undefined,
args: Array<RedisArgument>,
options?: CommandOptions
) { ) {
const client = await this._slots.getClient(firstKey, isReadonly); return this.#execute(
return client.executePipeline(commands); firstKey,
isReadonly,
client => client.executeScript(script, args, options)
);
} }
/** /**
* @internal * @internal
*/ */
async executeMulti( async _executePipeline(
firstKey: RedisArgument | undefined, firstKey: RedisArgument | undefined,
isReadonly: boolean | undefined, isReadonly: boolean | undefined,
commands: Array<RedisMultiQueuedCommand> commands: Array<RedisMultiQueuedCommand>
) { ) {
const client = await this._slots.getClient(firstKey, isReadonly); const client = await this._slots.getClient(firstKey, isReadonly);
return client.executeMulti(commands); return client._executePipeline(commands);
}
/**
* @internal
*/
async _executeMulti(
firstKey: RedisArgument | undefined,
isReadonly: boolean | undefined,
commands: Array<RedisMultiQueuedCommand>
) {
const client = await this._slots.getClient(firstKey, isReadonly);
return client._executeMulti(commands);
} }
MULTI(routing?: RedisArgument): RedisClusterMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING> { MULTI(routing?: RedisArgument): RedisClusterMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING> {

View File

@@ -213,7 +213,7 @@ export default class RedisClusterMultiCommand<REPLIES = []> {
if (execAsPipeline) return this.execAsPipeline<T>(); if (execAsPipeline) return this.execAsPipeline<T>();
return this._multi.transformReplies( return this._multi.transformReplies(
await this._cluster.executeMulti( await this._cluster._executeMulti(
this._firstKey, this._firstKey,
this._isReadonly, this._isReadonly,
this._multi.queue this._multi.queue
@@ -231,7 +231,7 @@ export default class RedisClusterMultiCommand<REPLIES = []> {
if (this._multi.queue.length === 0) return [] as MultiReplyType<T, REPLIES>; if (this._multi.queue.length === 0) return [] as MultiReplyType<T, REPLIES>;
return this._multi.transformReplies( return this._multi.transformReplies(
await this._cluster.executePipeline( await this._cluster._executePipeline(
this._firstKey, this._firstKey,
this._isReadonly, this._isReadonly,
this._multi.queue this._multi.queue

View File

@@ -67,3 +67,5 @@ export class ErrorReply extends Error {
export class SimpleError extends ErrorReply {} export class SimpleError extends ErrorReply {}
export class BlobError extends ErrorReply {} export class BlobError extends ErrorReply {}
export class TimeoutError extends Error {}

44
test/package-lock.json generated
View File

@@ -1,44 +0,0 @@
{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "test",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@redis/client": "next"
}
},
"node_modules/@redis/client": {
"version": "2.0.0-next.2",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-2.0.0-next.2.tgz",
"integrity": "sha512-+sf9n+PBHac2xXSofSX0x79cYa5H4ighu80F993q4H1T109ZthFNGBmg33DfwfPrDMKc256qTXvsb0lCqzwMmg==",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
"engines": {
"node": ">= 4"
}
}
}
}

View File

@@ -1,15 +0,0 @@
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@redis/client": "next"
}
}

View File

@@ -1,21 +0,0 @@
import { RESP_TYPES, createClient } from '@redis/client';
const client = createClient({
RESP: 3,
commandOptions: {
typeMapping: {
[RESP_TYPES.MAP]: Map
}
}
});
client.on('error', err => console.error(err));
await client.connect();
console.log(
await client.flushAll(),
await client.hSet('key', 'field', 'value'),
await client.hGetAll('key')
)
client.destroy();