nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
360 lines (298 loc) • 10.2 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/cluster/primary.js
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import * as assert from "nstdlib/lib/internal/assert";
import { fork } from "nstdlib/lib/child_process";
import * as path from "nstdlib/lib/path";
import * as EventEmitter from "nstdlib/lib/events";
import * as RoundRobinHandle from "nstdlib/lib/internal/cluster/round_robin_handle";
import * as SharedHandle from "nstdlib/lib/internal/cluster/shared_handle";
import Worker from "nstdlib/lib/internal/cluster/worker";
import {
getInspectPort,
isUsingInspector,
} from "nstdlib/lib/internal/util/inspector";
import { internal, sendHelper } from "nstdlib/lib/internal/cluster/utils";
const { ERR_SOCKET_BAD_PORT } = __codes__;
const cluster = new EventEmitter();
const intercom = new EventEmitter();
const SCHED_NONE = 1;
const SCHED_RR = 2;
export default cluster;
const handles = new Map();
cluster.isWorker = false;
cluster.isMaster = true; // Deprecated alias. Must be same as isPrimary.
cluster.isPrimary = true;
cluster.Worker = Worker;
cluster.workers = {};
cluster.settings = {};
cluster.SCHED_NONE = SCHED_NONE; // Leave it to the operating system.
cluster.SCHED_RR = SCHED_RR; // Primary distributes connections.
let ids = 0;
let initialized = false;
// XXX(bnoordhuis) Fold cluster.schedulingPolicy into cluster.settings?
let schedulingPolicy = process.env.NODE_CLUSTER_SCHED_POLICY;
if (schedulingPolicy === "rr") schedulingPolicy = SCHED_RR;
else if (schedulingPolicy === "none") schedulingPolicy = SCHED_NONE;
else if (process.platform === "win32") {
// Round-robin doesn't perform well on
// Windows due to the way IOCP is wired up.
schedulingPolicy = SCHED_NONE;
} else schedulingPolicy = SCHED_RR;
cluster.schedulingPolicy = schedulingPolicy;
cluster.setupPrimary = function (options) {
const settings = {
args: Array.prototype.slice.call(process.argv, 2),
exec: process.argv[1],
execArgv: process.execArgv,
silent: false,
...cluster.settings,
...options,
};
// Tell V8 to write profile data for each process to a separate file.
// Without --logfile=v8-%p.log, everything ends up in a single, unusable
// file. (Unusable because what V8 logs are memory addresses and each
// process has its own memory mappings.)
if (
Array.prototype.some.call(settings.execArgv, (s) =>
String.prototype.startsWith.call(s, "--prof"),
) &&
!Array.prototype.some.call(settings.execArgv, (s) =>
String.prototype.startsWith.call(s, "--logfile="),
)
) {
settings.execArgv = [...settings.execArgv, "--logfile=v8-%p.log"];
}
cluster.settings = settings;
if (initialized === true) return process.nextTick(setupSettingsNT, settings);
initialized = true;
schedulingPolicy = cluster.schedulingPolicy; // Freeze policy.
assert(
schedulingPolicy === SCHED_NONE || schedulingPolicy === SCHED_RR,
`Bad cluster.schedulingPolicy: ${schedulingPolicy}`,
);
process.nextTick(setupSettingsNT, settings);
process.on("internalMessage", (message) => {
if (message.cmd !== "NODE_DEBUG_ENABLED") return;
for (const worker of Object.values(cluster.workers)) {
if (worker.state === "online" || worker.state === "listening") {
process._debugProcess(worker.process.pid);
} else {
worker.once("online", function () {
process._debugProcess(this.process.pid);
});
}
}
});
};
// Deprecated alias must be same as setupPrimary
cluster.setupMaster = cluster.setupPrimary;
function setupSettingsNT(settings) {
cluster.emit("setup", settings);
}
function createWorkerProcess(id, env) {
const workerEnv = { ...process.env, ...env, NODE_UNIQUE_ID: `${id}` };
const execArgv = [...cluster.settings.execArgv];
if (cluster.settings.inspectPort === null) {
throw new ERR_SOCKET_BAD_PORT("Port", null, true);
}
if (isUsingInspector(cluster.settings.execArgv)) {
Array.prototype.push.call(
execArgv,
`--inspect-port=${getInspectPort(cluster.settings.inspectPort)}`,
);
}
return fork(cluster.settings.exec, cluster.settings.args, {
cwd: cluster.settings.cwd,
env: workerEnv,
serialization: cluster.settings.serialization,
silent: cluster.settings.silent,
windowsHide: cluster.settings.windowsHide,
execArgv: execArgv,
stdio: cluster.settings.stdio,
gid: cluster.settings.gid,
uid: cluster.settings.uid,
});
}
function removeWorker(worker) {
assert(worker);
delete cluster.workers[worker.id];
if (Object.keys(cluster.workers).length === 0) {
assert(handles.size === 0, "Resource leak detected.");
intercom.emit("disconnect");
}
}
function removeHandlesForWorker(worker) {
assert(worker);
for (const { 0: key, 1: handle } of handles) {
if (handle.remove(worker)) handles.delete(key);
}
}
cluster.fork = function (env) {
cluster.setupPrimary();
const id = ++ids;
const workerProcess = createWorkerProcess(id, env);
const worker = new Worker({
id: id,
process: workerProcess,
});
worker.on("message", function (message, handle) {
cluster.emit("message", this, message, handle);
});
worker.process.once("exit", (exitCode, signalCode) => {
/*
* Remove the worker from the workers list only
* if it has disconnected, otherwise we might
* still want to access it.
*/
if (!worker.isConnected()) {
removeHandlesForWorker(worker);
removeWorker(worker);
}
worker.exitedAfterDisconnect = !!worker.exitedAfterDisconnect;
worker.state = "dead";
worker.emit("exit", exitCode, signalCode);
cluster.emit("exit", worker, exitCode, signalCode);
});
worker.process.once("disconnect", () => {
/*
* Now is a good time to remove the handles
* associated with this worker because it is
* not connected to the primary anymore.
*/
removeHandlesForWorker(worker);
/*
* Remove the worker from the workers list only
* if its process has exited. Otherwise, we might
* still want to access it.
*/
if (worker.isDead()) removeWorker(worker);
worker.exitedAfterDisconnect = !!worker.exitedAfterDisconnect;
worker.state = "disconnected";
worker.emit("disconnect");
cluster.emit("disconnect", worker);
});
worker.process.on("internalMessage", internal(worker, onmessage));
process.nextTick(emitForkNT, worker);
cluster.workers[worker.id] = worker;
return worker;
};
function emitForkNT(worker) {
cluster.emit("fork", worker);
}
cluster.disconnect = function (cb) {
const workers = Object.values(cluster.workers);
if (workers.length === 0) {
process.nextTick(() => intercom.emit("disconnect"));
} else {
for (const worker of workers) {
if (worker.isConnected()) {
worker.disconnect();
}
}
}
if (typeof cb === "function") intercom.once("disconnect", cb);
};
const methodMessageMapping = {
close,
exitedAfterDisconnect,
listening,
online,
queryServer,
};
function onmessage(message, handle) {
const worker = this;
const fn = methodMessageMapping[message.act];
if (typeof fn === "function") fn(worker, message);
}
function online(worker) {
worker.state = "online";
worker.emit("online");
cluster.emit("online", worker);
}
function exitedAfterDisconnect(worker, message) {
worker.exitedAfterDisconnect = true;
send(worker, { ack: message.seq });
}
function queryServer(worker, message) {
// Stop processing if worker already disconnecting
if (worker.exitedAfterDisconnect) return;
const key =
`${message.address}:${message.port}:${message.addressType}:` +
`${message.fd}:${message.index}`;
let handle = handles.get(key);
if (handle === undefined) {
let address = message.address;
// Find shortest path for unix sockets because of the ~100 byte limit
if (
message.port < 0 &&
typeof address === "string" &&
process.platform !== "win32"
) {
address = path.relative(process.cwd(), address);
if (message.address.length < address.length) address = message.address;
}
// UDP is exempt from round-robin connection balancing for what should
// be obvious reasons: it's connectionless. There is nothing to send to
// the workers except raw datagrams and that's pointless.
if (
schedulingPolicy !== SCHED_RR ||
message.addressType === "udp4" ||
message.addressType === "udp6"
) {
handle = new SharedHandle(key, address, message);
} else {
handle = new RoundRobinHandle(key, address, message);
}
handles.set(key, handle);
}
if (!handle.data) handle.data = message.data;
// Set custom server data
handle.add(worker, (errno, reply, handle) => {
const { data } = handles.get(key);
if (errno) handles.delete(key); // Gives other workers a chance to retry.
send(
worker,
{
errno,
key,
ack: message.seq,
data,
...reply,
},
handle,
);
});
}
function listening(worker, message) {
const info = {
addressType: message.addressType,
address: message.address,
port: message.port,
fd: message.fd,
};
worker.state = "listening";
worker.emit("listening", info);
cluster.emit("listening", worker, info);
}
// Server in worker is closing, remove from list. The handle may have been
// removed by a prior call to removeHandlesForWorker() so guard against that.
function close(worker, message) {
const key = message.key;
const handle = handles.get(key);
if (handle && handle.remove(worker)) handles.delete(key);
}
function send(worker, message, handle, cb) {
return sendHelper(worker.process, message, handle, cb);
}
// Extend generic Worker with methods specific to the primary process.
Worker.prototype.disconnect = function () {
this.exitedAfterDisconnect = true;
send(this, { act: "disconnect" });
removeHandlesForWorker(this);
removeWorker(this);
return this;
};
Worker.prototype.destroy = function (signo) {
const signal = signo || "SIGTERM";
this.process.kill(signal);
};