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)

  1. Server: fork client and start ipc server
  2. Client: "I am ready"
  3. Server: "Great, here is the data, do your magic"
  4. Client:  [grumble grumble...]
    "Done, here is the result"
  5. Server: "Ok, thanks. Now we die together" [server stop]
  6. 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)

  1. Server: fork client and start node-ipc server
  2. Client: "I am ready"
  3. Server: "Great, here is the input data size"
  4. Client: "Thanks, ready for data"
  5. Server: "Here is the data, all of it or in chunks"
  6. Client: [receive and assemble data,do magic]
    "Done, here's the result size"
  7. Server: "Thanks, ready for data"
  8. Client: "Here is the data, all of it or in chunks"
  9. Server: [after reading and assembling the data]
    "Ok, thanks, now we die" [stop server]
  10. 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,568