UNPKG

@kitmi/jacaranda

Version:

JavaScript application framework

212 lines (211 loc) 7.21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { WorkerPool: function() { return WorkerPool; }, /** * Thread pool to run tasks in parallel * @module Feature_ThreadPool */ default: function() { return _default; } }); const _nodeworker_threads = require("node:worker_threads"); const _types = require("@kitmi/types"); const _Feature = /*#__PURE__*/ _interop_require_default(require("../Feature")); const _deque = /*#__PURE__*/ _interop_require_default(require("collections/deque")); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const recreateWorkerError = (sourceError)=>{ const error = new Error(sourceError.message); for (const [key, value] of Object.entries(sourceError)){ if (key !== 'message') { error[key] = value; } } return error; }; let poolIdCounter = 0; class WorkerPool { createWorker(idle) { const worker = new _nodeworker_threads.Worker(this.workerFile, this.workerOptions); worker.on('message', (message)=>{ if (message.id === '$CALLBACK') { const { task, payload } = message; const handler = this.handlers?.[task]; if (handler == null) { throw new Error(`Unknown callback task "${task}".`); } Promise.resolve(handler(payload)).catch(this.app.logError); } else { const task = this.tasks.get(message.id); if (task) { this.tasks.delete(message.id); const workerContext = this.busyWorkers.get(worker.threadId); workerContext.ongoing--; if (workerContext.ongoing === 0) { worker.unref(); this.busyWorkers.delete(worker.threadId); if (this.idleWorkers.length < this.lowThreadNum) { this.idleWorkers.push(worker); } else { worker.terminate(); } } if (message.error == null) { task.resolve(message.value); } else { task.reject(recreateWorkerError(message.error)); } } } }); worker.on('error', (error)=>{ // Any error here is effectively an equivalent of segfault, and have no scope, so we just throw it on callback level throw error; }); if (idle) { this.idleWorkers.push(worker); } return worker; } getNextWorker() { // If we have idle workers, just use them if (this.idleWorkers.length > 0) { const worker = this.idleWorkers.shift(); this.busyWorkers.set(worker.threadId, { ongoing: 1, worker }); return worker; } // If we have less than highThreadNum workers, create a new one if (this.busyWorkers.size < this.highThreadNum) { const worker = this.createWorker(); this.busyWorkers.set(worker.threadId, { ongoing: 1, worker }); return worker; } // Otherwise, just use the first worker const [workerThreadId, workerContext] = this.busyWorkers.entries().next().value; workerContext.ongoing++; this.busyWorkers.delete(workerThreadId); this.busyWorkers.set(workerThreadId, workerContext); return workerContext.worker; } async runTask_(task, payload, transferList) { return new Promise((resolve, reject)=>{ const taskId = this.taskIdCounter++; this.tasks.set(taskId, { resolve, reject }); const worker = this.getNextWorker(); worker.ref(); worker.postMessage({ id: taskId, task, payload }, transferList); }); } setCallbackHandlers(handlers) { this.handlers = { ...this.handlers, ...handlers }; } constructor(app, options){ const { name, workerFile, lowThreadNum, highThreadNum, workerOptions } = options; this.app = app; this.workerFile = workerFile; this.lowThreadNum = lowThreadNum; this.highThreadNum = highThreadNum; this.workerOptions = workerOptions; this.poolId = poolIdCounter++; this.name = name ?? `${this.app.name}_tp${this.poolId}`; this.taskIdCounter = 0; this.tasks = new Map(); this.idleWorkers = new _deque.default(); this.busyWorkers = new Map(); if (lowThreadNum > 0) { for(let i = 0; i < lowThreadNum; i++){ this.createWorker(true); } } } } const _default = { /** * This feature is loaded at service stage * @member {string} */ stage: _Feature.default.SERVICE, groupable: true, packages: [ 'collections' ], /** * Load the feature * @param {App} app - The app module object * @param {object} [options] - Options for the feature * @returns {Promise.<void>} * */ load_: async function(app, options, name) { options = app.featureConfig(options, { schema: { name: { type: 'text', optional: true }, workerFile: { type: 'text' }, workerOptions: { type: 'object', optional: true }, lowThreadNum: { type: 'integer', default: 0 }, highThreadNum: { type: 'integer', default: 1 } }, keepUnsanitized: true }, name); const { lowThreadNum, highThreadNum } = options; if (highThreadNum < lowThreadNum) { throw new _types.InvalidConfiguration('"highThreadNum" must be greater than or equal to "lowThreadNum".', app, `${name}.highThreadNum`); } if (highThreadNum === 0) { throw new _types.InvalidConfiguration('"highThreadNum" must be greater than 0.', app, `${name}.highThreadNum`); } const pool = new WorkerPool(app, options); app.on('stopping', async ()=>{ for (const worker of pool.idleWorkers){ worker.terminate(); } for (const { worker } of pool.busyWorkers.values()){ worker.terminate(); } }); app.registerService(name, pool); } }; //# sourceMappingURL=threadPool.js.map