UNPKG

ioredis

Version:

A robust, performance-focused and full-featured Redis client for Node.js.

298 lines (297 loc) 11.7 kB
"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; return this.options.natMap[`${item.host}:${item.port}`] || item; } 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() { }