"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.emitDiagnostics = exports.dbgMaintenance = exports.MAINTENANCE_EVENTS = void 0; const net_1 = require("net"); const promises_1 = require("dns/promises"); const node_assert_1 = __importDefault(require("node:assert")); const promises_2 = require("node:timers/promises"); const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel")); exports.MAINTENANCE_EVENTS = { PAUSE_WRITING: "pause-writing", RESUME_WRITING: "resume-writing", TIMEOUTS_UPDATE: "timeouts-update", }; const PN = { MOVING: "MOVING", MIGRATING: "MIGRATING", MIGRATED: "MIGRATED", FAILING_OVER: "FAILING_OVER", FAILED_OVER: "FAILED_OVER", }; const dbgMaintenance = (...args) => { if (!process.env.REDIS_DEBUG_MAINTENANCE) return; return console.log("[MNT]", ...args); }; exports.dbgMaintenance = dbgMaintenance; const emitDiagnostics = (event) => { if (!process.env.REDIS_EMIT_DIAGNOSTICS) return; const channel = node_diagnostics_channel_1.default.channel("redis.maintenance"); channel.publish(event); }; exports.emitDiagnostics = emitDiagnostics; class EnterpriseMaintenanceManager { #commandsQueue; #options; #isMaintenance = 0; #client; static setupDefaultMaintOptions(options) { if (options.maintNotifications === undefined) { options.maintNotifications = options?.RESP === 3 ? "auto" : "disabled"; } if (options.maintEndpointType === undefined) { options.maintEndpointType = "auto"; } if (options.maintRelaxedSocketTimeout === undefined) { options.maintRelaxedSocketTimeout = 10000; } if (options.maintRelaxedCommandTimeout === undefined) { options.maintRelaxedCommandTimeout = 10000; } } static async getHandshakeCommand(options) { if (options.maintNotifications === "disabled") return; const host = options.url ? new URL(options.url).hostname : options.socket?.host; if (!host) return; const tls = options.socket?.tls ?? false; const movingEndpointType = await determineEndpoint(tls, host, options); return { cmd: [ "CLIENT", "MAINT_NOTIFICATIONS", "ON", "moving-endpoint-type", movingEndpointType, ], errorHandler: (error) => { (0, exports.dbgMaintenance)("handshake failed:", error); if (options.maintNotifications === "enabled") { throw error; } }, }; } constructor(commandsQueue, client, options) { this.#commandsQueue = commandsQueue; this.#options = options; this.#client = client; this.#commandsQueue.addPushHandler(this.#onPush); } #onPush = (push) => { (0, exports.dbgMaintenance)("ONPUSH:", push.map(String)); if (!Array.isArray(push) || !["MOVING", "MIGRATING", "MIGRATED", "FAILING_OVER", "FAILED_OVER"].includes(String(push[0]))) { return false; } const type = String(push[0]); (0, exports.emitDiagnostics)({ type, timestamp: Date.now(), data: { push: push.map(String), }, }); switch (type) { case PN.MOVING: { // [ 'MOVING', '17', '15', '54.78.247.156:12075' ] // ^seq ^after ^new ip const afterSeconds = push[2]; const url = push[3] ? String(push[3]) : null; (0, exports.dbgMaintenance)("Received MOVING:", afterSeconds, url); this.#onMoving(afterSeconds, url); return true; } case PN.MIGRATING: case PN.FAILING_OVER: { (0, exports.dbgMaintenance)("Received MIGRATING|FAILING_OVER"); this.#onMigrating(); return true; } case PN.MIGRATED: case PN.FAILED_OVER: { (0, exports.dbgMaintenance)("Received MIGRATED|FAILED_OVER"); this.#onMigrated(); return true; } } return false; }; // Queue: // toWrite [ C D E ] // waitingForReply [ A B ] - aka In-flight commands // // time: ---1-2---3-4-5-6--------------------------- // // 1. [EVENT] MOVING PN received // 2. [ACTION] Pause writing ( we need to wait for new socket to connect and for all in-flight commands to complete ) // 3. [EVENT] New socket connected // 4. [EVENT] In-flight commands completed // 5. [ACTION] Destroy old socket // 6. [ACTION] Resume writing -> we are going to write to the new socket from now on #onMoving = async (afterSeconds, url) => { // 1 [EVENT] MOVING PN received this.#onMigrating(); let host; let port; // The special value `none` indicates that the `MOVING` message doesn’t need // to contain an endpoint. Instead it contains the value `null` then. In // such a corner case, the client is expected to schedule a graceful // reconnect to its currently configured endpoint after half of the grace // period that was communicated by the server is over. if (url === null) { (0, node_assert_1.default)(this.#options.maintEndpointType === "none"); (0, node_assert_1.default)(this.#options.socket !== undefined); (0, node_assert_1.default)("host" in this.#options.socket); (0, node_assert_1.default)(typeof this.#options.socket.host === "string"); host = this.#options.socket.host; (0, node_assert_1.default)(typeof this.#options.socket.port === "number"); port = this.#options.socket.port; const waitTime = (afterSeconds * 1000) / 2; (0, exports.dbgMaintenance)(`Wait for ${waitTime}ms`); await (0, promises_2.setTimeout)(waitTime); } else { const split = url.split(":"); host = split[0]; port = Number(split[1]); } // 2 [ACTION] Pause writing (0, exports.dbgMaintenance)("Pausing writing of new commands to old socket"); this.#client._pause(); (0, exports.dbgMaintenance)("Creating new tmp client"); let start = performance.now(); // If the URL is provided, it takes precedense // the options object could just be mutated if (this.#options.url) { const u = new URL(this.#options.url); u.hostname = host; u.port = String(port); this.#options.url = u.toString(); } else { this.#options.socket = { ...this.#options.socket, host, port }; } const tmpClient = this.#client.duplicate(); tmpClient.on('error', (error) => { //We dont know how to handle tmp client errors (0, exports.dbgMaintenance)(`[ERR]`, error); }); (0, exports.dbgMaintenance)(`Tmp client created in ${(performance.now() - start).toFixed(2)}ms`); (0, exports.dbgMaintenance)(`Set timeout for tmp client to ${this.#options.maintRelaxedSocketTimeout}`); tmpClient._maintenanceUpdate({ relaxedCommandTimeout: this.#options.maintRelaxedCommandTimeout, relaxedSocketTimeout: this.#options.maintRelaxedSocketTimeout, }); (0, exports.dbgMaintenance)(`Connecting tmp client: ${host}:${port}`); start = performance.now(); await tmpClient.connect(); (0, exports.dbgMaintenance)(`Connected to tmp client in ${(performance.now() - start).toFixed(2)}ms`); // 3 [EVENT] New socket connected (0, exports.dbgMaintenance)(`Wait for all in-flight commands to complete`); await this.#commandsQueue.waitForInflightCommandsToComplete(); (0, exports.dbgMaintenance)(`In-flight commands completed`); // 4 [EVENT] In-flight commands completed (0, exports.dbgMaintenance)("Swap client sockets..."); const oldSocket = this.#client._ejectSocket(); const newSocket = tmpClient._ejectSocket(); this.#client._insertSocket(newSocket); tmpClient._insertSocket(oldSocket); tmpClient.destroy(); (0, exports.dbgMaintenance)("Swap client sockets done."); // 5 + 6 (0, exports.dbgMaintenance)("Resume writing"); this.#client._unpause(); this.#onMigrated(); }; #onMigrating = () => { this.#isMaintenance++; if (this.#isMaintenance > 1) { (0, exports.dbgMaintenance)(`Timeout relaxation already done`); return; } const update = { relaxedCommandTimeout: this.#options.maintRelaxedCommandTimeout, relaxedSocketTimeout: this.#options.maintRelaxedSocketTimeout, }; this.#client._maintenanceUpdate(update); }; #onMigrated = () => { //ensure that #isMaintenance doesnt go under 0 this.#isMaintenance = Math.max(this.#isMaintenance - 1, 0); if (this.#isMaintenance > 0) { (0, exports.dbgMaintenance)(`Not ready to unrelax timeouts yet`); return; } const update = { relaxedCommandTimeout: undefined, relaxedSocketTimeout: undefined }; this.#client._maintenanceUpdate(update); }; } exports.default = EnterpriseMaintenanceManager; function isPrivateIP(ip) { const version = (0, net_1.isIP)(ip); if (version === 4) { const octets = ip.split(".").map(Number); return (octets[0] === 10 || (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) || (octets[0] === 192 && octets[1] === 168)); } if (version === 6) { return (ip.startsWith("fc") || // Unique local ip.startsWith("fd") || // Unique local ip === "::1" || // Loopback ip.startsWith("fe80") // Link-local unicast ); } return false; } async function determineEndpoint(tlsEnabled, host, options) { (0, node_assert_1.default)(options.maintEndpointType !== undefined); if (options.maintEndpointType !== "auto") { (0, exports.dbgMaintenance)(`Determine endpoint type: ${options.maintEndpointType}`); return options.maintEndpointType; } const ip = (0, net_1.isIP)(host) ? host : (await (0, promises_1.lookup)(host, { family: 0 })).address; const isPrivate = isPrivateIP(ip); let result; if (tlsEnabled) { result = isPrivate ? "internal-fqdn" : "external-fqdn"; } else { result = isPrivate ? "internal-ip" : "external-ip"; } (0, exports.dbgMaintenance)(`Determine endpoint type: ${result}`); return result; } //# sourceMappingURL=enterprise-maintenance-manager.js.map