nodegears
Version:
NodeJS bindings for Gearman
368 lines (325 loc) • 9.35 kB
JavaScript
var net = require('net'),
util = require('util'),
EventEmitter= require('events').EventEmitter;
/**
* Gearman packets have the following attributes:
* Each packet starts with a 4 byte "magic" code: \0REQ or \0RES
* Each packet then has a 4 byte type code, defined in packets.types
* Each packet then has the 4 byte lengh of the
* Finally, each packet has a body that sends or recieves the required
* packet information
*/
var packets = {
magic: { // Our magic types
req: new Buffer([0x00, 0x52, 0x45, 0x51]),
res: new Buffer([0x00, 0x52, 0x45, 0x53])
},
types: { // Packet types for both clients and workers
CAN_DO: new Buffer([0x00, 0x00, 0x00, 0x01]),
CANT_DO: new Buffer([0x00, 0x00, 0x00, 0x02]),
RESET_ABILITIES: new Buffer([0x00, 0x00, 0x00, 0x03]),
PRE_SLEEP: new Buffer([0x00, 0x00, 0x00, 0x04]),
NOOP: new Buffer([0x00, 0x00, 0x00, 0x06]),
SUBMIT_JOB: new Buffer([0x00, 0x00, 0x00, 0x07]),
JOB_CREATED: new Buffer([0x00, 0x00, 0x00, 0x08]),
GRAB_JOB: new Buffer([0x00, 0x00, 0x00, 0x09]),
NO_JOB: new Buffer([0x00, 0x00, 0x00, 0x0a]),
JOB_ASSIGN: new Buffer([0x00, 0x00, 0x00, 0x0b]),
WORK_STATUS: new Buffer([0x00, 0x00, 0x00, 0x0c]),
WORK_COMPLETE: new Buffer([0x00, 0x00, 0x00, 0x0d]),
WORK_FAIL: new Buffer([0x00, 0x00, 0x00, 0x0e]),
GET_STATUS: new Buffer([0x00, 0x00, 0x00, 0x0f]),
ECHO_REQ: new Buffer([0x00, 0x00, 0x00, 0x10]),
ECHO_RES: new Buffer([0x00, 0x00, 0x00, 0x11]),
SUBMIT_JOB_BG: new Buffer([0x00, 0x00, 0x00, 0x12]),
ERROR: new Buffer([0x00, 0x00, 0x00, 0x13]),
STATUS_RES: new Buffer([0x00, 0x00, 0x00, 0x14]),
SUBMIT_JOB_HIGH: new Buffer([0x00, 0x00, 0x00, 0x15]),
SET_CLIENT_ID: new Buffer([0x00, 0x00, 0x00, 0x16]),
CAN_DO_TIMEOUT: new Buffer([0x00, 0x00, 0x00, 0x17]),
ALL_YOURS: new Buffer([0x00, 0x00, 0x00, 0x18]),
WORK_EXCEPTION: new Buffer([0x00, 0x00, 0x00, 0x19]),
OPTION_REQ: new Buffer([0x00, 0x00, 0x00, 0x1a]),
OPTION_RES: new Buffer([0x00, 0x00, 0x00, 0x1b]),
WORK_DATA: new Buffer([0x00, 0x00, 0x00, 0x1c]),
WORK_WORKING: new Buffer([0x00, 0x00, 0x00, 0x1d]),
GRAB_JOB_UNIQ: new Buffer([0x00, 0x00, 0x00, 0x1e]),
JOG_ASSIGN_UNIQ: new Buffer([0x00, 0x00, 0x00, 0x1f]),
SUBMIT_JOB_HIGH_BG: new Buffer([0x00, 0x00, 0x00, 0x20]),
SUBMIT_JOB_LOW: new Buffer([0x00, 0x00, 0x00, 0x21]),
SUBMIT_JOB_LOW_BG: new Buffer([0x00, 0x00, 0x00, 0x22]),
SUBMIT_JOB_SCHED: new Buffer([0x00, 0x00, 0x00, 0x23]),
SUBMIT_JOB_EPOCH: new Buffer([0x00, 0x00, 0x00, 0x24])
}
};
function Connection(server, port)
{
this.on(packets.types.ERROR, function(_packet)
{
var _body = this.parse_body(_packet.body, 2);
process.stderr.write('ERROR FROM JOB SERVER: ' +
_body[0] + ' (' + _body[1] + ')' );
this.emit('ERROR_'+_body[0], _body);
});
if(!this.connection)
this.connect(server, port);
}
// Connection will emit events based on reveived packets
util.inherits(Connection, EventEmitter);
/**
* Connect the the Job Server, and leave the connection open, while we wait
* for instruction
*
* @function
* @param server The server hostname we are connecting to
* @param port The port the server is running on
*/
Connection.prototype.connect = function(server, port)
{
this.info = {
host: server || "localhost",
port: port || 4730
};
this.connection = net.createConnection(this.info.port, this.info.host);
this.connection.on('connect', function()
{
this.setEncoding('binary');
this.setNoDelay();
});
var conn = this;
var _packet = {};
this.connection.on('data', function(d)
{
// Do we have a packet from our last event?
if(_packet.hasOwnProperty("data") && _packet.hasOwnProperty("length"))
{
// _packet.length is the remaining before we read
var remaining = _packet.remaining - _packet.bufSize;
var tpacket = conn.read(d, remaining);
var last = (tpacket.remaining - tpacket.bufSize) <= 0;
var tbuf = new Buffer(tpacket.bufSize + _packet.data.length);
// Copy the data into our new buffer
_packet.data.copy(tbuf);
tpacket.data.copy(tbuf, _packet.data.length);
_packet.data = tbuf; // The full packet data
_packet.remaining = tpacket.remaining;
_packet.bufSize = tpacket.bufSize;
if(last) // This is our last packet
{
conn.emit(_packet.type, _packet);
_packet = {};
}
} else
{
_packet = conn.read(d, 0);
// This is the total packet, or a "control"
if(_packet.length === 0 || _packet.bufSize == _packet.length)
{
conn.emit(_packet.type, _packet);
_packet = {};
}
}
});
};
/**
* Disconnect from the job server.
*
* @function
*/
Connection.prototype.disconnect = function()
{
this.connection.end();
};
/**
* Read a recieved packet from the job server, and decode it so that
* we know what to do with it.
*
* An important note: it is the job of whichever object recieves this event
* to parse the body how it will require it.
*
* @function
* @param buffer The buffer of bytes you recieved
*/
Connection.prototype.read = function(buffer, remaining)
{
var readbuf = Buffer(buffer, 'binary');
var _packet;
if(!Buffer.isBuffer(readbuf))
{
throw Error('Readable packets must be buffers');
}
if(remaining === 0)
{
_packet = {
magic: readbuf.slice(0, 4),
type: readbuf.slice(4, 8),
length: readbuf.slice(8, 12).readUInt32BE(0)
};
_packet.data = readbuf.slice(12);
_packet.bufSize = _packet.data.length;
_packet.remaining = _packet.length;
if(!this.hasBuffer(packets.magic, _packet.magic))
throw Error("Invalid magic header");
if(!this.hasBuffer(packets.types, _packet.type))
throw Error('Invalid packet type');
} else
{
_packet = {
data: readbuf,
remaining: remaining,
bufSize: readbuf.length
};
}
return _packet;
};
/**
* Return whether or not the given Buffer is found as a value in the given
* object.
*
* @param object The object being searched
* @param buffer The Buffer to find
*/
Connection.prototype.hasBuffer = function(object, buffer)
{
for(var i in object)
{
if(object[i].toString() == buffer.toString())
return true;
}
return false;
};
/**
* Build a packet that can be sent to the Job Server as a request or
* as a response to a request from the job server
*
* @function
* @param magic The packet magic type
* @param type The packet byte type
* @param length The length of the body
* @param body An array of buffers that act as the body of the packet
*/
Connection.prototype.build = function(magic, type, len, body)
{
var length = len || new Buffer([0x00]);
var _packet = Buffer.concat([magic, type]);
if(length >= 0 || length instanceof Buffer)
{
var b = new Buffer(4);
b.fill(0);
if( ! (length instanceof Buffer) )
{
b.writeUInt32BE(length, 0);
}
_packet = Buffer.concat([_packet, b]);
}
if(body)
{
if(body instanceof Array)
{
var sizebuf = new Buffer(4);
var _size = 0;
var _queue = [];
body.forEach(function(value, key)
{
var _newvalue = new Buffer(value);
if(key != body.length - 1)
{
var nb = new Buffer(1);
nb.writeUInt8(0x0, 0);
_newvalue = Buffer.concat([_newvalue, nb]);
}
_size += _newvalue.length;
_queue.push(_newvalue);
});
sizebuf.writeUInt32BE(_size, 0);
_packet = Buffer.concat([_packet, sizebuf]);
_queue.forEach(function(value)
{
_packet = Buffer.concat([_packet, value]);
});
} else
{
_packet = Buffer.concat([_packet, new Buffer(body.toString())]);
}
}
return _packet;
};
/**
* Send a buffer through the socket to the server to be read and acted on
* by the job server. The packet must be a buffer (usually built by the
* Connection.prototype.build function) with the correct magic and type
* information
*
* @function
* @param buffer The buffer to send as the binary packet
*/
Connection.prototype.send = function(buffer)
{
if(this.connection.readyState != 'open') // Don't pre-buffer
{
var send = this;
setImmediate(function()
{
send.send(buffer);
});
} else
{
var conn = this.connection;
setImmediate(function()
{
conn.write(buffer, 'binary');
});
}
};
/**
* Close the connection to the job server
*
* @function
*/
Connection.prototype.end = function()
{
this.connection.end();
};
/**
* Test our connection to the Job Server. ECHO_REQ should generate an
* ECHO_RES response from the Job Server
*
* @function
*/
Connection.prototype.test = function()
{
var _packet = this.build(comm.packets.magic.req,
comm.packets.types.ECHO_REQ,
-1,
['ECHO', 'REQ']
);
this.send(_packet);
};
/**
* Parse the packet body for the arguements that the packet type requires.
* This will break the packet body into it's argument form (arguements are
* separated by a NULL terminator)
*
* @function
* @param body The packet body
* @param size The number of arguements to split by
*/
Connection.parse_body = function(body, size)
{
var arguements = [];
var start = 0;
for(var i = 0; i <= body.length; i++)
{
if(arguements.length === size) break;
if(body[i] === 0x00 || body.length == i)
{
var _new = new Buffer(i - start);
body.copy(_new, 0, start, i);
arguements.push(_new);
start = i + 1; // Don't include the NULL byte at the end
}
}
return arguements;
};
exports.packets = packets;
exports.Connection = Connection;