node-concurrent-worker-tasks
Version:
This library provides a solution for managing concurrent tasks in a Node.js environment. It leverages a combination of worker threads organized into a pool and a task queue system to efficiently execute tasks, ensuring optimal utilization of system resour
197 lines (171 loc) • 7.46 kB
JavaScript
const { Worker, workerData, resourceLimits } = require('worker_threads');
const CustomError = require('./types/customError');
const os = require('os');
const totalMem = os.totalmem(); // Total RAM in bytes
const formatBytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(2)} MB`;
class TaskManager {
constructor(idleThreshold = 100) {
this.lastTaskTime = null;
this.idleThreshold = idleThreshold;
}
canExecuteTask() {
const now = Date.now();
if (this.lastTaskTime === null) {
this.lastTaskTime = now;
return true;
}
// Check if enough time has passed since last task
const timeSinceLastTask = now - this.lastTaskTime;
const canExecute = timeSinceLastTask >= this.idleThreshold;
if (canExecute) {
this.lastTaskTime = now;
}
return canExecute;
}
}
// Usage example
const taskManager = new TaskManager(); // 3 seconds idle threshold by default
class WorkerPool {
/** Creates a new WorkerPool instance.
* @param {number} poolSize - The size of the worker pool.
* @param {string} workerFilePath - The file path of the worker script.
* @param {number} maxWorkers - The maximum number of workers allowed in the pool.
*/
constructor(poolSize, workerFilePath, returnLog=true, minPercentage=20) {
process.on('exit', () => {
this.terminateAllWorkers();
});
process.on('SIGINT', () => {
process.exit(0);
});
this.workerIndex = 0
/** @type {number} */ this.poolSize = poolSize;
/** @type {number} */ this.minPercentage = minPercentage
/** @type {number} */ this.maxWorkers = 10;
/** @type {number} */ this.maxListener = 11;
/** @type {boolean} */ this.returnLog = returnLog;
/** @type {Array<WorkerObject>} */ this.pool = [];
/** @type {string} */ this.workerFilePath = workerFilePath;
try {
this.buildPool();
} catch (error) {
throw new CustomError(error.message, 503);
}
}
deleteWorkerFromPool = () => {
if (this.pool.length >= this.maxWorkers ) {
const { id, worker } = this.pool.pop();
worker.terminate();
console.log(`delete ${id} - length ${this.pool.length}`)
}
};
nextWorker = () => {
return new Promise((resolve) => {
this.workerIndex++;
if (this.workerIndex >= this.pool.length) this.workerIndex = 0 //Start at the beginning
resolve();
});
};
run = async (task) => {//Main Entry
if (this.pool.length === 0) await this.addWorkerToPool()
const workerFromPool = this.pool[this.workerIndex]; // Take last worker from pool
const { id, worker } = workerFromPool;
try {
const freeMemPercentage = await this.checkMemCapacity()
const messageListenerCount = worker.listenerCount('message');
if (messageListenerCount >= 5) {
await this.nextWorker()
}
const result = await this.executeWorker(worker, task, id)
// console.log('Message Listeners:', messageListenerCount);
// console.log(`using worker ${id}`)
if (this.returnLog) return { result, log: this.createLog(id, freeMemPercentage)};
return { result };
} catch (error) {
const { status, message } = error
throw new CustomError(message || 'something went wrong', status || 503);
}finally{
//this.pool.push(workerFromPool);
//console.log("put back",this.pool.length)
//this.deleteWorkerFromPool()
}
//this.terminateExcessWorkers();
};
checkMemCapacity = async () => {//throw new CustomError('Catch Server capacity is low. Please try again later.', 503);
return new Promise((resolve) => {
const freeMem = os.freemem(); // Free memory in bytes
const freeMemPercentage = parseInt(((freeMem / totalMem) * 100).toFixed(1));
//console.log(`${freeMemPercentage} % free capacity`)
if (freeMemPercentage <= this.minPercentage ) {
throw new CustomError('Catch Server capacity is low. Please try again later.', 503);
}
resolve(freeMemPercentage)
})
};
buildPool = async () => {
try {
for (let i = 0; i < this.poolSize; i++) {
await this.addWorkerToPool()
}
} catch (error) {
throw new CustomError('buildPool failed. Please try again later.', 503);
}
};
addWorkerToPool = async () => {
return new Promise((resolve, reject) => {
try {
const newWorker = new Worker(this.workerFilePath, { workerData });
const workerId = `-${this.pool.length + 1}-`;
newWorker.setMaxListeners(this.maxListener); // Set a max listener limit
newWorker.once('online', () => {
const workerItem = { id: workerId, worker: newWorker };
this.pool.push(workerItem);
resolve(workerItem);
});
newWorker.on('error', (error) => {//add errorlistener
reject(new CustomError(error.message, 300));
});
} catch (error) {
reject(new CustomError(error.message, 300));
}
});
};
executeWorker = async (worker, task, id) => {
return new Promise((resolve, reject) => {
const messageListener = (data) => {
worker.removeListener('message', messageListener); // Remove only this task's listener
if (worker.listenerCount('message') >= worker.getMaxListeners()) {
console.warn(`Worker-${id} exceeded max listeners! Cleaning...`);
worker.removeAllListeners('message'); // Remove all old listeners
}
resolve(data);
};
worker.once('message', messageListener); // Attach safely
try {
worker.postMessage(task);
} catch (err) {
worker.removeListener('message', messageListener); // Ensure cleanup on failure
reject(new Error(`Worker-${id} failed to post task: ${err.message || err}`));
}
});
};
createLog(freeWorker_id, freeMemPercentage) {
const mem = process.memoryUsage();
return {
worker: freeWorker_id,
poolLength: `${this.pool.length} worker`,
RSS: `${(mem.rss / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(2)} MB`,
freeMemPercentage
};
};
terminateAllWorkers() {
while (this.pool.length > 0) {
const { id, worker } = this.pool.pop();
// Log the number of listeners before termination
//console.log(`Worker ${id} has ${worker.listenerCount('message')} message listeners and ${worker.listenerCount('error')} error listeners before termination.`);
worker.terminate();
}
};
}
module.exports = WorkerPool;