Interprocess Communication in NodeJS
@jurisicmarko
February 2017
The Problem
- NodeJS is single-threaded
- GUI stops with long-running CPU intensive background processes (1.5h in our worst case)
- Users wanted a read-only access to the system while importing/exporting
1st Idea
- Just use NodeJS Cluster
Cluster
A single instance of Node.js runs in a single thread. To take advantage of multi-core systems the user will sometimes want to launch a cluster of Node.js processes to handle the load.
The cluster module allows you to easily create child processes that all share server ports.
https://nodejs.org/api/cluster.html
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
// Fork workers.
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer((req, res) => {
console.log('process ' + process.pid +
' processing request');
res.writeHead(200);
res.end('hello world\n' + process.pid);
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
Cluster
- No guarantee which process will process the next request
- A bit better than the original single-threaded solution but still did not solve our problem
1st Idea
Just use NodeJS Cluster
2nd Approach
- Node Child Process
- Spawn / fork
Child_process
- child_process.fork() - a special case of child_process.spawn()
- used specifically to spawn new Node.js processes.
let fork = require('child_process').fork;
let child = fork('child_process.js');
Child_process
- Server sends data via command-line params
- Client uses IPC to report when done
Full example
var fork = require('child_process').fork;
const http = require('http');
http.createServer((req, res) => {
console.log('process ' + process.pid +
' processing request');
res.writeHead(200);
let child = fork('client.js',['foo','bar']);
child.on('message', (msg) => {
res.end('child process:' +
child.pid + ' responded:' + msg );
});
}).listen(8000);
let firstparam = process.argv[2];
let secondparam = process.argv[3];
let result = firstparam + secondparam;
console.log('doing complex stuff at pid',
process.pid, 'got params:', firstparam,
secondparam);
for (i = 0; i < 900000000; i++) {
let z = i*i;
}
if (process.argv[3]) {
process.send('result:' + result)
}
Server
Child
Live code
let child = fork(
path.join(__dirname, '../../standalone_export.js'),
[req.user.loginname, JSON.stringify(options)]
);
child.on('message', (msg) => {
status.status.export_running = false;
res.json({message: msg});
});
let exporter = require('./api/exporter');
let username = process.argv[2];
let options = JSON.parse(process.argv[3]);
exporter.exportImages(username, options)
.then(() =>{
process.send('done');
db.disconnect();
})
.catch((err) =>{
process.send('error:' + err);
db.disconnect();
});
Server
Child
Export done
Import/compare problem
- With export we send small amount of data
- Startup params + username
- done message
- Import has to show what was imported (new, deleted, unchanged items)
- ~10MB+ of data
- IPC has internal limit of about 8-20k, depending on server internals
node-ipc
- A nodejs module for local and remote Inter Process Communication (IPC)
- full support for Linux, Mac and Windows
- also supports all forms of socket communication
const http = require('http');
var ipc = require('node-ipc');
var fork = require('child_process').fork;
ipc.config.id = 'demoserver';
ipc.config.silent = true;
ipc.config.retry = 1500;
fork('client.js');
ipc.serve(() =>{
ipc.server.on('client.started', (messagedata, socket) =>{
console.log('finished comparing data ...');
ipc.server.emit(socket, 'operation.start', {
data: 'this can be any json object'
})
});
ipc.server.on('operation.done', (data, socket) =>{
console.log('finished operation, got result:', data);
console.log('sending shutdown');
ipc.server.emit(socket, 'client.shutdown', {});
ipc.server.stop();
});
});
ipc.server.start();
Server
const ipc = require('node-ipc');
ipc.config.id = 'compare';
ipc.config.retry = 1000;
ipc.config.silent = true;
ipc.connectTo('demoserver', function (){
ipc.of.demoserver.on('connect', () =>{
ipc.of.demoserver.emit('client.started', {});
});
ipc.of.demoserver.on('operation.start', (data) =>{
console.log('got some data:', data);
console.log('starting long operation...');
let result;
for (i = 0; i < 900000000; i++) {
result = i*i;
}
ipc.of.demoserver.emit('operation.done', {result});
});
ipc.of.demoserver.on('client.shutdown', (data) =>{
console.log('got shutdown command, exiting...');
process.exit();
})
});
Client
Protocol (v1)
- Server: fork client and start ipc server
- Client: "I am ready"
- Server: "Great, here is the data, do your magic"
- Client: [grumble grumble...]
"Done, here is the result" - Server: "Ok, thanks. Now we die together" [server stop]
- Client: "Ok :(" [commits suicide]
var ipc = require('node-ipc');
var fork = require('child_process').fork;
var path = require('path');
ipc.config.id = 'pds';
ipc.config.silent = true;
ipc.config.retry = 1500;
var compareData = function(data) {
logger.debug('start comparing data ...');
cache_entry.setState(CacheEntry.COMPARING_DATA);
// fork child node process for comparing, communicate using node-ipc
fork(path.join(__dirname, '../../standalone_compare.js'));
var defer = Q.defer();
ipc.serve(() =>{
ipc.server.on('comparator.started', (messagedata, socket) =>{
ipc.server.emit(socket, 'compare.start', {
import_package: import_package,
importer_instance: importer_instance,
data: data
})
}
);
ipc.server.on('compare.done', (data, socket) =>{
cache_entry.setState(CacheEntry.COMPARED_DATA);
logger.debug('finished comparing data ...');
ipc.server.emit(socket, 'compare.shutdown', {});
ipc.server.stop();
defer.resolve(data.result);
}
)
});
ipc.server.start();
return defer.promise;
};
Server
1
3
5
var ipc = require('node-ipc');
ipc.config.id = 'compare';
ipc.config.retry = 1000;
var compare = require('./api/importer/compare');
ipc.config.silent = true;
ipc.connectTo('pds', function (){
ipc.of.pds.on('connect', () =>{
ipc.of.pds.emit('comparator.started', {});
});
ipc.of.pds.on('compare.start', (data) =>{
let db = require('./api/db').default_connection();
db.connect().then(() =>{
compare.compareData(data.import_package,
data.importer_instance,
data.data).then(result => {
db.disconnect().then(() => {
ipc.of.pds.emit('compare.done', {result});
});
});
});
});
ipc.of.pds.on('compare.shutdown', (data) =>{
process.exit();
})
});
Client
2
4
6
so far so good...
https://github.com/nodejs/node/issues/3145
child_process IPC is very slow, about 100-10000x slower than I expected (scales with msg size) #3145
But...
node-ipc, second try
- zip messages and use rawbuffer
var ipc = require('node-ipc');
var zlib = require('zlib');
var compare = require('./api/importer/compare');
ipc.config.id = 'compare';
ipc.config.retry = 1000;
ipc.config.rawBuffer = true;
ipc.config.encoding = 'hex';
ipc.config.silent = true;
ipc.connectTo('pds', function (){
ipc.of.pds.on('connect', () =>{
ipc.of.pds.emit(Buffer.from('start', 'utf-8'));
});
ipc.of.pds.on('data', (data) =>{
// got start message
if (data.length == 8) {
console.log('got shutdown message, kill client');
process.exit();
} else {
var db = require('./api/db').default_connection();
db.connect().then(() =>{
zlib.unzip(data, (err, buffer) =>{
if (!err) {
var jsonData = JSON.parse(buffer.toString('utf-8'));
compare.compareData(jsonData.import_package, jsonData.importer_instance, jsonData.data).then(result =>{
db.disconnect().then(() =>{
zlib.deflate(JSON.stringify(result), (err, buffer) =>{
if (!err) {
ipc.of.pds.emit(buffer);
} else {
ipc.of.pds.emit('errror');
console.error('Error', err);
}
});
});
});
} else {
console.error('Error', err);
ipc.of.pds.emit('errror');
}
});
});
}
});
});
Client
var ipc = require('node-ipc');
var zlib = require('zlib');
var compare = require('./api/importer/compare');
var lf = require('ts-node-logging');
var logger = lf.app();
ipc.config.id = 'compare';
ipc.config.retry = 1000;
ipc.config.rawBuffer = true;
ipc.config.encoding = 'hex';
ipc.config.silent = true;
ipc.connectTo('pds', function (){
ipc.of.pds.on('connect', () =>{
ipc.of.pds.emit(new Buffer('start', 'utf-8'));
});
ipc.of.pds.on('data', (data) =>{
// got start message
if (data.length == 8) {
logger.debug('got shutdown message, kill client');
process.exit();
} else {
var db = require('./api/db').default_connection();
db.connect().then(() =>{
zlib.gunzip(data, (err, buffer) =>{
if (!err) {
var jsonData = JSON.parse(buffer.toString('utf-8'));
compare.compareData(jsonData.import_package, jsonData.importer_instance, jsonData.data).then(result =>{
db.disconnect().then(() =>{
zlib.gzip(JSON.stringify(result), (err, buffer) =>{
if (!err) {
ipc.of.pds.emit(buffer);
} else {
ipc.of.pds.emit(new Buffer('errror', 'utf-8'));
logger.error('Error', err);
}
});
});
});
} else {
logger.error('Error', err);
ipc.of.pds.emit(new Buffer('errror', 'utf-8'));
}
});
});
}
});
});
Client
It worked!
On my machine (tested everything with the same node version as on the server)....
New problem...
- On my windows 7 PC everything worked as expected
- On our server the child process died silently
- No debug, log every line
- Message size limit on linux (64k)
A New Hope (Protocol)
- Server: fork client and start node-ipc server
- Client: "I am ready"
- Server: "Great, here is the input data size"
- Client: "Thanks, ready for data"
- Server: "Here is the data, all of it or in chunks"
- Client: [receive and assemble data,do magic]
"Done, here's the result size" - Server: "Thanks, ready for data"
- Client: "Here is the data, all of it or in chunks"
- Server: [after reading and assembling the data]
"Ok, thanks, now we die" [stop server] - Client: "Ok :(" [commits suicide]
var responseDataLength = 0;
var responseBuffer = new Buffer(0);
zlib.gzip(JSON.stringify(toZip), (err, buffer) => {
ipc.serve(() =>{
ipc.server.on(
'data',
(data, socket) => {
// got client started confirmation, start comparing
var shutdownServer = () => {
ipc.server.emit(
socket,
new Buffer('shutdown', 'utf-8')
);
ipc.server.stop();
};
// convert only short messages to string, zipped data can stay in buffer form
var clientMessage = '';
if (data.length < 100) {
clientMessage = data.toString('utf-8');
}
//start
if (clientMessage === 'start') {
logger.debug('server sending buffer size', buffer.length);
ipc.server.emit(
socket,
new Buffer('hexMessageLength' + buffer.length, 'utf-8')
);
} else if (clientMessage === 'ack') {
logger.debug('got ack from client, server sending payload', buffer.length);
ipc.server.emit(
socket,
buffer
);
} else if (clientMessage.indexOf('hexResponseLength') > -1) {
responseDataLength = parseInt(clientMessage.substring('hexResponseLength'.length));
ipc.server.emit(
socket,
new Buffer('ack', 'utf-8')
);
} else if (clientMessage === 'error') {
// got an error, shutdown client and reject promise
shutdownServer();
defer.reject(err);
} else {
// data packets
responseBuffer = Buffer.concat([responseBuffer, data], responseBuffer.length + data.length);
// all done shutdown server and client and return the result
if (responseBuffer.length == responseDataLength) {
shutdownServer();
zlib.gunzip(responseBuffer, (err, buffer) =>{
defer.resolve(JSON.parse(buffer.toString('utf-8')));
});
}
}
}
);
});
ipc.server.start();
Server
var totalDataLength = 0;
var tempBuffer = new Buffer(0);
var processedData;
ipc.connectTo('pds', function (){
ipc.of.pds.on('connect', () =>{
logger.debug('connecting to server');
ipc.of.pds.emit(new Buffer('start', 'utf-8'));
});
ipc.of.pds.on('data', (data) =>{
var message = '';
if (data.length < 100) {
message = data.toString('utf-8');
}
if (message.indexOf('hexMessageLength') != -1) {
totalDataLength = parseInt(message.substring('hexMessageLength'.length));
ipc.of.pds.emit(
new Buffer('ack', 'utf-8')
);
} else if ('shutdown' === message) {
logger.debug('got shutdown message, kill the client');
process.exit();
} else if ('ack' === message) {
// server received response data length message, send actual buffered response data
ipc.of.pds.emit(processedData);
} else {
// combine data packets und unzip after all data was received
tempBuffer = Buffer.concat([tempBuffer, data], tempBuffer.length + data.length);
if (tempBuffer.length == totalDataLength) {
processData(tempBuffer).then((processResult) =>{
// data processing finished, send buffer size so that server knows what to except
processedData = processResult;
ipc.of.pds.emit(
new Buffer('hexResponseLength' + processResult.length, 'utf-8')
);
}).fail((err) =>{
logger.error(err);
ipc.of.pds.emit(new Buffer('error', 'utf-8'));
});
}
}
});
});
Client
Questions?
https://github.com/mjurisic/viennajs-node-ipc
@jurisicmarko
mjurisic@gmail.com
Interprocess Communication in NodeJS
By Marko Jurišić
Interprocess Communication in NodeJS
- 1,696