-first commit

This commit is contained in:
2025-11-11 12:36:06 +07:00
commit b99c214434
5683 changed files with 713336 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
import { NetStream } from "../types";
export declare type ErrorEmitter = (type: string, err: Error) => void;
export default abstract class AbstractConnector {
firstError?: Error;
protected connecting: boolean;
protected stream: NetStream;
private disconnectTimeout;
constructor(disconnectTimeout: number);
check(info: any): boolean;
disconnect(): void;
abstract connect(_: ErrorEmitter): Promise<NetStream>;
}

View File

@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const debug = (0, utils_1.Debug)("AbstractConnector");
class AbstractConnector {
constructor(disconnectTimeout) {
this.connecting = false;
this.disconnectTimeout = disconnectTimeout;
}
check(info) {
return true;
}
disconnect() {
this.connecting = false;
if (this.stream) {
const stream = this.stream; // Make sure callbacks refer to the same instance
const timeout = setTimeout(() => {
debug("stream %s:%s still open, destroying it", stream.remoteAddress, stream.remotePort);
stream.destroy();
}, this.disconnectTimeout);
stream.on("close", () => clearTimeout(timeout));
stream.end();
}
}
}
exports.default = AbstractConnector;

View File

@@ -0,0 +1,5 @@
import AbstractConnector from "./AbstractConnector";
interface ConnectorConstructor {
new (options: unknown): AbstractConnector;
}
export default ConnectorConstructor;

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,11 @@
import SentinelConnector from "./index";
import { Sentinel } from "./types";
export declare class FailoverDetector {
private connector;
private sentinels;
private isDisconnected;
constructor(connector: SentinelConnector, sentinels: Sentinel[]);
cleanup(): void;
subscribe(): Promise<void>;
private disconnect;
}

View File

@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FailoverDetector = void 0;
const utils_1 = require("../../utils");
const debug = (0, utils_1.Debug)("FailoverDetector");
const CHANNEL_NAME = "+switch-master";
class FailoverDetector {
// sentinels can't be used for regular commands after this
constructor(connector, sentinels) {
this.isDisconnected = false;
this.connector = connector;
this.sentinels = sentinels;
}
cleanup() {
this.isDisconnected = true;
for (const sentinel of this.sentinels) {
sentinel.client.disconnect();
}
}
async subscribe() {
debug("Starting FailoverDetector");
const promises = [];
for (const sentinel of this.sentinels) {
const promise = sentinel.client.subscribe(CHANNEL_NAME).catch((err) => {
debug("Failed to subscribe to failover messages on sentinel %s:%s (%s)", sentinel.address.host || "127.0.0.1", sentinel.address.port || 26739, err.message);
});
promises.push(promise);
sentinel.client.on("message", (channel) => {
if (!this.isDisconnected && channel === CHANNEL_NAME) {
this.disconnect();
}
});
}
await Promise.all(promises);
}
disconnect() {
// Avoid disconnecting more than once per failover.
// A new FailoverDetector will be created after reconnecting.
this.isDisconnected = true;
debug("Failover detected, disconnecting");
// Will call this.cleanup()
this.connector.disconnect();
}
}
exports.FailoverDetector = FailoverDetector;

View File

@@ -0,0 +1,13 @@
import { SentinelAddress } from "./types";
export default class SentinelIterator implements Iterator<Partial<SentinelAddress>> {
private cursor;
private sentinels;
constructor(sentinels: Array<Partial<SentinelAddress>>);
next(): {
done: boolean;
value: Partial<SentinelAddress>;
};
reset(moveCurrentEndpointToFirst: boolean): void;
add(sentinel: SentinelAddress): boolean;
toString(): string;
}

View File

