UNPKG

batch-cluster

Version:
260 lines 13.6 kB
"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