191 lines
7.4 KiB
JavaScript
191 lines
7.4 KiB
JavaScript
/**
|
|
* Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*
|
|
*/
|
|
|
|
'use strict';Object.defineProperty(exports, "__esModule", { value: true });var _child_process;
|
|
|
|
function _load_child_process() {return _child_process = _interopRequireDefault(require('child_process'));}var _types;
|
|
|
|
function _load_types() {return _types = require('./types');}function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj };}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This class wraps the child process and provides a nice interface to
|
|
* communicate with. It takes care of:
|
|
*
|
|
* - Re-spawning the process if it dies.
|
|
* - Queues calls while the worker is busy.
|
|
* - Re-sends the requests if the worker blew up.
|
|
*
|
|
* The reason for queueing them here (since childProcess.send also has an
|
|
* internal queue) is because the worker could be doing asynchronous work, and
|
|
* this would lead to the child process to read its receiving buffer and start a
|
|
* second call. By queueing calls here, we don't send the next call to the
|
|
* children until we receive the result of the previous one.
|
|
*
|
|
* As soon as a request starts to be processed by a worker, its "processed"
|
|
* field is changed to "true", so that other workers which might encounter the
|
|
* same call skip it.
|
|
*/exports.default =
|
|
class {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(options) {
|
|
this._options = options;
|
|
this._queue = [];
|
|
|
|
this._initialize();
|
|
}
|
|
|
|
getStdout() {
|
|
return this._child.stdout;
|
|
}
|
|
|
|
getStderr() {
|
|
return this._child.stderr;
|
|
}
|
|
|
|
send(request, callback) {
|
|
this._queue.push({ callback, request });
|
|
this._process();
|
|
}
|
|
|
|
_initialize() {
|
|
const child = (_child_process || _load_child_process()).default.fork(
|
|
require.resolve('./child'),
|
|
// $FlowFixMe: Flow does not work well with Object.assign.
|
|
Object.assign(
|
|
{
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
// suppress --debug / --inspect flags while preserving others (like --harmony)
|
|
execArgv: process.execArgv.filter(v => !/^--(debug|inspect)/.test(v)),
|
|
silent: true },
|
|
|
|
this._options.forkOptions));
|
|
|
|
|
|
|
|
child.on('message', this._receive.bind(this));
|
|
child.on('exit', this._exit.bind(this));
|
|
|
|
// $FlowFixMe: wrong "ChildProcess.send" signature.
|
|
child.send([(_types || _load_types()).CHILD_MESSAGE_INITIALIZE, false, this._options.workerPath]);
|
|
|
|
this._retries++;
|
|
this._child = child;
|
|
this._busy = false;
|
|
|
|
// If we exceeded the amount of retries, we will emulate an error reply
|
|
// coming from the child. This avoids code duplication related with cleaning
|
|
// the queue, and scheduling the next call.
|
|
if (this._retries > this._options.maxRetries) {
|
|
const error = new Error('Call retries were exceeded');
|
|
|
|
this._receive([(_types || _load_types()).PARENT_MESSAGE_ERROR,
|
|
|
|
error.name,
|
|
error.message,
|
|
error.stack,
|
|
{ type: 'WorkerError' }]);
|
|
|
|
}
|
|
}
|
|
|
|
_process() {
|
|
if (this._busy) {
|
|
return;
|
|
}
|
|
|
|
const queue = this._queue;
|
|
let skip = 0;
|
|
|
|
// Calls in the queue might have already been processed by another worker,
|
|
// so we have to skip them.
|
|
while (queue.length > skip && queue[skip].request[1]) {
|
|
skip++;
|
|
}
|
|
|
|
// Remove all pieces at once.
|
|
queue.splice(0, skip);
|
|
|
|
if (queue.length) {
|
|
const call = queue[0];
|
|
|
|
// Flag the call as processed, so that other workers know that they don't
|
|
// have to process it as well.
|
|
call.request[1] = true;
|
|
|
|
this._retries = 0;
|
|
this._busy = true;
|
|
|
|
// $FlowFixMe: wrong "ChildProcess.send" signature.
|
|
this._child.send(call.request);
|
|
}
|
|
}
|
|
|
|
_receive(response /* Should be ParentMessage */) {
|
|
const callback = this._queue[0].callback;
|
|
|
|
this._busy = false;
|
|
this._process();
|
|
|
|
switch (response[0]) {
|
|
case (_types || _load_types()).PARENT_MESSAGE_OK:
|
|
callback.call(this, null, response[1]);
|
|
break;
|
|
|
|
case (_types || _load_types()).PARENT_MESSAGE_ERROR:
|
|
let error = response[4];
|
|
|
|
if (error != null && typeof error === 'object') {
|
|
const extra = error;
|
|
const NativeCtor = global[response[1]];
|
|
const Ctor = typeof NativeCtor === 'function' ? NativeCtor : Error;
|
|
|
|
error = new Ctor(response[2]);
|
|
// $FlowFixMe: adding custom properties to errors.
|
|
error.type = response[1];
|
|
error.stack = response[3];
|
|
|
|
for (const key in extra) {
|
|
// $FlowFixMe: adding custom properties to errors.
|
|
error[key] = extra[key];
|
|
}
|
|
}
|
|
|
|
callback.call(this, error, null);
|
|
break;
|
|
|
|
default:
|
|
throw new TypeError('Unexpected response from worker: ' + response[0]);}
|
|
|
|
}
|
|
|
|
_exit(exitCode) {
|
|
if (exitCode !== 0) {
|
|
this._initialize();
|
|
}
|
|
}}; |