1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-07 13:22:56 +03:00

fix #2679 - fix socket types, and clean some code

This commit is contained in:
Leibale
2024-02-12 18:38:00 -05:00
parent ec33759e8a
commit 77ecdf2721
4 changed files with 206 additions and 164 deletions

View File

@@ -1,16 +1,17 @@
# `createClient` configuration
| Property | Default | Description |
|--------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|------------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| url | | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) |
| socket | | Socket connection properties. Unlisted [`net.connect`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) properties (and [`tls.connect`](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)) are also supported |
| socket.port | `6379` | Redis server port |
| socket.host | `'localhost'` | Redis server hostname |
| socket.family | `0` | IP Stack version (one of `4 \| 6 \| 0`) |
| socket.path | | Path to the UNIX Socket |
| socket.connectTimeout | `5000` | Connection Timeout (in milliseconds) |
| socket.connectTimeout | `5000` | Connection timeout (in milliseconds) |
| socket.noDelay | `true` | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
| socket.keepAlive | `5000` | Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality |
| socket.keepAlive | `true` | Toggle [`keep-alive`](https://nodejs.org/api/net.html#socketsetkeepaliveenable-initialdelay) functionality |
| socket.keepAliveInitialDelay | `5000` | If set to a positive number, it sets the initial delay before the first keepalive probe is sent on an idle socket |
| socket.tls | | See explanation and examples [below](#TLS) |
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |

View File

@@ -1,5 +1,27 @@
# v4 to v5 migration guide
## Client Configuration
### Keep Alive
To better align with Node.js build-in [`net`](https://nodejs.org/api/net.html) and [`tls`](https://nodejs.org/api/tls.html) modules, the `keepAlive` option has been split into 2 options: `keepAlive` (`boolean`) and `keepAliveInitialDelay` (`number`). The defaults remain `true` and `5000`.
### Legacy Mode
In the previous version, you could access "legacy" mode by creating a client and passing in `{ legacyMode: true }`. Now, you can create one off of an existing client by calling the `.legacy()` function. This allows easier access to both APIs and enables better TypeScript support.
```javascript
// use `client` for the current API
const client = createClient();
await client.set('key', 'value');
// use `legacyClient` for the "legacy" API
const legacyClient = client.legacy();
legacyClient.set('key', 'value', (err, reply) => {
// ...
});
```
## Command Options
In v4, command options are passed as a first optional argument:
@@ -59,22 +81,6 @@ for await (const keys of client.scanIterator()) {
for more information, see the [Scan Iterators guide](./scan-iterators.md).
## Legacy Mode
In the previous version, you could access "legacy" mode by creating a client and passing in `{ legacyMode: true }`. Now, you can create one off of an existing client by calling the `.legacy()` function. This allows easier access to both APIs and enables better TypeScript support.
```javascript
// use `client` for the current API
const client = createClient();
await client.set('key', 'value');
// use `legacyClient` for the "legacy" API
const legacyClient = client.legacy();
legacyClient.set('key', 'value', (err, reply) => {
// ...
});
```
## Isolation Pool
[TODO](./blocking-commands.md).

View File

@@ -1,5 +1,5 @@
import COMMANDS from '../commands';
import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket';
import RedisSocket, { RedisSocketOptions } from './socket';
import RedisCommandsQueue, { CommandOptions } from './commands-queue';
import { EventEmitter } from 'node:events';
import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
@@ -244,7 +244,7 @@ export default class RedisClient<
};
if (protocol === 'rediss:') {
(parsed.socket as RedisTlsSocketOptions).tls = true;
parsed!.socket!.tls = true;
} else if (protocol !== 'redis:') {
throw new TypeError('Invalid protocol');
}

View File

@@ -1,84 +1,65 @@
import { EventEmitter } from 'node:events';
import * as net from 'node:net';
import * as tls from 'node:tls';
import { EventEmitter, once } from 'node:events';
import net from 'node:net';
import tls from 'node:tls';
import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError } from '../errors';
import { setTimeout } from 'node:timers/promises';
import { RedisArgument } from '../RESP/types';
export interface RedisSocketCommonOptions {
type NetOptions = {
tls?: false;
};
type TcpOptions = NetOptions & Omit<
net.TcpNetConnectOpts,
'timeout' | 'onread' | 'readable' | 'writable' | 'port'
> & {
port?: number;
};
type IpcOptions = NetOptions & Omit<
net.IpcNetConnectOpts,
'timeout' | 'onread' | 'readable' | 'writable'
>;
type TlsOptions = {
tls: true;
} & tls.ConnectionOptions;
type ReconnectStrategyFunction = (retries: number, cause: Error) => false | Error | number;
export type RedisSocketOptions = {
/**
* Connection Timeout (in milliseconds)
* Connection timeout (in milliseconds)
*/
connectTimeout?: number;
/**
* Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay)
*/
noDelay?: boolean;
/**
* Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay)
*/
keepAlive?: number | false;
/**
* When the socket closes unexpectedly (without calling `.close()`/`.destroy()`), the client uses `reconnectStrategy` to decide what to do. The following values are supported:
* 1. `false` -> do not reconnect, close the client and flush the command queue.
* 2. `number` -> wait for `X` milliseconds before reconnecting.
* 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
* Defaults to `retries => Math.min(retries * 50, 500)`
*/
reconnectStrategy?: false | number | ((retries: number, cause: Error) => false | Error | number);
}
export interface RedisNetConnectOpts extends Omit<Partial<net.TcpNetConnectOpts>, 'keepAlive'>, Partial<net.IpcNetConnectOpts>, RedisSocketCommonOptions {
tls?: false;
};
export interface RedisTlsSocketOptions extends Partial<tls.ConnectionOptions>, RedisSocketCommonOptions {
tls: true;
};
export type RedisSocketOptions = RedisNetConnectOpts | RedisTlsSocketOptions
interface CreateSocketReturn<T> {
connectEvent: string;
socket: T;
}
reconnectStrategy?: false | number | ReconnectStrategyFunction;
} & (TcpOptions | IpcOptions | TlsOptions);
export type RedisSocketInitiator = () => void | Promise<unknown>;
export default class RedisSocket extends EventEmitter {
static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions {
options ??= {};
if (!(options as net.IpcSocketConnectOpts).path) {
(options as net.TcpSocketConnectOpts).port ??= 6379;
(options as net.TcpSocketConnectOpts).host ??= 'localhost';
}
options.connectTimeout ??= 5000;
options.keepAlive ??= 5000;
options.noDelay ??= true;
return options;
}
static #isTlsSocket(options: RedisSocketOptions): options is RedisTlsSocketOptions {
return (options as RedisTlsSocketOptions).tls === true;
}
readonly #initiator: RedisSocketInitiator;
readonly #options: RedisSocketOptions;
readonly #initiator;
readonly #connectTimeout;
readonly #reconnectStrategy;
readonly #socketFactory;
#socket?: net.Socket | tls.TLSSocket;
#isOpen = false;
get isOpen(): boolean {
get isOpen() {
return this.#isOpen;
}
#isReady = false;
get isReady(): boolean {
get isReady() {
return this.#isReady;
}
@@ -88,28 +69,101 @@ export default class RedisSocket extends EventEmitter {
super();
this.#initiator = initiator;
this.#options = RedisSocket.#initiateOptions(options);
this.#connectTimeout = options?.connectTimeout ?? 5000;
this.#reconnectStrategy = this.#createReconnectStrategy(options);
this.#socketFactory = this.#createSocketFactory(options);
}
#reconnectStrategy(retries: number, cause: Error) {
if (this.#options.reconnectStrategy === false) {
return false;
} else if (typeof this.#options.reconnectStrategy === 'number') {
return this.#options.reconnectStrategy;
} else if (this.#options.reconnectStrategy) {
#createReconnectStrategy(options?: RedisSocketOptions): ReconnectStrategyFunction {
const strategy = options?.reconnectStrategy;
if (strategy === false || typeof strategy === 'number') {
return () => strategy;
}
if (strategy) {
return (retries, cause) => {
try {
const retryIn = this.#options.reconnectStrategy(retries, cause);
const retryIn = strategy(retries, cause);
if (retryIn !== false && !(retryIn instanceof Error) && typeof retryIn !== 'number') {
throw new TypeError(`Reconnect strategy should return \`false | Error | number\`, got ${retryIn} instead`);
}
return retryIn;
} catch (err) {
this.emit('error', err);
return Math.min(retries * 50, 500);
}
};
}
return Math.min(retries * 50, 500);
return retries => Math.min(retries * 50, 500);
}
#createSocketFactory(options?: RedisSocketOptions) {
// TLS
if (options?.tls === true) {
const withDefaults: tls.ConnectionOptions = {
...options,
port: options?.port ?? 6379,
// https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed"
// @types/node is... incorrect...
// @ts-expect-error
noDelay: options?.noDelay ?? true,
// https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed"
// @types/node is... incorrect...
// @ts-expect-error
keepAlive: options?.keepAlive ?? true,
// https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed"
// @types/node is... incorrect...
// @ts-expect-error
keepAliveInitialDelay: options?.keepAliveInitialDelay ?? 5000,
timeout: undefined,
onread: undefined,
readable: true,
writable: true
};
return {
create() {
return tls.connect(withDefaults);
},
event: 'secureConnect'
};
}
// IPC
if (options && 'path' in options) {
const withDefaults: net.IpcNetConnectOpts = {
...options,
timeout: undefined,
onread: undefined,
readable: true,
writable: true
};
return {
create() {
return net.createConnection(withDefaults);
},
event: 'connect'
};
}
// TCP
const withDefaults: net.TcpNetConnectOpts = {
...options,
port: options?.port ?? 6379,
noDelay: options?.noDelay ?? true,
keepAlive: options?.keepAlive ?? true,
keepAliveInitialDelay: options?.keepAliveInitialDelay ?? 5000,
timeout: undefined,
onread: undefined,
readable: true,
writable: true
};
return {
create() {
return net.createConnection(withDefaults);
},
event: 'connect'
};
}
#shouldReconnect(retries: number, cause: Error) {
@@ -165,55 +219,36 @@ export default class RedisSocket extends EventEmitter {
} while (this.#isOpen && !this.#isReady);
}
#createSocket(): Promise<net.Socket | tls.TLSSocket> {
return new Promise((resolve, reject) => {
const { connectEvent, socket } = RedisSocket.#isTlsSocket(this.#options) ?
this.#createTlsSocket() :
this.#createNetSocket();
async #createSocket(): Promise<net.Socket | tls.TLSSocket> {
const socket = this.#socketFactory.create();
if (this.#options.connectTimeout) {
socket.setTimeout(this.#options.connectTimeout, () => socket.destroy(new ConnectionTimeoutError()));
let onTimeout;
if (this.#connectTimeout !== undefined) {
onTimeout = () => socket.destroy(new ConnectionTimeoutError());
socket.once('timeout', onTimeout);
socket.setTimeout(this.#connectTimeout);
}
if (this.#isSocketUnrefed) {
socket.unref();
}
socket
.setNoDelay(this.#options.noDelay)
.once('error', reject)
.once(connectEvent, () => {
socket
.setTimeout(0)
// https://github.com/nodejs/node/issues/31663
.setKeepAlive(this.#options.keepAlive !== false, this.#options.keepAlive || 0)
.off('error', reject)
.once('error', (err: Error) => this.#onSocketError(err))
.once('close', hadError => {
if (!hadError && this.#isOpen && this.#socket === socket) {
this.#onSocketError(new SocketClosedUnexpectedlyError());
await once(socket, this.#socketFactory.event);
if (onTimeout) {
socket.removeListener('timeout', onTimeout);
}
socket
.once('error', err => this.#onSocketError(err))
.once('close', hadError => {
if (hadError || !this.#isOpen || this.#socket !== socket) return;
this.#onSocketError(new SocketClosedUnexpectedlyError());
})
.on('drain', () => this.emit('drain'))
.on('data', data => this.emit('data', data));
resolve(socket);
});
});
}
#createNetSocket(): CreateSocketReturn<net.Socket> {
return {
connectEvent: 'connect',
socket: net.connect(this.#options as net.NetConnectOpts) // TODO
};
}
#createTlsSocket(): CreateSocketReturn<tls.TLSSocket> {
return {
connectEvent: 'secureConnect',
socket: tls.connect(this.#options as tls.ConnectionOptions) // TODO
};
return socket;
}
#onSocketError(err: Error): void {
@@ -229,11 +264,11 @@ export default class RedisSocket extends EventEmitter {
});
}
write(iterator: IterableIterator<Array<RedisArgument>>): void {
write(iterable: Iterable<Array<RedisArgument>>) {
if (!this.#socket) return;
this.#socket.cork();
for (const args of iterator) {
for (const args of iterable) {
for (const toWrite of args) {
this.#socket.write(toWrite);
}
@@ -282,12 +317,12 @@ export default class RedisSocket extends EventEmitter {
this.emit('end');
}
ref(): void {
ref() {
this.#isSocketUnrefed = false;
this.#socket?.ref();
}
unref(): void {
unref() {
this.#isSocketUnrefed = true;
this.#socket?.unref();
}