280 lines
11 KiB
JavaScript
280 lines
11 KiB
JavaScript
"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
|