@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function isSentinelEql(a, b) {
return ((a.host || "127.0.0.1") === (b.host || "127.0.0.1") &&
(a.port || 26379) === (b.port || 26379));
}
class SentinelIterator {
constructor(sentinels) {
this.cursor = 0;
this.sentinels = sentinels.slice(0);
}
next() {
const done = this.cursor >= this.sentinels.length;
return { done, value: done ? undefined : this.sentinels[this.cursor++] };
}
reset(moveCurrentEndpointToFirst) {
if (moveCurrentEndpointToFirst &&
this.sentinels.length > 1 &&
this.cursor !== 1) {
this.sentinels.unshift(...this.sentinels.splice(this.cursor - 1));
}
this.cursor = 0;
}
add(sentinel) {
for (let i = 0; i < this.sentinels.length; i++) {
if (isSentinelEql(sentinel, this.sentinels[i])) {
return false;
}
}
this.sentinels.push(sentinel);
return true;
}
toString() {
return `${JSON.stringify(this.sentinels)} @${this.cursor}`;
}
}
exports.default = SentinelIterator;

View File

@@ -0,0 +1,72 @@
/// <reference types="node" />
import { EventEmitter } from "events";
import { NatMap } from "../../cluster/ClusterOptions";
import { ConnectionOptions } from "tls";
import SentinelIterator from "./SentinelIterator";
import { SentinelAddress } from "./types";
import AbstractConnector, { ErrorEmitter } from "../AbstractConnector";
import { NetStream } from "../../types";
interface AddressFromResponse {
port: string;
ip: string;
flags?: string;
}
declare type PreferredSlaves = ((slaves: AddressFromResponse[]) => AddressFromResponse | null) | Array<{
port: string;
ip: string;
prio?: number;
}> | {
port: string;
ip: string;
prio?: number;
};
export { SentinelAddress, SentinelIterator };
export interface SentinelConnectionOptions {
/**
* Master group name of the Sentinel
*/
name?: string;
/**
* @default "master"
*/
role?: "master" | "slave";
tls?: ConnectionOptions;
sentinelUsername?: string;
sentinelPassword?: string;
sentinels?: Array<Partial<SentinelAddress>>;
sentinelRetryStrategy?: (retryAttempts: number) => number | void | null;
sentinelReconnectStrategy?: (retryAttempts: number) => number | void | null;
preferredSlaves?: PreferredSlaves;
connectTimeout?: number;
disconnectTimeout?: number;
sentinelCommandTimeout?: number;
enableTLSForSentinelMode?: boolean;
sentinelTLS?: ConnectionOptions;
natMap?: NatMap;
updateSentinels?: boolean;
/**
* @default 10
*/
sentinelMaxConnections?: number;
failoverDetector?: boolean;
}
export default class SentinelConnector extends AbstractConnector {
protected options: SentinelConnectionOptions;
emitter: EventEmitter | null;
protected sentinelIterator: SentinelIterator;
private retryAttempts;
private failoverDetector;
constructor(options: SentinelConnectionOptions);
check(info: {
role?: string;
}): boolean;
disconnect(): void;
connect(eventEmitter: ErrorEmitter): Promise<NetStream>;
private updateSentinels;
private resolveMaster;
private resolveSlave;
private sentinelNatResolve;
private connectToSentinel;
private resolve;
private initFailoverDetector;
}

View File

