batch-cluster
Version:
Manage a cluster of child processes
260 lines • 13.6 kB
JavaScript
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _ProcessPoolManager_instances, _ProcessPoolManager_procs, _ProcessPoolManager_logger, _ProcessPoolManager_healthMonitor, _ProcessPoolManager_nextSpawnTime, _ProcessPoolManager_lastPidsCheckTime, _ProcessPoolManager_spawnedProcs, _ProcessPoolManager_maybeCheckPids, _ProcessPoolManager_maxSpawnDelay, _ProcessPoolManager_procsToSpawn, _ProcessPoolManager_spawnNewProc;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProcessPoolManager = void 0;
const node_timers_1 = __importDefault(require("node:timers"));
const Array_1 = require("./Array");
const BatchProcess_1 = require("./BatchProcess");
const Error_1 = require("./Error");
const ProcessHealthMonitor_1 = require("./ProcessHealthMonitor");
const Timeout_1 = require("./Timeout");
/**
* Manages the lifecycle of a pool of BatchProcess instances.
* Handles spawning, health monitoring, and cleanup of child processes.
*/
class ProcessPoolManager {
constructor(options, emitter, onIdle) {
_ProcessPoolManager_instances.add(this);
this.options = options;
this.emitter = emitter;
this.onIdle = onIdle;
_ProcessPoolManager_procs.set(this, []);
_ProcessPoolManager_logger.set(this, void 0);
_ProcessPoolManager_healthMonitor.set(this, void 0);
_ProcessPoolManager_nextSpawnTime.set(this, 0);
_ProcessPoolManager_lastPidsCheckTime.set(this, 0);
_ProcessPoolManager_spawnedProcs.set(this, 0);
__classPrivateFieldSet(this, _ProcessPoolManager_logger, options.logger, "f");
__classPrivateFieldSet(this, _ProcessPoolManager_healthMonitor, new ProcessHealthMonitor_1.ProcessHealthMonitor(options, emitter), "f");
}
/**
* Get all current processes
*/
get processes() {
return __classPrivateFieldGet(this, _ProcessPoolManager_procs, "f");
}
/**
* Get the current number of spawned child processes
*/
get procCount() {
return __classPrivateFieldGet(this, _ProcessPoolManager_procs, "f").length;
}
/**
* Alias for procCount to match BatchCluster interface
*/
get processCount() {
return this.procCount;
}
/**
* Get the current number of child processes currently servicing tasks
*/
get busyProcCount() {
return (0, Array_1.count)(__classPrivateFieldGet(this, _ProcessPoolManager_procs, "f"),
// don't count procs that are starting up as "busy":
(ea) => !ea.starting && !ea.ending && !ea.idle);
}
/**
* Get the current number of starting processes
*/
get startingProcCount() {
return (0, Array_1.count)(__classPrivateFieldGet(this, _ProcessPoolManager_procs, "f"),
// don't count procs that are starting up as "busy":
(ea) => ea.starting && !ea.ending);
}
/**
* Get the current number of ready processes
*/
get readyProcCount() {
return (0, Array_1.count)(__classPrivateFieldGet(this, _ProcessPoolManager_procs, "f"), (ea) => ea.ready);
}
/**
* Get the total number of child processes created by this instance
*/
get spawnedProcCount() {
return __classPrivateFieldGet(this, _ProcessPoolManager_spawnedProcs, "f");
}
/**
* Get the milliseconds until the next spawn is allowed
*/
get msBeforeNextSpawn() {
return Math.max(0, __classPrivateFieldGet(this, _ProcessPoolManager_nextSpawnTime, "f") - Date.now());
}
/**
* Get all currently running tasks from all processes
*/
currentTasks() {
const tasks = [];
for (const proc of __classPrivateFieldGet(this, _ProcessPoolManager_procs, "f")) {
if (proc.currentTask != null) {
tasks.push(proc.currentTask);
}
}
return tasks;
}
/**
* Find the first ready process that can handle a new task
*/
findReadyProcess() {
return __classPrivateFieldGet(this, _ProcessPoolManager_procs, "f").find((ea) => ea.ready);
}
/**
* Verify that each BatchProcess PID is actually alive.
* @return the spawned PIDs that are still in the process table.
*/
pids() {
const arr = [];
for (const proc of [...__classPrivateFieldGet(this, _ProcessPoolManager_procs, "f")]) {
if (proc != null && proc.running()) {
arr.push(proc.pid);
}
}
return arr;
}
/**
* Shut down any currently-running child processes.
*/
async closeChildProcesses(gracefully = true) {
const procs = [...__classPrivateFieldGet(this, _ProcessPoolManager_procs, "f")];
__classPrivateFieldGet(this, _ProcessPoolManager_procs, "f").length = 0;
await Promise.all(procs.map((proc) => proc
.end(gracefully, "ending")
.catch((err) => this.emitter.emit("endError", (0, Error_1.asError)(err), proc))));
}
/**
* Run maintenance on currently spawned child processes.
* Removes unhealthy processes and enforces maxProcs limit.
*/
vacuumProcs() {
__classPrivateFieldGet(this, _ProcessPoolManager_instances, "m", _ProcessPoolManager_maybeCheckPids).call(this);
const endPromises = [];
let pidsToReap = Math.max(0, __classPrivateFieldGet(this, _ProcessPoolManager_procs, "f").length - this.options.maxProcs);
(0, Array_1.filterInPlace)(__classPrivateFieldGet(this, _ProcessPoolManager_procs, "f"), (proc) => {
var _a;
// Only check `.idle` (not `.ready`) procs. We don't want to reap busy
// procs unless we're ending, and unhealthy procs (that we want to reap)
// won't be `.ready`.
if (proc.idle) {
// don't reap more than pidsToReap pids. We can't use #procs.length
// within filterInPlace because #procs.length only changes at iteration
// completion: the prior impl resulted in all idle pids getting reaped
// when maxProcs was reduced.
const why = (_a = proc.whyNotHealthy) !== null && _a !== void 0 ? _a : (--pidsToReap >= 0 ? "tooMany" : null);
if (why != null) {
endPromises.push(proc.end(true, why));
return false;
}
proc.maybeRunHealthcheck();
}
return true;
});
return Promise.all(endPromises);
}
/**
* Spawn new processes if needed based on pending task count and capacity
*/
async maybeSpawnProcs(pendingTaskCount, ended) {
var _a;
let procsToSpawn = __classPrivateFieldGet(this, _ProcessPoolManager_instances, "m", _ProcessPoolManager_procsToSpawn).call(this, pendingTaskCount);
if (ended || __classPrivateFieldGet(this, _ProcessPoolManager_nextSpawnTime, "f") > Date.now() || procsToSpawn === 0) {
return;
}
// prevent concurrent runs:
__classPrivateFieldSet(this, _ProcessPoolManager_nextSpawnTime, Date.now() + __classPrivateFieldGet(this, _ProcessPoolManager_instances, "m", _ProcessPoolManager_maxSpawnDelay).call(this), "f");
for (let i = 0; i < procsToSpawn; i++) {
if (ended) {
break;
}
// Kick the lock down the road:
__classPrivateFieldSet(this, _ProcessPoolManager_nextSpawnTime, Date.now() + __classPrivateFieldGet(this, _ProcessPoolManager_instances, "m", _ProcessPoolManager_maxSpawnDelay).call(this), "f");
__classPrivateFieldSet(this, _ProcessPoolManager_spawnedProcs, (_a = __classPrivateFieldGet(this, _ProcessPoolManager_spawnedProcs, "f"), _a++, _a), "f");
try {
const proc = __classPrivateFieldGet(this, _ProcessPoolManager_instances, "m", _ProcessPoolManager_spawnNewProc).call(this);
const result = await (0, Timeout_1.thenOrTimeout)(proc, this.options.spawnTimeoutMillis);
if (result === Timeout_1.Timeout) {
void proc
.then((bp) => {
void bp.end(false, "startError");
this.emitter.emit("startError", (0, Error_1.asError)("Failed to spawn process in " +
this.options.spawnTimeoutMillis +
"ms"), bp);
})
.catch((err) => {
// this should only happen if the processFactory throws a
// rejection:
this.emitter.emit("startError", (0, Error_1.asError)(err));
});
}
else {
__classPrivateFieldGet(this, _ProcessPoolManager_logger, "f").call(this).debug("ProcessPoolManager.maybeSpawnProcs() started healthy child process", { pid: result.pid });
}
// tasks may have been popped off or setMaxProcs may have reduced
// maxProcs. Do this at the end so the for loop ends properly.
procsToSpawn = Math.min(__classPrivateFieldGet(this, _ProcessPoolManager_instances, "m", _ProcessPoolManager_procsToSpawn).call(this, pendingTaskCount), procsToSpawn);
}
catch (err) {
this.emitter.emit("startError", (0, Error_1.asError)(err));
}
}
// YAY WE MADE IT.
// Only let more children get spawned after minDelay:
const delay = Math.max(100, this.options.minDelayBetweenSpawnMillis);
__classPrivateFieldSet(this, _ProcessPoolManager_nextSpawnTime, Date.now() + delay, "f");
// And schedule #onIdle for that time:
node_timers_1.default.setTimeout(this.onIdle, delay).unref();
}
/**
* Update the maximum number of processes allowed
*/
setMaxProcs(maxProcs) {
this.options.maxProcs = maxProcs;
}
}
exports.ProcessPoolManager = ProcessPoolManager;
_ProcessPoolManager_procs = new WeakMap(), _ProcessPoolManager_logger = new WeakMap(), _ProcessPoolManager_healthMonitor = new WeakMap(), _ProcessPoolManager_nextSpawnTime = new WeakMap(), _ProcessPoolManager_lastPidsCheckTime = new WeakMap(), _ProcessPoolManager_spawnedProcs = new WeakMap(), _ProcessPoolManager_instances = new WeakSet(), _ProcessPoolManager_maybeCheckPids = function _ProcessPoolManager_maybeCheckPids() {
if (this.options.cleanupChildProcs &&
this.options.pidCheckIntervalMillis > 0 &&
__classPrivateFieldGet(this, _ProcessPoolManager_lastPidsCheckTime, "f") + this.options.pidCheckIntervalMillis < Date.now()) {
__classPrivateFieldSet(this, _ProcessPoolManager_lastPidsCheckTime, Date.now(), "f");
void this.pids();
}
}, _ProcessPoolManager_maxSpawnDelay = function _ProcessPoolManager_maxSpawnDelay() {
// 10s delay is certainly long enough for .spawn() to return, even on a
// loaded windows machine.
return Math.max(10000, this.options.spawnTimeoutMillis);
}, _ProcessPoolManager_procsToSpawn = function _ProcessPoolManager_procsToSpawn(pendingTaskCount) {
const remainingCapacity = this.options.maxProcs - __classPrivateFieldGet(this, _ProcessPoolManager_procs, "f").length;
// take into account starting procs, so one task doesn't result in multiple
// processes being spawned:
const requestedCapacity = pendingTaskCount - this.startingProcCount;
const atLeast0 = Math.max(0, Math.min(remainingCapacity, requestedCapacity));
return this.options.minDelayBetweenSpawnMillis === 0
? // we can spin up multiple processes in parallel.
atLeast0
: // Don't spin up more than 1:
Math.min(1, atLeast0);
}, _ProcessPoolManager_spawnNewProc =
// must only be called by this.maybeSpawnProcs()
async function _ProcessPoolManager_spawnNewProc() {
// no matter how long it takes to spawn, always push the result into #procs
// so we don't leak child processes:
const procOrPromise = this.options.processFactory();
const proc = await procOrPromise;
const result = new BatchProcess_1.BatchProcess(proc, this.options, this.onIdle, __classPrivateFieldGet(this, _ProcessPoolManager_healthMonitor, "f"));
__classPrivateFieldGet(this, _ProcessPoolManager_procs, "f").push(result);
return result;
};
//# sourceMappingURL=ProcessPoolManager.js.map