gearmanode-up
Version:
Node.js library for the Gearman distributed job system with support for multiple servers
335 lines (279 loc) • 11.9 kB
JavaScript
// Copyright 2013 The GearmaNode Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
* @fileoverview This script represents class communicating as a client with Gearman job server.
* @author vaclav.sykora@google.com (Vaclav Sykora)
*/
var util = require('util'),
events = require('events'),
winston = require('winston'),
ServerManager = require('./server-manager').ServerManager,
Job = require('./job').Job,
lb = require('./load-balancing'),
protocol = require('./protocol'),
common = require('./common'),
JS_CONSTANTS = require('./job-server').CONSTANTS;
/**
* @class Client
* @classdesc A client for communicating with Gearman job servers.
* @constructor
* @augments events.EventEmitter
* @mixes ServerManager
*
* @param options literal representing the client
* @param {string} options.host hostname of single job server
* @param {number} options.port port of single job server
* @param {array} options.servers array of host,port pairs of multiple job servers
* @param {string} [options.loadBalancing=Sequence] name of load balancing strategy
* @param {number} [options.recoverTime=30000] delay in milliseconds before retrying the downed job server
*/
var Client = exports.Client = function(options) {
var returned, clientOptions;
options = options || {};
clientOptions = { loadBalancing: options.loadBalancing, recoverTime: options.recoverTime };
if (options.hasOwnProperty('loadBalancing')) { delete options.loadBalancing; }
if (options.hasOwnProperty('recoverTime')) { delete options.recoverTime; }
returned = common.verifyAndSanitizeOptions(clientOptions, { loadBalancing: 'Sequence', recoverTime: JS_CONSTANTS.DEFAULT_RECOVER_TIME });
if (returned instanceof Error) { return returned; }
this._type = 'Client';
// call ServerManager initialization
returned = this.initServers(options);
if (returned instanceof Error) { return returned; }
// Table of Jobs submited by this client.
// A Job is inserted after JOB_CREATED packet.
// A Job is removed:
// * a non-background job after WORK_COMPLETE
// * a background job by call of Job#close()
this.jobs = {};
// load balancing
switch (clientOptions.loadBalancing) {
case 'RoundRobin':
this.loadBalancer = new lb.RoundRobin(this.jobServers.length);
break;
case 'Sequence':
this.loadBalancer = new lb.Sequence(this.jobServers.length);
break;
default:
return new Error('unknow load balancing strategy: ' + clientOptions.loadBalancing);
}
if (!common.isNumber(clientOptions.recoverTime)) { return new Error('option recoverTime is not number'); }
this.loadBalancer.recoverTime = clientOptions.recoverTime;
events.EventEmitter.call(this);
Client.logger.log('info', 'client initialized with %d job server(s)', this.jobServers.length);
Client.logger.log('debug', 'load balancing: strategy=%s, recoverTime=%d[ms]', clientOptions.loadBalancing, clientOptions.recoverTime);
};
// inheritance
util.inherits(Client, events.EventEmitter);
// mixes ServerManager
ServerManager.mixin(Client);
// static logger
Client.logger = winston.loggers.get('Client');
/**
* Ends the client and all its associated resources, e.g. socket connections.
* Sets property 'closed' to 'true'.
* Removes all registered listeners on the object.
*
* @method
* @fires Client#close
* @returns {void} nothing
*/
Client.prototype.close = function () {
this.closed = true;
this.closeServers();
// clear submited & incomplete jobs
for (var i in this.jobs) {
if (this.jobs.hasOwnProperty(i)) {
this.jobs[i].close();
delete this.jobs[i];
}
}
this.emit('close'); // trigger event
this.removeAllListeners();
};
/**
* Submits given job to a job server according to load balancing strategy.
*
* @method
* @param {string} name function name
* @param {string|Buffer} payload opaque data that is given to the function as an argument
* @param options literal representing additional job configuration
* @param {boolean} options.background flag whether the job should be processed in background/asynchronous
* @param {string} options.priority priority in job server queue, 'HIGH'|'NORMAL'|'LOW'
* @param {string} options.unique unique identifiter for this job, the identifier is assigned by the client
* @param {string} options.toStringEncoding if given received payload will be converted to String with this encoding, otherwise payload turned over as Buffer
* @returns {Job} newly created job
* @fires Job#submited
*/
Client.prototype.submitJob = function(name, payload, options) {
var self = this, job, jobServer, packet, tryToSend;
options = options || {};
common.mixin({'name': name, 'payload': payload}, options);
job = new Job(this, options);
if (job instanceof Error) { return job; }
job.processing = true;
packet = protocol.encodePacket(job.getPacketType(), [job.name, (job.unique ? job.unique : ''), job.payload]);
var jsSendCallback = function(err) {
if (err instanceof Error) {
Client.logger.log('warn', 'failed to submit job, server=%s, error:', jobServer.getUid(), err);
tryToSend();
} else {
jobServer.jobsWaiting4Created.push(job); // add to queue of submited jobs waiting for confirmation
job.jobServerUid = jobServer.getUid(); // store job server UID on job to later usage
process.nextTick(function() { job.emit('submited'); });
Client.logger.log('debug', 'job submited, name=%s, unique=%s', job.name, job.unique);
}
};
tryToSend = function() {
jobServer = self._getJobServer();
if (!jobServer) { // problem escalated by '_getJobServer' on Client too
return job.emit('error', new Error('all job servers fail'));
}
jobServer.send(packet, jsSendCallback);
};
tryToSend();
return job;
};
/**
* @method
* @access protected
* @inheritDoc
*/
Client.prototype._response = function (jobServer, packetType, parsedPacket) { // #unit: not needed
var packetCode = protocol.PACKET_CODES[packetType];
var handle = parsedPacket[1];
var job, jobStatus;
var jobUid = jobServer.getUid() + '#' + handle;
if (!this.jobs.hasOwnProperty(jobUid)) {
this._unrecoverableError('unknown job, uid=' + jobUid);
return;
}
job = this.jobs[jobUid];
if (Client.logger.isLevelEnabled('debug')) {
Client.logger.log('debug', 'response for client: type=%s, uid=%s', packetCode, job.getUid());
}
switch (packetType) {
case protocol.PACKET_TYPES.JOB_CREATED:
job.emit('created'); // trigger event
break;
case protocol.PACKET_TYPES.WORK_COMPLETE:
case protocol.PACKET_TYPES.WORK_FAIL:
job.processing = false;
delete this.jobs[job.getUid()]; // remove it from table of submited & incomplete jobs
job.response = parsedPacket[2];
// encoding of payload; only for WORK_COMPLETE which have data
if (packetType === protocol.PACKET_TYPES.WORK_COMPLETE && job.toStringEncoding) {
job.response = job.response.toString(job.toStringEncoding);
}
job.emit(packetType === protocol.PACKET_TYPES.WORK_COMPLETE ? 'complete' : 'failed'); // trigger event
break;
case protocol.PACKET_TYPES.WORK_DATA:
case protocol.PACKET_TYPES.WORK_WARNING:
var data = parsedPacket[2];
// encoding of data
if (job.toStringEncoding) { data = data.toString(job.toStringEncoding); }
job.emit(packetType == protocol.PACKET_TYPES.WORK_DATA ? 'workData' : 'warning', data); // trigger event
break;
case protocol.PACKET_TYPES.WORK_EXCEPTION:
var data = parsedPacket[2];
// encoding of data
if (job.toStringEncoding) { data = data.toString(job.toStringEncoding); }
job.processing = false;
delete this.jobs[job.getUid()]; // remove it from table of submited & incomplete jobs
job.emit('exception', data); // trigger event
break;
case protocol.PACKET_TYPES.STATUS_RES:
jobStatus = {};
jobStatus.known = '1' == parsedPacket[2];
jobStatus.running = '1' == parsedPacket[3];
jobStatus.percent_done_num = parsedPacket[4];
jobStatus.percent_done_den = parsedPacket[5];
if (Client.logger.isLevelEnabled('verbose')) {
Client.logger.log('verbose', 'job status, handle=%s, known=%d, running=%d, num=%d, den=%d',
handle, jobStatus.known, jobStatus.running, jobStatus.percent_done_num, jobStatus.percent_done_den);
}
job.emit('status', jobStatus); // trigger event
break;
case protocol.PACKET_TYPES.WORK_STATUS:
jobStatus = {};
jobStatus.percent_done_num = parsedPacket[2];
jobStatus.percent_done_den = parsedPacket[3];
if (Client.logger.isLevelEnabled('verbose')) {
Client.logger.log('verbose', 'job status, handle=%s, num=%d, den=%d',
handle, jobStatus.percent_done_num, jobStatus.percent_done_den);
}
job.emit('status', jobStatus); // trigger event
break;
}
};
/**
* Returns a human readable string representation of the object.
*
* @method
* @returns {string} object description
*/
Client.prototype.toString = function() { // #unit: not needed
return 'Client(jobServers=' + util.inspect(this.jobServers) + ')';
};
/**
* @method
* @fires Client#error
* @access protected
* @inheritDoc
*/
Client.prototype._unrecoverableError = function (msg) { // #unit: not needed
Client.logger.log('error', msg);
this.emit('error', new Error(msg)); // trigger event
};
/**
* Gets a job server according to load balancing strategy.
*
* @method
* @access private
*/
Client.prototype._getJobServer = function() {
var idx = this.loadBalancer.nextIndex();
if (idx === null) {
this._unrecoverableError('failed to obtain job server from load balancer (all servers invalid?)');
return null;
}
return this.jobServers[idx];
};
/**
* @class Job
*/
common.mixin({
/**
* Sends request to get status of a background job.
* <br/>
* See Gearman Documentation:
* <br/>
* <i>This is used by clients that have submitted a job with SUBMIT_JOB_BG to see if the
* job has been completed, and if not, to get the percentage complete.</i>
*
* @method
* @memberof Job
* @fires Job#status when response successfully arrived
* @returns {void} nothing
*/
getStatus: function() {
var jobServer, packet;
if (!this.background) { return new Error('this is not a background job'); }
if (!this.handle) { return new Error("no job's handle (no created job?)"); }
if (!this.jobServerUid) { return new Error('no associated job server (never ever submited job?)'); }
jobServer = this.clientOrWorker._getJobServerByUid(this.jobServerUid);
if (!jobServer) { return new Error('job server not found by UID, uid=' + this.jobServerUid); }
packet = protocol.encodePacket(protocol.PACKET_TYPES.GET_STATUS, [this.handle]);
jobServer.send(packet);
}
}, Job.prototype);