@@ -0,0 +1,305 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SentinelIterator = void 0;
const net_1 = require("net");
const utils_1 = require("../../utils");
const tls_1 = require("tls");
const SentinelIterator_1 = require("./SentinelIterator");
exports.SentinelIterator = SentinelIterator_1.default;
const AbstractConnector_1 = require("../AbstractConnector");
const Redis_1 = require("../../Redis");
const FailoverDetector_1 = require("./FailoverDetector");
const debug = (0, utils_1.Debug)("SentinelConnector");
class SentinelConnector extends AbstractConnector_1.default {
constructor(options) {
super(options.disconnectTimeout);
this.options = options;
this.emitter = null;
this.failoverDetector = null;
if (!this.options.sentinels.length) {
throw new Error("Requires at least one sentinel to connect to.");
}
if (!this.options.name) {
throw new Error("Requires the name of master.");
}
this.sentinelIterator = new SentinelIterator_1.default(this.options.sentinels);
}
check(info) {
const roleMatches = !info.role || this.options.role === info.role;
if (!roleMatches) {
debug("role invalid, expected %s, but got %s", this.options.role, info.role);
// Start from the next item.
// Note that `reset` will move the cursor to the previous element,
// so we advance two steps here.
this.sentinelIterator.next();
this.sentinelIterator.next();
this.sentinelIterator.reset(true);
}
return roleMatches;
}
disconnect() {
super.disconnect();
if (this.failoverDetector) {
this.failoverDetector.cleanup();
}
}
connect(eventEmitter) {
this.connecting = true;
this.retryAttempts = 0;
let lastError;
const connectToNext = async () => {
const endpoint = this.sentinelIterator.next();
if (endpoint.done) {
this.sentinelIterator.reset(false);
const retryDelay = typeof this.options.sentinelRetryStrategy === "function"
? this.options.sentinelRetryStrategy(++this.retryAttempts)
: null;
let errorMsg = typeof retryDelay !== "number"
? "All sentinels are unreachable and retry is disabled."
: `All sentinels are unreachable. Retrying from scratch after ${retryDelay}ms.`;
if (lastError) {
errorMsg += ` Last error: ${lastError.message}`;
}
debug(errorMsg);
const error = new Error(errorMsg);
if (typeof retryDelay === "number") {
eventEmitter("error", error);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
return connectToNext();
}
else {
throw error;
}
}
let resolved = null;
let err = null;
try {
resolved = await this.resolve(endpoint.value);
}
catch (error) {
err = error;
}
if (!this.connecting) {
throw new Error(utils_1.CONNECTION_CLOSED_ERROR_MSG);
}
const endpointAddress = endpoint.value.host + ":" + endpoint.value.port;
if (resolved) {
debug("resolved: %s:%s from sentinel %s", resolved.host, resolved.port, endpointAddress);
if (this.options.enableTLSForSentinelMode && this.options.tls) {
Object.assign(resolved, this.options.tls);
this.stream = (0, tls_1.connect)(resolved);
this.stream.once("secureConnect", this.initFailoverDetector.bind(this));
}
else {
this.stream = (0, net_1.createConnection)(resolved);
this.stream.once("connect", this.initFailoverDetector.bind(this));
}
this.stream.once("error", (err) => {
this.firstError = err;
});
return this.stream;
}
else {
const errorMsg = err
? "failed to connect to sentinel " +
endpointAddress +
" because " +
err.message
: "connected to sentinel " +
endpointAddress +
" successfully, but got an invalid reply: " +
resolved;
debug(errorMsg);
eventEmitter("sentinelError", new Error(errorMsg));
if (err) {
lastError = err;
}
return connectToNext();
}
};
return connectToNext();
}
async updateSentinels(client) {
if (!this.options.updateSentinels) {
return;
}
const result = await client.sentinel("sentinels", this.options.name);
if (!Array.isArray(result)) {
return;
}
result
.map(utils_1.packObject)
.forEach((sentinel) => {
const flags = sentinel.flags ? sentinel.flags.split(",") : [];
if (flags.indexOf("disconnected") === -1 &&
sentinel.ip &&
sentinel.port) {
const endpoint = this.sentinelNatResolve(addressResponseToAddress(sentinel));
if (this.sentinelIterator.add(endpoint)) {
debug("adding sentinel %s:%s", endpoint.host, endpoint.port);
}
}
});
debug("Updated internal sentinels: %s", this.sentinelIterator);
}
async resolveMaster(client) {
const result = await client.sentinel("get-master-addr-by-name", this.options.name);
await this.updateSentinels(client);
return this.sentinelNatResolve(Array.isArray(result)
? { host: result[0], port: Number(result[1]) }
: null);
}
async resolveSlave(client) {
const result = await client.sentinel("slaves", this.options.name);
if (!Array.isArray(result)) {
return null;
}
const availableSlaves = result
.map(utils_1.packObject)
.filter((slave) => slave.flags && !slave.flags.match(/(disconnected|s_down|o_down)/));
return this.sentinelNatResolve(selectPreferredSentinel(availableSlaves, this.options.preferredSlaves));
}
sentinelNatResolve(item) {
if (!item || !this.options.natMap)
return item;
const key = `${item.host}:${item.port}`;
let result = item;
if (typeof this.options.natMap === "function") {
result = this.options.natMap(key) || item;
}
else if (typeof this.options.natMap === "object") {
result = this.options.natMap[key] || item;
}
return result;
}
connectToSentinel(endpoint, options) {
const redis = new Redis_1.default({
port: endpoint.port || 26379,
host: endpoint.host,
username: this.options.sentinelUsername || null,
password: this.options.sentinelPassword || null,
family: endpoint.family ||
// @ts-expect-error
("path" in this.options && this.options.path
? undefined
: // @ts-expect-error
this.options.family),
tls: this.options.sentinelTLS,
retryStrategy: null,
enableReadyCheck: false,
connectTimeout: this.options.connectTimeout,
commandTimeout: this.options.sentinelCommandTimeout,
...options,
});
// @ts-expect-error
return redis;
}
async resolve(endpoint) {
const client = this.connectToSentinel(endpoint);
// ignore the errors since resolve* methods will handle them
client.on("error", noop);
try {
if (this.options.role === "slave") {
return await this.resolveSlave(client);
}
else {
return await this.resolveMaster(client);
}
}
finally {
client.disconnect();
}
}
async initFailoverDetector() {
var _a;
if (!this.options.failoverDetector) {
return;
}
// Move the current sentinel to the first position
this.sentinelIterator.reset(true);
const sentinels = [];
// In case of a large amount of sentinels, limit the number of concurrent connections
while (sentinels.length < this.options.sentinelMaxConnections) {
const { done, value } = this.sentinelIterator.next();
if (done) {
break;
}
const client = this.connectToSentinel(value, {
lazyConnect: true,
retryStrategy: this.options.sentinelReconnectStrategy,
});
client.on("reconnecting", () => {
var _a;
// Tests listen to this event
(_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit("sentinelReconnecting");
});
sentinels.push({ address: value, client });
}
this.sentinelIterator.reset(false);
if (this.failoverDetector) {
// Clean up previous detector
this.failoverDetector.cleanup();
}
this.failoverDetector = new FailoverDetector_1.FailoverDetector(this, sentinels);
await this.failoverDetector.subscribe();
// Tests listen to this event
(_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit("failoverSubscribed");
}
}
exports.default = SentinelConnector;
function selectPreferredSentinel(availableSlaves, preferredSlaves) {
if (availableSlaves.length === 0) {
return null;
}
let selectedSlave;
if (typeof preferredSlaves === "function") {
selectedSlave = preferredSlaves(availableSlaves);
}
else if (preferredSlaves !== null && typeof preferredSlaves === "object") {
const preferredSlavesArray = Array.isArray(preferredSlaves)
? preferredSlaves
: [preferredSlaves];
// sort by priority
preferredSlavesArray.sort((a, b) => {
// default the priority to 1
if (!a.prio) {
a.prio = 1;
}
if (!b.prio) {
b.prio = 1;
}
// lowest priority first
if (a.prio < b.prio) {
return -1;
}
if (a.prio > b.prio) {
return 1;
}
return 0;
});
// loop over preferred slaves and return the first match
for (let p = 0; p < preferredSlavesArray.length; p++) {
for (let a = 0; a < availableSlaves.length; a++) {
const slave = availableSlaves[a];
if (slave.ip === preferredSlavesArray[p].ip) {
if (slave.port === preferredSlavesArray[p].port) {
selectedSlave = slave;
break;
}
}
}
if (selectedSlave) {
break;
}
}
}
// if none of the preferred slaves are available, a random available slave is returned
if (!selectedSlave) {
selectedSlave = (0, utils_1.sample)(availableSlaves);
}
return addressResponseToAddress(selectedSlave);
}
function addressResponseToAddress(input) {
return { host: input.ip, port: Number(input.port) };
}
function noop() { }

View File

@@ -0,0 +1,21 @@
import { RedisOptions } from "../../redis/RedisOptions";
export interface SentinelAddress {
port: number;
host: string;
family?: number;
}
export interface RedisClient {
options: RedisOptions;
sentinel(subcommand: "sentinels", name: string): Promise<string[]>;
sentinel(subcommand: "get-master-addr-by-name", name: string): Promise<string[]>;
sentinel(subcommand: "slaves", name: string): Promise<string[]>;
subscribe(...channelNames: string[]): Promise<number>;
on(event: "message", callback: (channel: string, message: string) => void): void;
on(event: "error", callback: (error: Error) => void): void;
on(event: "reconnecting", callback: () => void): void;
disconnect(): void;
}
export interface Sentinel {
address: Partial<SentinelAddress>;
client: RedisClient;
}

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,17 @@
/// <reference types="node" />
import { IpcNetConnectOpts, TcpNetConnectOpts } from "net";
import { ConnectionOptions } from "tls";
import { NetStream } from "../types";
import AbstractConnector, { ErrorEmitter } from "./AbstractConnector";
declare type TcpOptions = Pick<TcpNetConnectOpts, "port" | "host" | "family">;
declare type IpcOptions = Pick<IpcNetConnectOpts, "path">;
export declare type StandaloneConnectionOptions = Partial<TcpOptions & IpcOptions> & {
disconnectTimeout?: number;
tls?: ConnectionOptions;
};
export default class StandaloneConnector extends AbstractConnector {
protected options: StandaloneConnectionOptions;
constructor(options: StandaloneConnectionOptions);
connect(_: ErrorEmitter): Promise<NetStream>;
}
export {};

View File

@@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const net_1 = require("net");
const tls_1 = require("tls");
const utils_1 = require("../utils");
const AbstractConnector_1 = require("./AbstractConnector");
class StandaloneConnector extends AbstractConnector_1.default {
constructor(options) {
super(options.disconnectTimeout);
this.options = options;
}
connect(_) {
const { options } = this;
this.connecting = true;
let connectionOptions;
if ("path" in options && options.path) {
connectionOptions = {
path: options.path,
};
}
else {
connectionOptions = {};
if ("port" in options && options.port != null) {
connectionOptions.port = options.port;
}
if ("host" in options && options.host != null) {
connectionOptions.host = options.host;
}
if ("family" in options && options.family != null) {
connectionOptions.family = options.family;
}
}
if (options.tls) {
Object.assign(connectionOptions, options.tls);
}
// TODO:
// We use native Promise here since other Promise
// implementation may use different schedulers that
// cause issue when the stream is resolved in the
// next tick.
// Should use the provided promise in the next major
// version and do not connect before resolved.
return new Promise((resolve, reject) => {
process.nextTick(() => {
if (!this.connecting) {
reject(new Error(utils_1.CONNECTION_CLOSED_ERROR_MSG));
return;
}
try {
if (options.tls) {
this.stream = (0, tls_1.connect)(connectionOptions);
}
else {
this.stream = (0, net_1.createConnection)(connectionOptions);
}
}
catch (err) {
reject(err);
return;
}
this.stream.once("error", (err) => {
this.firstError = err;
});
resolve(this.stream);
});
});
}
}
exports.default = StandaloneConnector;

3
node_modules/ioredis/built/connectors/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
import StandaloneConnector from "./StandaloneConnector";
import SentinelConnector from "./SentinelConnector";
export { StandaloneConnector, SentinelConnector };

7
node_modules/ioredis/built/connectors/index.js generated vendored Normal file
View File

@@ -0,0 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SentinelConnector = exports.StandaloneConnector = void 0;
const StandaloneConnector_1 = require("./StandaloneConnector");
exports.StandaloneConnector = StandaloneConnector_1.default;
const SentinelConnector_1 = require("./SentinelConnector");
exports.SentinelConnector = SentinelConnector_1.default;