@kitmi/jacaranda
Version:
JavaScript application framework
212 lines (211 loc) • 7.21 kB
JavaScript
"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