UNPKG

@wfh/thread-promise-pool

Version:

2 Node.js utilities: a thread worker pool and a promise queue

279 lines (243 loc) 9.01 kB
// tslint:disable no-console import {Worker, WorkerOptions} from 'worker_threads'; import {ChildProcess, fork} from 'child_process'; // import {queue} from './promise-queque'; import {Task, Command, InitialOptions} from './worker'; import {Task as ProcessTask, InitialOptions as InitialOptions4Proc} from './worker-process'; import os from 'os'; export {Task}; class PromisedTask<T> { promise: Promise<T>; resolve: Parameters<ConstructorParameters<typeof Promise>[0]>[0]; reject: Parameters<ConstructorParameters<typeof Promise>[0]>[1]; constructor(private task: Task, verbose = false) { this.promise = new Promise<T>((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } runByWorker(worker: Worker) { const onMessage = (msg: {type: 'error' | 'wait', data: T}) => { if (msg.type === 'wait') { unsubscribeWorker(); this.resolve(msg.data); } else if (msg.type === 'error') { unsubscribeWorker(); this.reject(msg.data); } }; const onExit = (code: number) => { // if (this.verbose) { // console.log('[thread-pool] PromisedTask on exit'); // } unsubscribeWorker(); if (code !== 0) { this.reject(`Thread ${worker.threadId} exist with code ` + code); } }; const unsubscribeWorker = () => { worker.off('message', onMessage); worker.off('error', onError); worker.off('messageerror', onError); worker.off('exit', onExit); }; const onError = (err: any) => { unsubscribeWorker(); this.reject(err); }; worker.on('message', onMessage); worker.on('messageerror', onError); // TODO: not sure if work will exit worker.on('error', onError); worker.on('exit', onExit); const msg = {...this.task}; delete msg.transferList; worker.postMessage(msg, msg.transferList); } } class PromisedProcessTask<T> { promise: Promise<T>; resolve: Parameters<ConstructorParameters<typeof Promise>[0]>[0]; reject: Parameters<ConstructorParameters<typeof Promise>[0]>[1]; constructor(private task: ProcessTask | InitialOptions4Proc) { this.promise = new Promise<T>((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } runByProcess(worker: ChildProcess, verbose: boolean) { const onMessage = (msg: {type: 'error' | 'wait', data: T}) => { if (msg.type === 'wait') { this.resolve(msg.data); unsubscribeWorker(); } else if (msg.type === 'error') { this.reject(msg.data); unsubscribeWorker(); } }; const onExit = (code: number) => { unsubscribeWorker(); if (code !== 0) { this.reject('Child process exist with code ' + code); } }; const unsubscribeWorker = () => { worker.off('message', onMessage); worker.off('error', onError); // worker.off('messageerror', onError); worker.off('exit', onExit); }; const onError = (err: any) => { unsubscribeWorker(); this.reject(err); }; worker.on('message', onMessage); // worker.on('messageerror', onError); // TODO: not sure if work will exit worker.on('error', onError); worker.on('exit', onExit); const msg = {...this.task, verbose}; if (!worker.send(msg)) { this.reject('Is Child process event threshold full? This is weird.'); } } } export class Pool { private runningWorkers = new Set<Worker|ChildProcess>(); /** Last in first run, always run the latest created worker, give chance for old ones to be removed after timeout */ private idleWorkers: (Worker|ChildProcess)[] = []; private idleTimers = new WeakMap<Worker | ChildProcess, ReturnType<typeof setTimeout>>(); private tasks: (PromisedTask<any> | PromisedProcessTask<any>)[] = []; private totalCreatedWorkers = 0; /** * @param maxParalle max number of paralle workers, default is `os.cpus().length - 1` * @param idleTimeMs let worker exit to release memory, after a worker being idle for some time (in ms) * @param workerOptions thread worker options, e.g. initializing some environment * stuff */ constructor(private maxParalle = os.cpus().length - 1, private idleTimeMs = 0, public workerOptions?: WorkerOptions & InitialOptions) { } submit<T>(task: Task): Promise<T> { // 1. Bind a task with a promise const promisedTask = new PromisedTask<T>(task, this.workerOptions?.verbose); if (this.workerOptions?.verbose) { console.log(`[thread-pool] submit task, idle workers: ${this.idleWorkers.length}, running workers: ${this.runningWorkers.size}`); } this.tasks.push(promisedTask); if (this.idleWorkers.length > 0) { // 2. Look for availabe idle worker const worker = this.idleWorkers.pop()!; this.runWorker(worker); } else if (this.runningWorkers.size < this.maxParalle) { // 3. Create new worker if number of them is less than maxParalle this.createWorker(promisedTask); } return promisedTask.promise; } submitProcess<T>(task: ProcessTask): Promise<T> { // 1. Bind a task with a promise const promisedTask = new PromisedProcessTask<T>(task); if (this.workerOptions?.verbose) { console.log(`[thread-pool] submit child process, idle process: ${this.idleWorkers.length}, ` + `running process or workers: ${this.runningWorkers.size}`); } this.tasks.push(promisedTask); if (this.idleWorkers.length > 0) { // 2. Look for availabe idle worker const worker = this.idleWorkers.pop()!; this.runWorker(worker); } else if (this.runningWorkers.size < this.maxParalle) { // 3. Create new worker if number of them is less than maxParalle this.createChildProcess(); } return promisedTask.promise; } private async runWorker(worker: Worker | ChildProcess) { this.idleTimers.delete(worker); this.runningWorkers.add(worker); while (this.tasks.length > 0) { const task = this.tasks.shift()!; if (worker instanceof Worker) (task as PromisedTask<any>).runByWorker(worker); else (task as PromisedProcessTask<any>).runByProcess(worker, !!this.workerOptions?.verbose); await task.promise.catch(e => {}); } // No more task, put worker in idle this.runningWorkers.delete(worker); this.idleWorkers.push(worker); // setup idle timer const timer = setTimeout(() => { const cmd: Command = {exit: true}; if (worker instanceof Worker) { worker.postMessage(cmd); if (this.workerOptions?.verbose) console.log('[thread-pool] Remove expired worker thread:', worker.threadId); } else { worker.send(cmd); if (this.workerOptions?.verbose) console.log('[thread-pool] Remove expired child process:', worker.pid); } this.idleTimers.delete(worker); }, this.idleTimeMs); this.idleTimers.set(worker, timer); } private async createChildProcess() { let worker: ChildProcess = fork(require.resolve('./worker-process'), {serialization: 'advanced', stdio: 'inherit'}); this.runningWorkers.add(worker); // if (this.workerOptions && (this.workerOptions.verbose || this.workerOptions.initializer)) { const verbose = !!this.workerOptions?.verbose; if (verbose) console.log('[thread-pool] createChildProcess'); if (this.workerOptions?.initializer) { const initTask = new PromisedProcessTask({ verbose, initializer: this.workerOptions?.initializer }); initTask.runByProcess(worker, !!this.workerOptions?.verbose); await initTask.promise; } // } this.runWorker(worker); const onWorkerExit = () => { if (this.runningWorkers.has(worker)) { this.runningWorkers.delete(worker); } else { const idx = this.idleWorkers.indexOf(worker); if (idx >= 0) { this.idleWorkers.splice(idx, 1); } } }; worker.on('error', onWorkerExit); worker.on('exit', onWorkerExit); return worker; } private createWorker(task: PromisedTask<any>) { let worker: Worker; if (this.workerOptions?.verbose) { console.log('[thread-pool] createWorker'); } worker = new Worker(require.resolve('./worker'), { ...this.workerOptions, workerData: { id: ++this.totalCreatedWorkers + '', verbose: !!this.workerOptions?.verbose, initializer: this.workerOptions?.initializer, ...this.workerOptions?.workerData || {} } }); this.runWorker(worker); const onWorkerExit = () => { if (this.runningWorkers.has(worker)) { this.runningWorkers.delete(worker); } else { const idx = this.idleWorkers.indexOf(worker); if (idx >= 0) { this.idleWorkers.splice(idx, 1); } } }; worker.on('error', onWorkerExit); worker.on('exit', onWorkerExit); return worker; } }