nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
297 lines (243 loc) • 8.13 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/cluster/child.js
import * as assert from "nstdlib/lib/internal/assert";
import * as path from "nstdlib/lib/path";
import * as EventEmitter from "nstdlib/lib/events";
import { symbols as __symbols__ } from "nstdlib/lib/internal/async_hooks";
import Worker from "nstdlib/lib/internal/cluster/worker";
import { internal, sendHelper } from "nstdlib/lib/internal/cluster/utils";
import { exitCodes as __exitCodes__ } from "nstdlib/stub/binding/errors";
import { TIMEOUT_MAX } from "nstdlib/lib/internal/timers";
import { setInterval, clearInterval } from "nstdlib/lib/timers";
const { owner_symbol } = __symbols__;
const { kNoFailure } = __exitCodes__;
const cluster = new EventEmitter();
const handles = new Map();
const indexes = new Map();
const noop = Function.prototype;
export default cluster;
cluster.isWorker = true;
cluster.isMaster = false; // Deprecated alias. Must be same as isPrimary.
cluster.isPrimary = false;
cluster.worker = null;
cluster.Worker = Worker;
cluster._setupWorker = function () {
const worker = new Worker({
id: +process.env.NODE_UNIQUE_ID | 0,
process: process,
state: "online",
});
cluster.worker = worker;
process.once("disconnect", () => {
worker.emit("disconnect");
if (!worker.exitedAfterDisconnect) {
// Unexpected disconnect, primary exited, or some such nastiness, so
// worker exits immediately.
process.exit(kNoFailure);
}
});
process.on("internalMessage", internal(worker, onmessage));
send({ act: "online" });
function onmessage(message, handle) {
if (message.act === "newconn") onconnection(message, handle);
else if (message.act === "disconnect")
ReflectApply(_disconnect, worker, [true]);
}
};
// `obj` is a net#Server or a dgram#Socket object.
cluster._getServer = function (obj, options, cb) {
let address = options.address;
// Resolve unix socket paths to absolute paths
if (
options.port < 0 &&
typeof address === "string" &&
process.platform !== "win32"
)
address = path.resolve(address);
const indexesKey = Array.prototype.join.call(
[address, options.port, options.addressType, options.fd],
":",
);
let indexSet = indexes.get(indexesKey);
if (indexSet === undefined) {
indexSet = { nextIndex: 0, set: new Set() };
indexes.set(indexesKey, indexSet);
}
const index = indexSet.nextIndex++;
indexSet.set.add(index);
const message = {
act: "queryServer",
index,
data: null,
...options,
};
message.address = address;
// Set custom data on handle (i.e. tls tickets key)
if (obj._getServerData) message.data = obj._getServerData();
send(message, (reply, handle) => {
if (typeof obj._setServerData === "function")
obj._setServerData(reply.data);
if (handle) {
// Shared listen socket
shared(reply, { handle, indexesKey, index }, cb);
} else {
// Round-robin.
rr(reply, { indexesKey, index }, cb);
}
});
obj.once("listening", () => {
// short-lived sockets might have been closed
if (!indexes.has(indexesKey)) {
return;
}
cluster.worker.state = "listening";
const address = obj.address();
message.act = "listening";
message.port = (address && address.port) || options.port;
send(message);
});
};
function removeIndexesKey(indexesKey, index) {
const indexSet = indexes.get(indexesKey);
if (!indexSet) {
return;
}
indexSet.set.delete(index);
if (indexSet.set.size === 0) {
indexes.delete(indexesKey);
}
}
// Shared listen socket.
function shared(message, { handle, indexesKey, index }, cb) {
const key = message.key;
// Monkey-patch the close() method so we can keep track of when it's
// closed. Avoids resource leaks when the handle is short-lived.
const close = handle.close;
handle.close = function () {
send({ act: "close", key });
handles.delete(key);
removeIndexesKey(indexesKey, index);
return ReflectApply(close, handle, arguments);
};
assert(handles.has(key) === false);
handles.set(key, handle);
cb(message.errno, handle);
}
// Round-robin. Master distributes handles across workers.
function rr(message, { indexesKey, index }, cb) {
if (message.errno) return cb(message.errno, null);
let key = message.key;
let fakeHandle = null;
function ref() {
if (!fakeHandle) {
fakeHandle = setInterval(noop, TIMEOUT_MAX);
}
}
function unref() {
if (fakeHandle) {
clearInterval(fakeHandle);
fakeHandle = null;
}
}
function listen(backlog) {
// TODO(bnoordhuis) Send a message to the primary that tells it to
// update the backlog size. The actual backlog should probably be
// the largest requested size by any worker.
return 0;
}
function close() {
// lib/net.js treats server._handle.close() as effectively synchronous.
// That means there is a time window between the call to close() and
// the ack by the primary process in which we can still receive handles.
// onconnection() below handles that by sending those handles back to
// the primary.
if (key === undefined) return;
unref();
// If the handle is the last handle in process,
// the parent process will delete the handle when worker process exits.
// So it is ok if the close message get lost.
// See the comments of https://github.com/nodejs/node/pull/46161
send({ act: "close", key });
handles.delete(key);
removeIndexesKey(indexesKey, index);
key = undefined;
}
function getsockname(out) {
if (key) Object.assign(out, message.sockname);
return 0;
}
// Faux handle. net.Server is not associated with handle,
// so we control its state(ref or unref) by setInterval.
const handle = { close, listen, ref, unref };
handle.ref();
if (message.sockname) {
handle.getsockname = getsockname; // TCP handles only.
}
assert(handles.has(key) === false);
handles.set(key, handle);
cb(0, handle);
}
// Round-robin connection.
function onconnection(message, handle) {
const key = message.key;
const server = handles.get(key);
let accepted = server !== undefined;
if (accepted && server[owner_symbol]) {
const self = server[owner_symbol];
if (
self.maxConnections != null &&
self._connections >= self.maxConnections
) {
accepted = false;
}
}
send({ ack: message.seq, accepted });
if (accepted) server.onconnection(0, handle);
else handle.close();
}
function send(message, cb) {
return sendHelper(process, message, null, cb);
}
function _disconnect(primaryInitiated) {
this.exitedAfterDisconnect = true;
let waitingCount = 1;
function checkWaitingCount() {
waitingCount--;
if (waitingCount === 0) {
// If disconnect is worker initiated, wait for ack to be sure
// exitedAfterDisconnect is properly set in the primary, otherwise, if
// it's primary initiated there's no need to send the
// exitedAfterDisconnect message
if (primaryInitiated) {
process.disconnect();
} else {
send({ act: "exitedAfterDisconnect" }, () => process.disconnect());
}
}
}
for (const handle of handles.values()) {
waitingCount++;
if (handle[owner_symbol]) handle[owner_symbol].close(checkWaitingCount);
else handle.close(checkWaitingCount);
}
handles.clear();
checkWaitingCount();
}
// Extend generic Worker with methods specific to worker processes.
Worker.prototype.disconnect = function () {
if (this.state !== "disconnecting" && this.state !== "destroying") {
this.state = "disconnecting";
ReflectApply(_disconnect, this, []);
}
return this;
};
Worker.prototype.destroy = function () {
if (this.state === "destroying") return;
this.exitedAfterDisconnect = true;
if (!this.isConnected()) {
process.exit(kNoFailure);
} else {
this.state = "destroying";
send({ act: "exitedAfterDisconnect" }, () => process.disconnect());
process.once("disconnect", () => process.exit(kNoFailure));
}
};