mob
Version:
simple multi-process applications
367 lines (276 loc) • 8.82 kB
JavaScript
////
var EventEmitter = require ( 'events' ).EventEmitter,
cluster = require ( 'cluster' );
if ( cluster.isMaster || !cluster.isWorker )
throw new Error ( "Not worker." );
////
function Mobster ()
{
var self = this,
base,
roles = {},
role,
exports = {}, proxies = {},
callbacks = new Callbacks ( this );
//// Export and require.
function publishExports ( object )
{
var key,
methods = [];
if ( !object )
throw new Error ( "Falsy export." );
if ( typeof object === 'function' )
methods.push ( '.' );
for ( key in object )
if ( typeof object [ key ] === 'function' )
methods.push ( key );
process.send ({ MOB : 'EXPORT', methods : methods });
callbacks.forward ( object );
}
self.require = function ( name )
{
if ( !exports [ name ] )
throw new Error ( "Not aware of `" + name + "`'s exports, perhaps change `.role()` order." );
if ( !proxies [ name ] )
proxies [ name ] = callbacks.makeProxy ( name, exports [ name ] );
return proxies [ name ];
};
//// Assign role.
self.base = function ( module )
{
base = module;
return self;
};
self.start = function ()
{
delete self.start;
process.on ( 'message', function ( msg, handle )
{
if ( !msg )
return;
if ( msg.MOB === 'ROLE' )
{
if ( role ) throw new Error ( "MOB/ROLE: Already assigned role." );
if ( !msg.role ) throw new Error ( "MOB/ROLE: No role." );
if ( !msg.name ) throw new Error ( "MOB/ROLE: No name." );
role = roles [ msg.role ];
if ( !role ) throw new Error ( "MOB/ROLE: No such role : " + msg.role );
process.title = msg.name + ' - ' + role + ' (mobster)';
exports = msg.exports;
publishExports
(
( base || module ).require ( role.path )
);
process.send ({ MOB : 'ROLE', name : role.name, path : role.path });
}
else if ( msg.MOB === 'CALL' )
callbacks.onCall ( msg );
else if ( msg.MOB === 'CALLBACK' )
callbacks.onCallback ( msg );
else
self.emit ( 'message', msg, handle );
});
process.nextTick
(
process.send.bind ( process, { MOB : 'JOIN' } )
);
return self;
};
self.role = function ( name, path, options )
{
if ( roles [ name ] )
throw new Error ( "'" + name + "' exists already." );
roles [ name ] = { name : name, path : path, options : options };
return self;
};
//// Scaling.
self.scale = function ( incr )
{
if ( typeof incr !== 'number' || !incr || incr % 1 )
throw new Error ( "Invalid increment: " + incr );
process.send ({ MOB : 'SCALE', incr : incr });
};
//// Messaging.
function send ( obj, handle, filter, count )
{
if ( !obj ) throw new Error ( "Falsy message." );
if ( !count ) throw new Error ( "Falsy count." );
process.send
(
{ MOB : 'MESSAGE', msg : obj, filter : filter, count : count },
handle
);
}
self.send = function ( obj, handle )
{
send ( obj, handle, null, 0xffffff );
return self;
};
self.any = function ( filter )
{
return { send : function ( obj, handle )
{
send ( obj, handle, filter, 1 );
return this;
}};
};
self.pid =
self.all = function ( filter )
{
return { send : function ( obj, handle )
{
send ( obj, handle, filter, 0xffffff );
return this;
}};
};
//// Uncaught exception logging and retrieval.
process.on ( 'uncaughtException', function ( err )
{
var name = ( role && role.name ) || 'unassigned worker',
quarantine;
if ( role && role.options && role.options.quarantine )
quarantine = require ( './quarantine' ) ( err.stack, role.options.quarantine );
if ( err.code !== 'EPIPE' )
require ( 'fs' ).writeFileSync
(
'./' + name + '.err',
JSON.stringify ({ name : err.name, errno : err.errno, code : err.code, message : err.message, stack : err.stack, quarantine : quarantine }) + '\n'
);
process.stderr.write ( '\033[1;31mUncaught exception in `' + name + '`:\033[0m\n' + err.stack + '\n' );
process.exit ( 1 );
});
self.lastErr = function ( callback )
{
var name = ( role && role.name ) || 'unassigned worker',
fs = require ( 'fs' ), fn = './' + name + '.err';
fs.readFile ( fn = './' + role.name + '.err', 'utf8', function ( err, data )
{
if ( err && err.code === 'ENOENT' )
callback ( null, null );
else
{
if ( data ) try
{
data = JSON.parse ( data );
}
catch ( e )
{
err = err || e;
}
callback ( err, data );
}
fs.unlink ( fn );
});
};
//// Events.
EventEmitter.call ( this );
}
Mobster.prototype = EventEmitter.prototype;
////
function Callbacks ( mob )
{
var self = this,
callbacks = {},
counter = 0,
exported;
////
function store ( func )
{
if ( !func ) throw new Error ( "No func!" );
callbacks [ ++ counter ] = func;
return [ process.pid, counter, Date.now () ];
}
function fetch ( tuple )
{
var func = callbacks [ tuple [ 1 ] ];
delete callbacks [ tuple [ 1 ] ];
return func;
}
////
function wrap ( realargs )
{
var args = [], i, n = realargs.length, realarg, type;
for ( i = 0; i < n; i ++ )
{
realarg = realargs [ i ];
if ( ( type = typeof realarg ) === 'function' )
args.push ({ type : 'function', tuple : store ( realarg ) });
else
args.push ({ type : type, data : realarg });
}
return args;
}
function linkCallback ( tuple )
{
if ( tuple [ 0 ] === process.pid )
throw Error ( "This is a local callback ID." );
return function ()
{
mob.pid ( tuple [ 0 ] )
.send ({ MOB : 'CALLBACK', tuple : tuple, args : wrap ( arguments ) });
};
}
function linkCall ( name, method )
{
return function ()
{
mob.any ( name )
.send ({ MOB : 'CALL', method : method, args : wrap ( arguments ) });
};
}
function unwrap ( args )
{
var realargs = [], i, n = args.length, arg;
for ( i = 0; i < n; i ++ )
{
arg = args [ i ];
if ( arg.type === 'function' )
realargs.push ( linkCallback ( arg.tuple ) );
else
realargs.push ( arg.data );
}
return realargs;
}
////
self.forward = function ( exports )
{
exported = exports;
};
self.makeProxy = function ( name, methods )
{
var obj, i, n = methods.length, method;
if ( n && methods [ 0 ] === '.' )
{
obj = linkCall ( name, methods.shift () );
n --;
}
else
obj = {};
for ( i = 0; i < n; i ++ )
{
method = methods [ i ];
obj [ method ] = linkCall ( name, method );
}
return obj;
};
////
self.onCall = function ( msg )
{
var func;
if ( !exported )
throw new Error ( "Nothing exported." );
if ( !( msg.method === '.' && typeof ( func = exported ) === 'function' ) && !( func = exported [ msg.method ] ) )
throw new Error ( "Not an exported method: " + msg.method );
func.apply ( exported, unwrap ( msg.args ) );
};
self.onCallback = function ( msg )
{
var func;
if ( !msg.tuple )
throw new Error ( "No tuple." );
if (( func = fetch ( msg.tuple ) ))
func.apply ( null, unwrap ( msg.args ) );
};
}
////
module.exports = new Mobster;