281 lines
6.3 KiB
JavaScript
Executable file
281 lines
6.3 KiB
JavaScript
Executable file
const {EventEmitter} = require('events');
|
|
const os = require('os');
|
|
const errorUtils = require('./errorUtils');
|
|
const Worker = require('./Worker');
|
|
|
|
let shared = null;
|
|
class WorkerFarm extends EventEmitter {
|
|
constructor(options, farmOptions = {}) {
|
|
super();
|
|
this.options = Object.assign(
|
|
{
|
|
maxConcurrentWorkers: WorkerFarm.getNumWorkers(),
|
|
maxConcurrentCallsPerWorker: 10,
|
|
forcedKillTime: 100,
|
|
warmWorkers: true,
|
|
useLocalWorker: true,
|
|
workerPath: '../worker'
|
|
},
|
|
farmOptions
|
|
);
|
|
|
|
this.started = false;
|
|
this.warmWorkers = 0;
|
|
this.children = new Map();
|
|
this.callQueue = [];
|
|
|
|
this.localWorker = require(this.options.workerPath);
|
|
this.run = this.mkhandle('run');
|
|
|
|
this.init(options);
|
|
}
|
|
|
|
warmupWorker(method, args) {
|
|
// Workers have started, but are not warmed up yet.
|
|
// Send the job to a remote worker in the background,
|
|
// but use the result from the local worker - it will be faster.
|
|
if (this.started) {
|
|
let promise = this.addCall(method, [...args, true]);
|
|
if (promise) {
|
|
promise
|
|
.then(() => {
|
|
this.warmWorkers++;
|
|
if (this.warmWorkers >= this.children.size) {
|
|
this.emit('warmedup');
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
mkhandle(method) {
|
|
return function(...args) {
|
|
// Child process workers are slow to start (~600ms).
|
|
// While we're waiting, just run on the main thread.
|
|
// This significantly speeds up startup time.
|
|
if (this.shouldUseRemoteWorkers()) {
|
|
return this.addCall(method, [...args, false]);
|
|
} else {
|
|
if (this.options.warmWorkers) {
|
|
this.warmupWorker(method, args);
|
|
}
|
|
|
|
return this.localWorker[method](...args, false);
|
|
}
|
|
}.bind(this);
|
|
}
|
|
|
|
onError(error, childId) {
|
|
// Handle ipc errors
|
|
if (error.code === 'ERR_IPC_CHANNEL_CLOSED') {
|
|
return this.stopChild(childId);
|
|
}
|
|
}
|
|
|
|
onExit(childId) {
|
|
// delay this to give any sends a chance to finish
|
|
setTimeout(() => {
|
|
let doQueue = false;
|
|
let child = this.children.get(childId);
|
|
if (child && child.calls.size) {
|
|
for (let call of child.calls.values()) {
|
|
call.retries++;
|
|
this.callQueue.unshift(call);
|
|
doQueue = true;
|
|
}
|
|
}
|
|
this.stopChild(childId);
|
|
if (doQueue) {
|
|
this.processQueue();
|
|
}
|
|
}, 10);
|
|
}
|
|
|
|
startChild() {
|
|
let worker = new Worker(this.options.workerPath, this.options);
|
|
|
|
worker.on('request', data => {
|
|
this.processRequest(data, worker);
|
|
});
|
|
|
|
worker.on('response', () => {
|
|
// allow any outstanding calls to be processed
|
|
this.processQueue();
|
|
});
|
|
|
|
worker.once('exit', () => {
|
|
this.onExit(worker.id);
|
|
});
|
|
|
|
worker.on('error', err => {
|
|
this.onError(err, worker.id);
|
|
});
|
|
|
|
this.children.set(worker.id, worker);
|
|
}
|
|
|
|
stopChild(childId) {
|
|
let child = this.children.get(childId);
|
|
if (child) {
|
|
child.stop();
|
|
this.children.delete(childId);
|
|
}
|
|
}
|
|
|
|
async processQueue() {
|
|
if (this.ending || !this.callQueue.length) return;
|
|
|
|
if (this.children.size < this.options.maxConcurrentWorkers) {
|
|
this.startChild();
|
|
}
|
|
|
|
for (let child of this.children.values()) {
|
|
if (!this.callQueue.length) {
|
|
break;
|
|
}
|
|
|
|
if (child.calls.size < this.options.maxConcurrentCallsPerWorker) {
|
|
child.call(this.callQueue.shift());
|
|
}
|
|
}
|
|
}
|
|
|
|
async processRequest(data, child = false) {
|
|
let result = {
|
|
idx: data.idx,
|
|
type: 'response'
|
|
};
|
|
|
|
let method = data.method;
|
|
let args = data.args;
|
|
let location = data.location;
|
|
let awaitResponse = data.awaitResponse;
|
|
|
|
if (!location) {
|
|
throw new Error('Unknown request');
|
|
}
|
|
|
|
const mod = require(location);
|
|
try {
|
|
let func;
|
|
if (method) {
|
|
func = mod[method];
|
|
} else {
|
|
func = mod;
|
|
}
|
|
result.contentType = 'data';
|
|
result.content = await func(...args);
|
|
} catch (e) {
|
|
result.contentType = 'error';
|
|
result.content = errorUtils.errorToJson(e);
|
|
}
|
|
|
|
if (awaitResponse) {
|
|
if (child) {
|
|
child.send(result);
|
|
} else {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
addCall(method, args) {
|
|
if (this.ending) return; // don't add anything new to the queue
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.callQueue.push({
|
|
method,
|
|
args: args,
|
|
retries: 0,
|
|
resolve,
|
|
reject
|
|
});
|
|
this.processQueue();
|
|
});
|
|
}
|
|
|
|
async end() {
|
|
this.ending = true;
|
|
for (let childId of this.children.keys()) {
|
|
this.stopChild(childId);
|
|
}
|
|
this.ending = false;
|
|
shared = null;
|
|
}
|
|
|
|
init(options) {
|
|
this.localWorker.init(options, true);
|
|
this.initRemoteWorkers(options);
|
|
}
|
|
|
|
async initRemoteWorkers(options) {
|
|
this.started = false;
|
|
this.warmWorkers = 0;
|
|
|
|
// Start workers if there isn't enough workers already
|
|
for (
|
|
let i = this.children.size;
|
|
i < this.options.maxConcurrentWorkers;
|
|
i++
|
|
) {
|
|
this.startChild();
|
|
}
|
|
|
|
// Reliable way of initialising workers
|
|
let promises = [];
|
|
for (let child of this.children.values()) {
|
|
promises.push(
|
|
new Promise((resolve, reject) => {
|
|
child.call({
|
|
method: 'init',
|
|
args: [options],
|
|
retries: 0,
|
|
resolve,
|
|
reject
|
|
});
|
|
})
|
|
);
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
if (this.options.maxConcurrentWorkers > 0) {
|
|
this.started = true;
|
|
this.emit('started');
|
|
}
|
|
}
|
|
|
|
shouldUseRemoteWorkers() {
|
|
return (
|
|
!this.options.useLocalWorker ||
|
|
(this.started &&
|
|
(this.warmWorkers >= this.children.size || !this.options.warmWorkers))
|
|
);
|
|
}
|
|
|
|
static getShared(options) {
|
|
if (!shared) {
|
|
shared = new WorkerFarm(options);
|
|
} else if (options) {
|
|
shared.init(options);
|
|
}
|
|
|
|
return shared;
|
|
}
|
|
|
|
static getNumWorkers() {
|
|
if (process.env.PARCEL_WORKERS) {
|
|
return parseInt(process.env.PARCEL_WORKERS, 10);
|
|
}
|
|
|
|
let cores;
|
|
try {
|
|
cores = require('physical-cpu-count');
|
|
} catch (err) {
|
|
cores = os.cpus().length;
|
|
}
|
|
return cores || 1;
|
|
}
|
|
}
|
|
|
|
module.exports = WorkerFarm;
|