flow like the river

This commit is contained in:
root 2025-11-07 00:06:12 +01:00
commit 013fe673f3
42435 changed files with 5764238 additions and 0 deletions

View file

@ -0,0 +1,141 @@
const childProcess = require('child_process');
const {EventEmitter} = require('events');
const errorUtils = require('./errorUtils');
const childModule =
parseInt(process.versions.node, 10) < 8
? require.resolve('../../lib/workerfarm/child')
: require.resolve('../../src/workerfarm/child');
let WORKER_ID = 0;
class Worker extends EventEmitter {
constructor(forkModule, options) {
super();
this.options = options;
this.id = WORKER_ID++;
this.sendQueue = [];
this.processQueue = true;
this.calls = new Map();
this.exitCode = null;
this.callId = 0;
this.stopped = false;
this.fork(forkModule);
}
fork(forkModule) {
let filteredArgs = process.execArgv.filter(
v => !/^--(debug|inspect)/.test(v)
);
let options = {
execArgv: filteredArgs,
env: process.env,
cwd: process.cwd()
};
this.child = childProcess.fork(childModule, process.argv, options);
this.send({
type: 'module',
module: forkModule,
child: this.id
});
this.child.on('message', this.receive.bind(this));
this.child.once('exit', code => {
this.exitCode = code;
this.emit('exit', code);
});
this.child.on('error', err => {
this.emit('error', err);
});
}
send(data) {
if (!this.processQueue) {
return this.sendQueue.push(data);
}
let result = this.child.send(data, error => {
if (error && error instanceof Error) {
// Ignore this, the workerfarm handles child errors
return;
}
this.processQueue = true;
if (this.sendQueue.length > 0) {
let queueCopy = this.sendQueue.slice(0);
this.sendQueue = [];
queueCopy.forEach(entry => this.send(entry));
}
});
if (!result || /^win/.test(process.platform)) {
// Queue is handling too much messages throttle it
this.processQueue = false;
}
}
call(call) {
let idx = this.callId++;
this.calls.set(idx, call);
this.send({
type: 'request',
idx: idx,
child: this.id,
method: call.method,
args: call.args
});
}
receive(data) {
if (this.stopped) {
return;
}
let idx = data.idx;
let type = data.type;
let content = data.content;
let contentType = data.contentType;
if (type === 'request') {
this.emit('request', data);
} else if (type === 'response') {
let call = this.calls.get(idx);
if (!call) {
// Return for unknown calls, these might accur if a third party process uses workers
return;
}
if (contentType === 'error') {
call.reject(errorUtils.jsonToError(content));
} else {
call.resolve(content);
}
this.calls.delete(idx);
this.emit('response', data);
}
}
stop() {
this.stopped = true;
this.send('die');
setTimeout(() => {
if (this.exitCode === null) {
this.child.kill('SIGKILL');
}
}, this.options.forcedKillTime);
}
}
module.exports = Worker;

View file

@ -0,0 +1,281 @@
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;

View file

@ -0,0 +1,140 @@
const errorUtils = require('./errorUtils');
class Child {
constructor() {
this.module = undefined;
this.childId = undefined;
this.callQueue = [];
this.responseQueue = new Map();
this.responseId = 0;
this.maxConcurrentCalls = 10;
}
messageListener(data) {
if (data === 'die') {
return this.end();
}
if (data.type === 'module' && data.module && !this.module) {
this.module = require(data.module);
this.childId = data.child;
if (this.module.setChildReference) {
this.module.setChildReference(this);
}
return;
}
let type = data.type;
if (type === 'response') {
return this.handleResponse(data);
} else if (type === 'request') {
return this.handleRequest(data);
}
}
async send(data) {
process.send(data, err => {
if (err && err instanceof Error) {
if (err.code === 'ERR_IPC_CHANNEL_CLOSED') {
// IPC connection closed
// no need to keep the worker running if it can't send or receive data
return this.end();
}
}
});
}
async handleRequest(data) {
let idx = data.idx;
let child = data.child;
let method = data.method;
let args = data.args;
let result = {idx, child, type: 'response'};
try {
result.contentType = 'data';
result.content = await this.module[method](...args);
} catch (e) {
result.contentType = 'error';
result.content = errorUtils.errorToJson(e);
}
this.send(result);
}
async handleResponse(data) {
let idx = data.idx;
let contentType = data.contentType;
let content = data.content;
let call = this.responseQueue.get(idx);
if (contentType === 'error') {
call.reject(errorUtils.jsonToError(content));
} else {
call.resolve(content);
}
this.responseQueue.delete(idx);
// Process the next call
this.processQueue();
}
// Keep in mind to make sure responses to these calls are JSON.Stringify safe
async addCall(request, awaitResponse = true) {
let call = request;
call.type = 'request';
call.child = this.childId;
call.awaitResponse = awaitResponse;
let promise;
if (awaitResponse) {
promise = new Promise((resolve, reject) => {
call.resolve = resolve;
call.reject = reject;
});
}
this.callQueue.push(call);
this.processQueue();
return promise;
}
async sendRequest(call) {
let idx;
if (call.awaitResponse) {
idx = this.responseId++;
this.responseQueue.set(idx, call);
}
this.send({
idx: idx,
child: call.child,
type: call.type,
location: call.location,
method: call.method,
args: call.args,
awaitResponse: call.awaitResponse
});
}
async processQueue() {
if (!this.callQueue.length) {
return;
}
if (this.responseQueue.size < this.maxConcurrentCalls) {
this.sendRequest(this.callQueue.shift());
}
}
end() {
return process.exit(0);
}
}
let child = new Child();
process.on('message', child.messageListener.bind(child));
module.exports = child;

View file

@ -0,0 +1,23 @@
function errorToJson(error) {
let jsonError = {
message: error.message,
stack: error.stack,
name: error.name
};
// Add all custom codeFrame properties
Object.keys(error).forEach(key => {
jsonError[key] = error[key];
});
return jsonError;
}
function jsonToError(json) {
let error = new Error(json.message);
Object.keys(json).forEach(key => {
error[key] = json[key];
});
return error;
}
exports.errorToJson = errorToJson;
exports.jsonToError = jsonToError;