Files
micro-service-api/node_modules/@redis/client/dist/lib/client/enterprise-maintenance-manager.js
2025-11-11 12:36:06 +07:00

280 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 doesnt 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