webgme-engine
Version:
WebGME server and Client API without a GUI
341 lines (296 loc) • 13.1 kB
JavaScript
/*eslint-env node*/
/**
* @module Server:ServerWorkerManager
* @author kecso / https://github.com/kecso
*/
;
var Child = require('child_process'),
Q = require('q'),
path = require('path'),
CONSTANTS = require('./constants'),
WorkerManagerBase = require('./WorkerManagerBase'),
SIMPLE_WORKER_JS = path.join(__dirname, 'simpleworker.js');
function ServerWorkerManager(parameters) {
var _managerId = null,
_workers = {},
_idToPid = {},
_waitingRequests = [],
debugPort = 5859,
gmeConfig = parameters.gmeConfig,
logger = parameters.logger.fork('serverworkermanager');
WorkerManagerBase.call(this, parameters);
logger.debug('SIMPLE_WORKER_JS:', SIMPLE_WORKER_JS);
//helping functions
function reserveWorker(workerType, callback) {
var debug = false,
execArgv = process.execArgv.filter(function (arg) {
if (arg.indexOf('--debug-brk') === 0) {
debug = '--debug-brk';
} else if (arg.indexOf('--debug') === 0) {
debug = '--debug';
} else if (arg.indexOf('--inspect-brk') === 0) {
debug = '--inspect-brk';
} else if (arg.indexOf('--inspect') === 0) {
debug = '--inspect';
} else {
return true;
}
return false;
});
// http://stackoverflow.com/questions/16840623/how-to-debug-node-js-child-forked-process
// For some reason --debug-brk is given here..
if (debug) {
logger.info('Main process is in debug mode [', debug, ']');
execArgv.push(debug + '=' + debugPort.toString());
logger.info('Child debug port: ' + debugPort.toString());
debugPort += 2;
}
logger.debug('execArgv for main process', process.execArgv);
logger.debug('execArgv for new child process', execArgv);
if (Object.keys(_workers || {}).length < gmeConfig.server.maxWorkers) {
var childProcess = Child.fork(SIMPLE_WORKER_JS, [], {execArgv: execArgv});
_workers[childProcess.pid] = {
childProcess: childProcess,
state: CONSTANTS.workerStates.initializing,
type: workerType,
cb: null
};
logger.debug('workerPid forked ' + childProcess.pid);
childProcess.on('message', function (msg) {
messageHandling(msg);
// FIXME: Do we really need the "initialize" in addition to "initialized"?
// FIXME: Why couldn't the worker start the initializing at spawn? (It can load the gmeConfig itself)
if (msg.type === CONSTANTS.msgTypes.initialized && typeof callback === 'function') {
callback();
callback = null;
}
});
childProcess.on('exit', function (code, signal) {
logger.debug('worker has exited: ' + childProcess.pid);
// When killing child-process the code is undefined and the signal SIGINT.
if (code !== 0 && signal !== 'SIGINT') {
logger.warn('worker ' + childProcess.pid + ' has exited abnormally with code ' + code +
', signal', signal);
if (typeof callback === 'function') {
callback(new Error('worker ' + childProcess.pid + ' exited abnormally with code ' + code));
}
} else {
logger.debug('worker ' + childProcess.pid + ' was terminated.');
}
delete _workers[childProcess.pid];
reserveWorkerIfNecessary(workerType);
});
} else if (typeof callback === 'function') {
callback();
}
}
function freeWorker(workerPid) {
logger.debug('freeWorker', workerPid);
if (_workers[workerPid]) {
_workers[workerPid].childProcess.kill('SIGINT');
delete _workers[workerPid];
} else {
logger.warn('freeWorker - worker did not exist', workerPid);
}
}
function freeAllWorkers(callback) {
logger.debug('closing all workers');
var len = Object.keys(_workers).length,
closeHandlers = {};
logger.debug('there are ' + len + ' workers to close');
Object.keys(_workers).forEach(function (workerPid) {
var worker = _workers[workerPid];
// Clear the previously assigned handlers for the child process.
worker.childProcess.removeAllListeners('exit');
worker.childProcess.removeAllListeners('message');
// Define and store a close-handler.
closeHandlers[workerPid] = function (err) {
if (err) {
logger.error(err);
}
logger.debug('workerPid closed: ' + workerPid + ', nbr left', len - 1);
// Reset the handler since both error and close may be triggered.
closeHandlers[workerPid].closeHandler = function () {
};
len -= 1;
if (len === 0) {
callback(null);
}
};
// Assign the close handler to both error and close event.
worker.childProcess.on('error', closeHandlers[workerPid]);
worker.childProcess.on('close', closeHandlers[workerPid]);
// Send kill to child process.
worker.childProcess.kill('SIGINT');
delete _workers[workerPid];
logger.debug('request closing workerPid: ' + workerPid);
});
if (len === 0) {
callback(null);
}
}
function assignRequest(workerPid) {
var worker;
if (_waitingRequests.length > 0) {
worker = _workers[workerPid];
if (worker.state === CONSTANTS.workerStates.free && worker.type === CONSTANTS.workerTypes.simple) {
var request = _waitingRequests.shift();
logger.debug('Request will be handled, request left in queue: ', _waitingRequests.length);
logger.debug('Worker "' + workerPid + '" will handle request: ', {metadata: request});
worker.state = CONSTANTS.workerStates.working;
worker.request = request.request;
worker.cb = request.cb;
worker.resid = null;
worker.childProcess.send(request.request);
}
}
}
function messageHandling(msg) {
var worker = _workers[msg.pid],
cFunction = null;
logger.debug('Message received from worker', {metadata: msg});
if (worker) {
logger.debug('Worker will handle message', {metadata: worker});
switch (msg.type) {
case CONSTANTS.msgTypes.result:
// Response to result request, so worker can be freed
cFunction = worker.cb;
worker.cb = null;
if (worker.type === CONSTANTS.workerTypes.simple) {
worker.state = CONSTANTS.workerStates.free;
if (worker.resid) {
delete _idToPid[worker.resid];
}
worker.resid = null;
delete worker.request;
} else {
logger.error('ConnectedWorker returned result!');
freeWorker(msg.pid);
}
if (cFunction) {
cFunction(msg.error ? new Error(msg.error) : null, msg.result);
} else {
logger.warn('No callback associated with', worker.resid);
}
break;
case CONSTANTS.msgTypes.initialize:
// This arrives when the worker is ready for initialization.
worker.childProcess.send({
command: CONSTANTS.workerCommands.initialize,
gmeConfig: gmeConfig
});
break;
case CONSTANTS.msgTypes.initialized:
// The worker has been initialized and is free to received requests.
if (worker.type === CONSTANTS.workerTypes.simple) {
worker.state = CONSTANTS.workerStates.free;
}
break;
default:
logger.error(new Error('Unexpected worker msg ' + msg.type));
}
}
}
function reserveWorkerIfNecessary(workerType, callback) {
var workerIds = Object.keys(_workers || {}),
i,
initializingWorkers = 0,
freeWorkers = 0;
for (i = 0; i < workerIds.length; i++) {
if (_workers[workerIds[i]].state === CONSTANTS.workerStates.initializing) {
initializingWorkers += 1;
} else if (_workers[workerIds[i]].state === CONSTANTS.workerStates.free) {
freeWorkers += 1;
}
}
if (_waitingRequests.length + 1 /* keep a spare */ > initializingWorkers + freeWorkers &&
workerIds.length < gmeConfig.server.maxWorkers) {
reserveWorker(workerType, callback);
} else if (typeof callback === 'function') {
callback();
}
}
function queueManager() {
var i,
workerPids,
initializingWorkers = 0,
firstIdleWorker;
if (_waitingRequests.length > 0) {
workerPids = Object.keys(_workers);
i = 0;
while (i < workerPids.length && _workers[workerPids[i]].state !== CONSTANTS.workerStates.free) {
if (_workers[workerPids[i]].state === CONSTANTS.workerStates.initializing) {
initializingWorkers += 1;
}
i += 1;
}
if (i < workerPids.length) {
assignRequest(workerPids[i]);
} else if (_waitingRequests.length + 1 /* keep a spare */ > initializingWorkers &&
Object.keys(_workers || {}).length < gmeConfig.server.maxWorkers) {
reserveWorker();
}
} else {
Object.getOwnPropertyNames(_workers).forEach(function (pid) {
if (_workers[pid].state === CONSTANTS.workerStates.free) {
if (firstIdleWorker === undefined) {
firstIdleWorker = _workers[pid];
} else {
freeWorker(pid);
}
}
});
}
}
this.request = function (parameters, callback) {
logger.debug('Incoming request', {metadata: parameters});
if (gmeConfig.server.maxQueuedWorkerRequests > -1 &&
_waitingRequests.length > gmeConfig.server.maxQueuedWorkerRequests) {
logger.warn('Worker-request got dismissed because of full queue.');
callback(new Error('Server currently has too many jobs queued, try again later.'));
} else {
logger.debug('Queue not full - will add worker-request to wait list.');
_waitingRequests.push({request: parameters, cb: callback});
reserveWorkerIfNecessary(CONSTANTS.workerTypes.simple);
queueManager();
}
};
this.start = function (callback) {
if (_managerId === null) {
_managerId = setInterval(queueManager, 10);
}
return Q.nfcall(reserveWorkerIfNecessary, CONSTANTS.workerTypes.simple)
.nodeify(callback);
};
this.stop = function (callback) {
clearInterval(_managerId);
_managerId = null;
return Q.nfcall(freeAllWorkers).nodeify(callback);
};
function copyAndSanitizeRequest(request) {
var result = request;
if (request) {
result = JSON.parse(JSON.stringify(request));
delete result.webgmeToken;
}
return result;
}
this.getStatus = function (callback) {
callback(null, {
waitingRequests: _waitingRequests.map(function (request) {
return copyAndSanitizeRequest(request.request);
}),
workers: Object.keys(_workers).map(function (pid) {
return {
pid: pid,
state: _workers[pid].state,
request: copyAndSanitizeRequest(_workers[pid].request)
};
})
});
};
this.CONSTANTS = CONSTANTS;
}
ServerWorkerManager.prototype = Object.create(WorkerManagerBase.prototype);
ServerWorkerManager.prototype.constructor = ServerWorkerManager;
module.exports = ServerWorkerManager;