├── ooad ├── Classes.dia └── Classes.png ├── .gitignore ├── example ├── readme-sample-worker.js ├── readme-sample-client.js ├── worker.rb ├── worker.php ├── job-server.js ├── stability-test-worker.js ├── stability-test-client.js ├── worker.js └── client.js ├── test ├── BF │ ├── 5 │ │ ├── worker.js │ │ └── client.js │ ├── 25 │ │ ├── worker.js │ │ └── client.js │ └── 26 │ │ ├── worker.php │ │ └── client.js ├── test-server-manager.js ├── test-job.js ├── test-common.js ├── test-load-balancing.js ├── test-client.js ├── test-all-stack.js ├── test-worker.js └── test-job-server.js ├── .travis.yml ├── package.json ├── lib ├── gearmanode │ ├── version.js │ ├── server-manager.js │ ├── load-balancing.js │ ├── job.js │ ├── protocol.js │ ├── common.js │ ├── client.js │ ├── worker.js │ └── job-server.js └── gearmanode.js ├── LICENSE └── README.md /ooad/Classes.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veny/GearmaNode/HEAD/ooad/Classes.dia -------------------------------------------------------------------------------- /ooad/Classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veny/GearmaNode/HEAD/ooad/Classes.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | experiment.js 16 | 17 | node_modules 18 | gearmanode.sublime-* 19 | ooad/Classes.dia~ 20 | out 21 | -------------------------------------------------------------------------------- /example/readme-sample-worker.js: -------------------------------------------------------------------------------- 1 | var gearmanode = require('../lib/gearmanode'); 2 | var worker = gearmanode.worker(); 3 | 4 | worker.addFunction('reverse', function (job) { 5 | job.sendWorkData(job.payload); // mirror input as partial result 6 | job.workComplete(job.payload.toString().split("").reverse().join("")); 7 | }); 8 | -------------------------------------------------------------------------------- /test/BF/5/worker.js: -------------------------------------------------------------------------------- 1 | var gearmanode = require('../../../lib/gearmanode'); 2 | 3 | // worker for BF#5 4 | var worker = gearmanode.worker(); 5 | worker.addFunction('reverse', function (job) { 6 | var rslt = job.payload.toString().split("").reverse().join(""); 7 | console.log("id=" + job.handle + ", result=" + rslt); 8 | job.workComplete(); 9 | }); 10 | -------------------------------------------------------------------------------- /example/readme-sample-client.js: -------------------------------------------------------------------------------- 1 | var gearmanode = require('../lib/gearmanode'); 2 | var client = gearmanode.client(); 3 | 4 | var job = client.submitJob('reverse', 'hello world!'); 5 | job.on('workData', function(data) { 6 | console.log('WORK_DATA >>> ' + data); 7 | }); 8 | job.on('complete', function() { 9 | console.log('RESULT >>> ' + job.response); 10 | client.close(); 11 | }); 12 | -------------------------------------------------------------------------------- /test/BF/25/worker.js: -------------------------------------------------------------------------------- 1 | var gearmanode = require('gearmanode'); 2 | 3 | gearmanode.Client.logger.transports.console.level = 'error'; 4 | 5 | // worker which returns a incrementing int 6 | var cnt = 0; 7 | var worker = gearmanode.worker(); 8 | worker.addFunction('wtf', function (job) { 9 | var response = { 10 | 'cnt': ++cnt 11 | } 12 | console.log("counter: " + cnt); 13 | job.workComplete(JSON.stringify(response)); 14 | }); -------------------------------------------------------------------------------- /test/BF/5/client.js: -------------------------------------------------------------------------------- 1 | var gearmanode = require('../../../lib/gearmanode'); 2 | 3 | // Background Job 4 | for (var i = 1; i < 1000; i ++) { 5 | var client = gearmanode.client(); 6 | var job = client.submitJob('reverse', 'hallo', {background: true}); 7 | job.on('created', function() { 8 | console.log('--- Job#created - ' + job.toString()); 9 | }); 10 | // job.on('status', function(result) { 11 | // console.log('--- result: ' + util.inspect(result)); 12 | // client.close(); 13 | // }); 14 | } -------------------------------------------------------------------------------- /example/worker.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # encoding: utf-8 3 | 4 | require 'gearman' 5 | 6 | worker = Gearman::Worker.new(['localhost']) 7 | 8 | 9 | worker.add_ability('reverse') do |data,job| 10 | result = data.force_encoding('UTF-8').reverse 11 | puts "reverse: job = #{job.inspect}, data = #{data}" 12 | result 13 | end 14 | 15 | 16 | worker.add_ability('sleep') do |data,job| 17 | seconds = data 18 | (1..seconds.to_i).each do |i| 19 | sleep 1 20 | puts "sleep: job = #{job.inspect}, idx = #{i}" 21 | job.report_status(i, seconds) 22 | end 23 | "done, seconds = #{seconds}" 24 | end 25 | loop { worker.work } 26 | -------------------------------------------------------------------------------- /example/worker.php: -------------------------------------------------------------------------------- 1 | addServer(); 5 | $worker->addFunction('reverse', 'my_reverse_function'); 6 | while ($worker->work()); 7 | 8 | function my_reverse_function($job) { 9 | $max = 5; 10 | $rslt = strrev($job->workload()); 11 | print('handle=' . $job->handle() . "\n"); 12 | 13 | for ($i = 0; $i < $max; $i ++) { 14 | sleep(1); 15 | //if ($i > 0) { echo 'XXX'; $job->sendComplete($rslt); } 16 | print('i=' . $i . "\n"); 17 | $job->sendStatus($i + 1, $max); 18 | } 19 | $job->sendComplete($rslt); 20 | 21 | return $rslt; 22 | } 23 | 24 | ?> -------------------------------------------------------------------------------- /test/BF/25/client.js: -------------------------------------------------------------------------------- 1 | var gearmanode = require('gearmanode'); 2 | 3 | gearmanode.Client.logger.transports.console.level = 'error'; 4 | 5 | // hammer the server with repeated jobs with gearmanode as client 6 | var client = gearmanode.client(); 7 | setInterval(function() { 8 | var job = client.submitJob( 9 | "wtf", 10 | JSON.stringify({"something": true}), 11 | {} 12 | ); 13 | job.on('complete', function () { 14 | try { 15 | var response = JSON.parse(job.response); 16 | } catch (e) { 17 | // the response should always be valid json 18 | console.log("FAIL") 19 | console.log(job.response.toString()) 20 | console.log(job); 21 | process.exit(); 22 | } 23 | console.log("job %s complete", response.cnt); 24 | }); 25 | }, 20); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "5" 5 | - "4" 6 | - "0.12" 7 | - "0.10" 8 | - "iojs" 9 | 10 | before_install: 11 | # Prepare repository for isntall "libboost". 12 | - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc) main universe restricted multiverse" 13 | - sudo apt-get update 14 | 15 | - sudo apt-get install libboost-all-dev gperf libevent-dev uuid-dev 16 | 17 | install: 18 | - cat /proc/version 19 | - uname -a 20 | - cat /etc/issue 21 | - curl -L https://launchpad.net/gearmand/1.2/1.1.12/+download/gearmand-1.1.12.tar.gz | tar zxv 22 | - cd gearmand-1.1.12 && ./configure && make && sudo make install 23 | 24 | before_script: 25 | - /usr/local/sbin/gearmand --daemon 26 | 27 | notifications: 28 | email: 29 | recipients: 30 | - vaclav.sykora@gmail.com 31 | on_success: change 32 | on_failure: change 33 | -------------------------------------------------------------------------------- /test/BF/26/worker.php: -------------------------------------------------------------------------------- 1 | addServer(); 5 | 6 | $c = 0; 7 | $worker->addFunction("cnt", function ($job) use (&$c) { 8 | $c++; 9 | print("job {$c}\n"); 10 | return $c; 11 | }); 12 | 13 | $onek = str_pad('', 1024); 14 | $worker->addFunction("1K", function ($job) use ($onek) { 15 | print("job 1K\n"); 16 | return $onek; 17 | }); 18 | 19 | $tenk = str_pad('', 1024*10); 20 | $worker->addFunction("10K", function ($job) use ($tenk) { 21 | print("job 10K\n"); 22 | return $tenk; 23 | }); 24 | 25 | $hundredk = str_pad('', 1024*100); 26 | $worker->addFunction("100K", function ($job) use ($hundredk) { 27 | print("job 100K\n"); 28 | return $hundredk; 29 | }); 30 | 31 | $onem = str_pad('', 1024*1024); 32 | $worker->addFunction("1M", function ($job) use ($onem) { 33 | print("job 1M\n"); 34 | return $onem; 35 | }); 36 | 37 | while ($worker->work()) { 38 | continue; 39 | } -------------------------------------------------------------------------------- /example/job-server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This script demonstrates how to: 3 | * 1) test or debug a job server with JobServer#echo 4 | * 2) set an option for the connection in the job server 5 | * 6 | * (C) 2013 Vaclav Sykora 7 | * Apache License, Version 2.0, http://www.apache.org/licenses/ 8 | */ 9 | 10 | var gearmanode = require('../lib/gearmanode'), 11 | util = require('util'); 12 | 13 | var client = gearmanode.client(); 14 | var js = client.jobServers[0]; 15 | 16 | // js.once('echo', function(resp) { 17 | // console.log('ECHO: response=' + resp); 18 | // client.close(); 19 | // }); 20 | // js.echo('ping') 21 | 22 | 23 | // js.once('option', function(resp) { 24 | // console.log('SET_OPTION: response=' + resp); 25 | // client.close(); 26 | // }); 27 | // js.setOption('exceptions') 28 | 29 | 30 | js.once('jobServerError', function(code, msg) { 31 | console.log('SET_OPTION: errCode=' + code +', message=' + msg); 32 | client.close(); 33 | }); 34 | js.setOption('unknown_option') 35 | -------------------------------------------------------------------------------- /example/stability-test-worker.js: -------------------------------------------------------------------------------- 1 | // start following CLI command before: 2 | // > gearmand 3 | // > gearmand -p 4731 4 | 5 | 6 | var gearmanode = require('../lib/gearmanode'); 7 | 8 | gearmanode.Worker.logger.transports.console.level = 'info'; 9 | 10 | var worker = gearmanode.worker({servers: [{}, {port: 4731}]}); 11 | //var worker = gearmanode.worker({port: 4731}); 12 | 13 | 14 | // Foreground Job 15 | worker.addFunction('reverse', function (job) { 16 | console.log('>>> reverse: ' + job.handle + ', serverUid: ' + job.jobServerUid + ', payload: ' + job.payload) 17 | var rslt = job.payload.toString().split("").reverse().join(""); 18 | job.workComplete(rslt); 19 | }); 20 | 21 | 22 | // Background Job 23 | worker.addFunction('add', function (job) { 24 | console.log('>>> add: ' + job.handle + ', serverUid: ' + job.jobServerUid + ', payload: ' + job.payload) 25 | var ab = job.payload.toString().split(' '); 26 | var a = new Number(ab[0]); 27 | var b = new Number(ab[1]); 28 | job.workComplete((a + b).toString()); 29 | }); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gearmanode", 3 | "version": "0.9.2", 4 | "description": "Node.js library for the Gearman distributed job system with support for multiple servers", 5 | "keywords": ["gearman", "distributed", "message queue", "job", "worker"], 6 | "author": "Vaclav Sykora ", 7 | "main": "./lib/gearmanode", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/veny/GearmaNode" 11 | }, 12 | "bugs" : { "url" : "https://github.com/veny/GearmaNode/issues" }, 13 | "engines": { "node": ">=0.10.0" }, 14 | "licenses": [{ 15 | "type": "Apache 2.0", 16 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 17 | }], 18 | "scripts": { 19 | "test": "mocha", 20 | "doc": "jsdoc lib/gearmanode.js lib/gearmanode/*.js" 21 | }, 22 | "dependencies": { 23 | "winston": ">=0.7.1" 24 | }, 25 | "devDependencies": { 26 | "mocha": ">=1.10.0", 27 | "should": ">=1.2.2", 28 | "sinon": ">=1.7.3" 29 | }, 30 | "files": ["lib", "README.md", "LICENSE"] 31 | } -------------------------------------------------------------------------------- /example/stability-test-client.js: -------------------------------------------------------------------------------- 1 | // start following CLI command before: 2 | // > gearmand 3 | // > gearmand -p 4731 4 | 5 | var gearmanode = require('../lib/gearmanode'); 6 | 7 | //gearmanode.Client.logger.transports.console.level = 'info'; 8 | var limit = 300; 9 | var timeout = 100; 10 | 11 | // Foreground Job 12 | var client = gearmanode.client({servers: [{}, {port: 4731}], loadBalancing: 'RoundRobin'}); 13 | 14 | 15 | function submitJobAndRegisterListeners(upto) { 16 | var job; 17 | 18 | if (upto >= limit) { 19 | console.log('<<< END'); 20 | return; 21 | } 22 | 23 | // random number between 1 and 10 24 | var a = Math.floor((Math.random() * 10) + 1); 25 | var b = Math.floor((Math.random() * 10) + 1); 26 | var data = a + ' ' + b; 27 | 28 | var method = a < 7 ? 'reverse' : 'add'; 29 | 30 | job = client.submitJob(method, data); 31 | job.on('complete', function() { 32 | console.log('RESULT >>> job=' + job.toString() + ', response=' + job.response); 33 | if (method == 'add' && job.response != (a + b)) { console.log('ERROR (unexpected response) >>>'); } 34 | if (method == 'reverse' && job.response != data.toString().split("").reverse().join("")) { console.log('ERROR (unexpected response) >>>'); } 35 | job.close(); 36 | }); 37 | job.on('failed', function() { 38 | console.log('FAILURE >>> ' + job.handle); 39 | job.close(); 40 | }); 41 | 42 | setTimeout(function() { 43 | submitJobAndRegisterListeners(++upto); 44 | }, timeout); 45 | } 46 | 47 | submitJobAndRegisterListeners(0); 48 | -------------------------------------------------------------------------------- /test/BF/26/client.js: -------------------------------------------------------------------------------- 1 | var numCompleted = 0; 2 | var bytesRecieved = 0; 3 | 4 | function completeNodeGearman (data) { 5 | numCompleted++; 6 | bytesRecieved+= data.length; 7 | } 8 | 9 | function completeGearmaNode () { 10 | numCompleted++; 11 | bytesRecieved+= this.response.length; 12 | } 13 | 14 | setTimeout(function() { 15 | function fileSizeSI(a,b,c,d,e){ 16 | return (b=Math,c=b.log,d=1e3,e=c(a)/c(d)|0,a/b.pow(d,e)).toFixed(2)+' '+(e?'kMGTPEZY'[--e]+'B':'Bytes') 17 | } 18 | console.log( 19 | "completed %d jobs and %s of payload", 20 | numCompleted, 21 | fileSizeSI(bytesRecieved) 22 | ); 23 | process.exit(); 24 | }, 10*1000); 25 | 26 | var jobName; 27 | jobName = 'cnt'; 28 | jobName = '1K'; 29 | jobName = '10K'; 30 | jobName = '100K'; 31 | jobName = '1M'; 32 | 33 | var binding = process.argv[2] 34 | if ('GearmaNode' === binding) { 35 | console.log('Binding: %s, jobName: %s', binding, jobName); 36 | var gearmanode = require('gearmanode'); 37 | gearmanode.Client.logger.transports.console.level = 'error'; 38 | var client = gearmanode.client(); 39 | setInterval(function() { 40 | var job = client.submitJob(jobName, ""); 41 | job.on('complete', completeGearmaNode); 42 | }, 1); 43 | } else if ('node-gearman' === binding) { 44 | console.log('Binding: %s, jobName: %s', binding, jobName); 45 | var NodeGearman = require('node-gearman'); 46 | var nodegearman = new NodeGearman(); 47 | setInterval(function() { 48 | var job = nodegearman.submitJob(jobName, ""); 49 | job.on("data", completeNodeGearman); 50 | }, 1); 51 | } else { 52 | console.log('Unknown binding!'); 53 | process.exit(); 54 | } -------------------------------------------------------------------------------- /lib/gearmanode/version.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The GearmaNode Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | * @fileoverview This script represents changelog and history of version evolution. 17 | * @author vaclav.sykora@google.com (Vaclav Sykora) 18 | */ 19 | 20 | 21 | exports.VERSION_HISTORY = [ 22 | ['0.9.2', '2015-12-21', 'PR #41'], 23 | ['0.9.1', '2015-06-28', 'PR #35, Enh #24'], 24 | ['0.9.0', '2015-06-19', 'PR #29, fully implemented Gearman Protocol'], 25 | ['0.2.2', '2015-02-04', 'BF #26'], 26 | ['0.2.1', '2015-01-04', 'BF #25'], 27 | ['0.2.0', '2015-23-03', 'Added support for binary data, Enh #2'], 28 | ['0.1.8', '2014-24-08', 'Enh #16, BF #17'], 29 | ['0.1.7', '2014-14-07', 'BF #14'], 30 | ['0.1.6', '2014-17-06', 'BF #13'], 31 | ['0.1.5', '2014-20-03', 'added SET_CLIENT_ID; integration with Travis CI; BF #9'], 32 | ['0.1.4', '2014-28-02', 'added CAN_DO_TIMEOUT; BF #7'], 33 | ['0.1.3', '2014-20-02', 'BF #6'], 34 | ['0.1.2', '2013-27-11', 'added Worker#resetAbilities'], 35 | ['0.1.1', '2013-12-11', 'BF #4; added Job#reportWarning & Job#sendWorkData'], 36 | ['0.1.0', '2013-11-10', 'Initial version on Node.js v0.10.8 and gearmand v1.1.7'] 37 | ]; 38 | 39 | exports.VERSION = exports.VERSION_HISTORY[0][0]; 40 | -------------------------------------------------------------------------------- /example/worker.js: -------------------------------------------------------------------------------- 1 | 2 | var gearmanode = require('../lib/gearmanode'); 3 | 4 | 5 | // Foreground Job 6 | // var worker = gearmanode.worker(); 7 | // worker.addFunction('reverse', function (job) { 8 | // job.sendWorkData(job.payload); // mirror input as partial result 9 | // var rslt = job.payload.toString().split("").reverse().join(""); 10 | // job.workComplete(rslt); 11 | // }); 12 | 13 | 14 | // Foreground Job with Timeout 15 | // var worker = gearmanode.worker(); 16 | // worker.addFunction('reverse', function (job) { 17 | // console.log("payload>>> " + job.payload); 18 | // setTimeout(function() { 19 | // console.log('WAKE UP'); 20 | // var rslt = job.payload.toString().split("").reverse().join(""); 21 | // job.workComplete(rslt); 22 | // }, 12000); 23 | // }, {timeout: 10}); 24 | 25 | 26 | // Foreground Job with Client ID 27 | var worker = gearmanode.worker(); 28 | worker.setWorkerId('FooBazBar'); 29 | worker.addFunction('reverse', function (job) { 30 | console.log("payload>>> " + job.payload); 31 | var rslt = job.payload.toString().split("").reverse().join(""); 32 | job.workComplete(rslt); 33 | }); 34 | 35 | 36 | // Background Job 37 | // var worker = gearmanode.worker(); 38 | // worker.addFunction('sleep', function (job) { 39 | // var seconds = new Number(job.payload); 40 | // var cnt = 0; 41 | // var tmo = function() { 42 | // if (cnt < seconds) { 43 | // cnt ++; 44 | // console.log('== sleep: idx=' + cnt + ', ' + job.toString()); 45 | // job.reportStatus(cnt, seconds); 46 | // setTimeout(tmo, 1000); 47 | // } else { 48 | // job.workComplete(); 49 | // } 50 | // } 51 | // tmo(); 52 | // }); 53 | 54 | 55 | // Job#reportError (for background jobs) 56 | // var worker = gearmanode.worker(); 57 | // worker.addFunction('reverse', function (job) { 58 | // // job.reportError(); 59 | // // job.reportException('delta alfa'); 60 | // job.reportWarning('delta alfa'); 61 | // job.workComplete('OIIUSHDF'); 62 | // }); 63 | -------------------------------------------------------------------------------- /example/client.js: -------------------------------------------------------------------------------- 1 | 2 | // start following CLI command before: 3 | // gearman -w -f reverse -- rev 4 | 5 | var gearmanode = require('../lib/gearmanode'), 6 | util = require('util'); 7 | 8 | 9 | // Foreground Job waiting for completition 10 | var client = gearmanode.client(); 11 | var job = client.submitJob('reverse', 'hello world!'); 12 | //var job = client.submitJob('reverse', 'žluťoučký kůň'); 13 | job.on('workData', function(data) { 14 | console.log('WORK_DATA >>> ' + data); 15 | }); 16 | job.on('complete', function() { 17 | console.log('RESULT >>> ' + job.response); 18 | client.close(); 19 | }); 20 | job.on('failed', function() { 21 | console.log('FAILURE >>> ' + job.handle); 22 | client.close(); 23 | }); 24 | 25 | 26 | // Foreground Job receiving status update 27 | // var client = gearmanode.client(); 28 | // var job = client.submitJob('sleep', '3'); 29 | // job.on('status', function(result) { 30 | // console.log('STATUS >> ' + util.inspect(result)); 31 | // }); 32 | // job.on('complete', function() { 33 | // console.log("RESULT >>> " + job.response); 34 | // client.close(); 35 | // }); 36 | 37 | 38 | // Background Job asking for status 39 | // var timeout = 3000; 40 | // var client = gearmanode.client(); 41 | // var job = client.submitJob('sleep', '5', {background: true}); 42 | // job.on('created', function() { 43 | // console.log('--- Job#created - ' + job.toString()); 44 | // console.log('--- waiting for wake-up ' + timeout + '[ms] ...') 45 | // setTimeout((function() { 46 | // job.getStatus(function(err){console.log('=========== ' + err)}); 47 | // }), timeout); 48 | // }); 49 | // job.on('status', function(result) { 50 | // console.log('--- result: ' + util.inspect(result)); 51 | // client.close(); 52 | // }); 53 | 54 | 55 | // Foreground Job obtaining error/exception 56 | // var client = gearmanode.client(); 57 | // //client.jobServers[0].setOption('exceptions', function(){}); 58 | // var job = client.submitJob('reverse', 'hi'); 59 | // job.on('complete', function() { 60 | // console.log('RESULT >>> ' + job.response); 61 | // client.close(); 62 | // }); 63 | // job.on('failed', function() { 64 | // console.log('FAILURE >>> ' + job.handle); 65 | // client.close(); 66 | // }); 67 | // job.on('exception', function(text) { // needs configuration of job server session (JobServer#setOption) 68 | // console.log('EXCEPTION >>> ' + text); 69 | // client.close(); 70 | // }) 71 | -------------------------------------------------------------------------------- /lib/gearmanode.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The GearmaNode Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | * @fileoverview This script represents the entry point for Gearmanode: the Node.js binding for Gearman. 17 | * @author vaclav.sykora@google.com (Vaclav Sykora) 18 | * @exports gearmanode 19 | */ 20 | 21 | var winston = require('winston'), 22 | version = require('./gearmanode/version'), 23 | common = require('./gearmanode/common'), 24 | job = require('./gearmanode/job'), 25 | Client = require('./gearmanode/client').Client, 26 | Worker = require('./gearmanode/worker').Worker; 27 | 28 | 29 | winston.log('info', 'GearmaNode %s, running on Node.js %s [%s %s], with pid %d', 30 | version.VERSION, process.version, process.platform, process.arch, process.pid); 31 | 32 | /** 33 | * Winston logging extension: 34 | * This method represent a way to eliminate the cost constructing a logging message, string construction and concatenation, 35 | * regardless of whether the message is logged or not. 36 | * 37 | * @function 38 | * @param level logging level to be tested if enabled 39 | * @returns {boolean} flag whether given level enabled or not 40 | */ 41 | winston.Logger.prototype.isLevelEnabled = function(level) { 42 | for (var key in this.transports) { 43 | var transportLevel = this.transports[key].level; 44 | if (this.levels[transportLevel] <= this.levels[level]) { return true; } 45 | } 46 | return false; 47 | }; 48 | 49 | /** 50 | * Factory method for a client. 51 | * 52 | * @function 53 | * @param options see constructor of {@link Client} 54 | * @returns {Client} newly created client 55 | * @link Client 56 | */ 57 | exports.client = function (options) { 58 | return new Client(options); 59 | }; 60 | 61 | 62 | /** 63 | * Factory method for a worker. 64 | * 65 | * @function 66 | * @param options see constructor of {@link Worker} 67 | * @returns {Worker} newly created worker 68 | * @link Worker 69 | */ 70 | exports.worker = function (options) { 71 | return new Worker(options); 72 | }; 73 | 74 | 75 | // Expose core related prototypes 76 | exports.Client = Client; 77 | exports.Worker = Worker; 78 | exports.Job = job.Job; 79 | -------------------------------------------------------------------------------- /test/test-server-manager.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | util = require('util'), 3 | sinon = require('sinon'), 4 | ServerManager = require('../lib/gearmanode/server-manager').ServerManager, 5 | JobServer = require('../lib/gearmanode/job-server').JobServer; 6 | 7 | 8 | describe('ServerManager', function() { 9 | var StubKlass = function () {}; 10 | var serMan; 11 | before(function() { 12 | ServerManager.mixin(StubKlass); 13 | }); 14 | beforeEach(function() { 15 | serMan = new StubKlass(); 16 | serMan._type = 'Client'; 17 | serMan.emit = sinon.spy(); 18 | }); 19 | 20 | 21 | describe('#mixin', function() { 22 | it('should mix in methods from ServerManager', function() { 23 | StubKlass.prototype.initServers.should.be.an.instanceof(Function); 24 | StubKlass.prototype.closeServers.should.be.an.instanceof(Function); 25 | StubKlass.prototype._getJobServerByUid.should.be.an.instanceof(Function); 26 | }) 27 | }) 28 | 29 | 30 | describe('#initServers', function() { 31 | it('should return one default server', function() { 32 | var returned = serMan.initServers(); 33 | should.not.exist(returned); 34 | should.exist(serMan.jobServers); 35 | serMan.jobServers.length.should.equal(1); 36 | should.exist(serMan.jobServers[0].clientOrWorker); 37 | serMan.jobServers[0].clientOrWorker.should.equal(serMan); 38 | serMan.jobServers[0].host.should.equal('localhost'); 39 | serMan.jobServers[0].port.should.equal(4730); 40 | }) 41 | it('should return one specified server', function() { 42 | var returned = serMan.initServers({host: 'test.com', port: 4444}); 43 | should.not.exist(returned); 44 | serMan.jobServers.length.should.equal(1); 45 | serMan.jobServers[0].should.be.an.instanceof(JobServer); 46 | serMan.jobServers[0].host.should.equal('test.com'); 47 | serMan.jobServers[0].port.should.equal(4444); 48 | }) 49 | it('should return error when an unknown option found', function() { 50 | serMan.initServers({unknown: true}).should.be.an.instanceof(Error); 51 | }) 52 | it('should return error when servers not/empty array', function() { 53 | serMan.initServers({ servers: 1 }).should.be.an.instanceof(Error); 54 | serMan.initServers({ servers: [] }).should.be.an.instanceof(Error); 55 | }) 56 | it('should return error when servers are duplicate', function() { 57 | serMan.initServers({ servers: [{host: 'localhost'}, {host: 'localhost'}] }).should.be.an.instanceof(Error); 58 | }) 59 | it('should return corresponding array of job servers', function() { 60 | serMan.initServers({ servers: [{ host: 'foo.com'}, { port: 4444 }] }); 61 | serMan.jobServers.length.should.equal(2); 62 | serMan.jobServers[0].should.be.an.instanceof(JobServer); 63 | serMan.jobServers[0].host.should.equal('foo.com'); 64 | serMan.jobServers[0].port.should.equal(4730); 65 | serMan.jobServers[1].should.be.an.instanceof(JobServer); 66 | serMan.jobServers[1].host.should.equal('localhost'); 67 | serMan.jobServers[1].port.should.equal(4444); 68 | }) 69 | }) 70 | 71 | 72 | describe('#closeServers', function() { 73 | it('should clean up object', function() { 74 | serMan.initServers(); 75 | serMan.jobServers.length.should.equal(1); 76 | serMan.closeServers(); 77 | serMan.jobServers.length.should.equal(1); 78 | }) 79 | }) 80 | 81 | }) 82 | -------------------------------------------------------------------------------- /lib/gearmanode/server-manager.js: -------------------------------------------------------------------------------- 1 | 2 | var util = require('util'), 3 | common = require('./common'), 4 | JobServer = require('./job-server').JobServer; 5 | 6 | 7 | /** 8 | * This class represents a mixin for Client and/or Worker 9 | * which adds functionality for manipulation with multiple job servers. 10 | * 11 | * @mixin 12 | * 13 | */ 14 | var ServerManager = exports.ServerManager = function () {}; 15 | 16 | 17 | /** 18 | * *options* 19 | * *host - hostname of single job server 20 | * *port - port of single job server 21 | * *servers - array of host,port pairs of multiple job servers 22 | */ 23 | ServerManager.prototype.initServers = function (options) { 24 | var pattern, returned, jobServer, clonedOptions; 25 | 26 | if (!this.hasOwnProperty('_type')) { return new Error('this object is neither Client nor Worker'); } 27 | 28 | options = options || {}; 29 | clonedOptions = common.clone(options); 30 | 31 | // VALIDATION 32 | pattern = { host: 'localhost', port: 4730, servers: 'optional' }; 33 | returned = common.verifyAndSanitizeOptions(clonedOptions, pattern); 34 | if (returned instanceof Error) { return returned; } 35 | 36 | if (clonedOptions.hasOwnProperty('servers')) { 37 | if (!util.isArray(clonedOptions.servers)) { return new Error('servers: not an array'); } 38 | if (clonedOptions.servers.length === 0) { return new Error('servers: empty array'); } 39 | } else { // fake servers if only single server given 40 | clonedOptions.servers = [{ host: clonedOptions.host, port: clonedOptions.port }]; 41 | } 42 | 43 | this.jobServers = []; 44 | 45 | // iterate server definitions and instantiate JobServer 46 | pattern = { host: 'localhost', port: 4730 }; 47 | for (var i = 0; i < clonedOptions.servers.length; i ++) { 48 | common.verifyAndSanitizeOptions(clonedOptions.servers[i], pattern); 49 | jobServer = new JobServer(clonedOptions.servers[i]); 50 | 51 | // assert whether no duplicate server 52 | if (!this.jobServers.every(function(el) { return el.getUid() != jobServer.getUid(); })) { 53 | return new Error('duplicate server, uid=' + jobServer.getUid()); 54 | } 55 | // only paranoia 56 | if (jobServer instanceof Error) { return jobServer; } 57 | 58 | jobServer.clientOrWorker = this; // bidirectional association management 59 | 60 | this.jobServers.push(jobServer); 61 | } 62 | }; 63 | 64 | 65 | /** 66 | * Cleanly ends associated job servers. 67 | */ 68 | ServerManager.prototype.closeServers = function () { 69 | for (var i = 0; i < this.jobServers.length; i ++) { 70 | this.jobServers[i].disconnect(); 71 | delete this.jobServers[i].clientOrWorker; // bidirectional association management 72 | } 73 | }; 74 | 75 | 76 | /** 77 | * Mixin - augment the target constructor with the ServerManager functions. 78 | */ 79 | ServerManager.mixin = function (destinationCtor) { // #unit: not needed 80 | common.mixin(ServerManager.prototype, destinationCtor.prototype); 81 | }; 82 | 83 | 84 | /** 85 | * Gets index of a job server according to given UID or 'undefined' if not found. 86 | */ 87 | ServerManager.prototype._getJobServerIndexByUid = function (uid) { // #unit: not needed 88 | for (var i = 0; i < this.jobServers.length; i ++) { 89 | if (uid === this.jobServers[i].getUid()) { 90 | return i; 91 | } 92 | } 93 | return undefined; 94 | }; 95 | 96 | 97 | /** 98 | * Gets a JobServer object according to given UID or 'undefined' if not found. 99 | */ 100 | ServerManager.prototype._getJobServerByUid = function (uid) { // #unit: not needed 101 | var idx = this._getJobServerIndexByUid(uid); 102 | if (idx === undefined) { return undefined; } 103 | return this.jobServers[idx]; 104 | }; 105 | 106 | 107 | /** 108 | * Invoked when a job server response is delivered to client/worker. 109 | * 110 | * @method 111 | * @param jobServer job server received the packet 112 | * @param packetType type of the packet 113 | * @param parsedPacket parsed array with packet's data 114 | * @abtract 115 | */ 116 | ServerManager.prototype._response = common.abstractMethod; 117 | 118 | 119 | /** 120 | * Processes an error that cannot be covered by client/worker. 121 | * 122 | * @method 123 | * @param msg message describing the error 124 | * @abtract 125 | * @access protected 126 | */ 127 | ServerManager.prototype._unrecoverableError = common.abstractMethod; 128 | -------------------------------------------------------------------------------- /lib/gearmanode/load-balancing.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This script represents implementation of load balancing strategy. 3 | * 4 | * (C) 2013 Vaclav Sykora 5 | * Apache License, Version 2.0, http://www.apache.org/licenses/ 6 | * 7 | */ 8 | 9 | var util = require('util'), 10 | winston = require('winston'), 11 | common = require('./common'); 12 | JS_CONSTANTS = require('./job-server').CONSTANTS; 13 | 14 | 15 | // After what time [s] can be a failed node reused in load balancing. 16 | //DEFAULT_RECOVER_TIME = 30; 17 | 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @params nodeCount count of nodes to be balanced 23 | */ 24 | var LBStrategy = function(nodeCount) { 25 | if (!common.isNumber(nodeCount)) { return new Error('count of nodes is not number'); } 26 | 27 | this.nodeCount = nodeCount; 28 | this.badNodes = {}; 29 | this.recoverTime = JS_CONSTANTS.DEFAULT_RECOVER_TIME; 30 | }; 31 | 32 | /** Static logger. */ 33 | LBStrategy.logger = winston.loggers.get('LBStrategy'); 34 | 35 | 36 | /** 37 | * Gets index of node to be used for next request. 38 | * 39 | * @return `null` if there is no one next 40 | */ 41 | LBStrategy.prototype.nextIndex = common.abstractMethod; 42 | 43 | 44 | /** 45 | * Marks an index as good that means it can be used for next server calls. 46 | * 47 | * @param idx index to be marked as good 48 | */ 49 | LBStrategy.prototype.goodOne = function(idx) { 50 | delete this.badNodes[idx]; 51 | }; 52 | 53 | 54 | /** 55 | * Marks an index as bad that means it will be not used until: 56 | * * there is other 'good' node 57 | * * timeout 58 | * 59 | * @param idx index to be marked as good 60 | */ 61 | LBStrategy.prototype.badOne = function(idx) { 62 | if (idx < this.nodeCount && common.isNumber(idx)) { 63 | this.badNodes[idx] = new Date(); 64 | } 65 | }; 66 | 67 | 68 | /** 69 | * Tries to find a new node if the given failed. 70 | * 71 | * @param badIdx index of bad node 72 | * @return `null` if no one found 73 | */ 74 | LBStrategy.prototype._searchNextGood = function (badIdx) { 75 | var candidate, timeoutCandidate = null, failureTime; 76 | LBStrategy.logger.log('warn', 'identified bad node, idx=%d, age=%d [ms]', badIdx, (this.badNodes[badIdx] - new Date())); 77 | 78 | for (var i = 0; i < this.nodeCount; i ++) { 79 | candidate = (i + badIdx) % this.nodeCount; 80 | 81 | if (this.badNodes.hasOwnProperty(candidate)) { 82 | // select a timeout based candidate 83 | failureTime = this.badNodes[candidate]; 84 | if ((new Date() - failureTime) > this.recoverTime) { 85 | timeoutCandidate = candidate; 86 | LBStrategy.logger.log('debug', 'node timeout recovery, idx=%d', candidate); 87 | this.goodOne(candidate) 88 | } 89 | } else { 90 | LBStrategy.logger.log('debug', 'found good node, idx=%d', candidate); 91 | return candidate; 92 | } 93 | } 94 | 95 | // no good index found -> try timeouted one 96 | if (timeoutCandidate !== null) { 97 | LBStrategy.logger.log('debug', 'good node not found, delivering timeouted one, idx=%d', timeoutCandidate); 98 | return timeoutCandidate; 99 | } 100 | 101 | LBStrategy.logger.log('error', 'all nodes invalid, no candidate more'); 102 | return null; 103 | }; 104 | 105 | 106 | 107 | /** 108 | * Implementation of Sequence strategy. 109 | * Assigns work in the order of nodes defined by the client initialization. 110 | * 111 | * @params nodeCount count of nodes to be balanced 112 | */ 113 | var Sequence = exports.Sequence = function(nodeCount) { 114 | // parent constructor 115 | var returned = LBStrategy.call(this, nodeCount); 116 | if (returned instanceof Error) { return returned; } 117 | }; 118 | 119 | // inheritance 120 | util.inherits(Sequence, LBStrategy); 121 | 122 | 123 | /** 124 | * @inheritedDoc 125 | */ 126 | Sequence.prototype.nextIndex = function () { 127 | if (this.lastIndex === null || this.lastIndex === undefined) { this.lastIndex = 0; } 128 | 129 | if (this.badNodes.hasOwnProperty(this.lastIndex)) { 130 | this.lastIndex = this._searchNextGood(this.lastIndex); 131 | } 132 | return this.lastIndex; 133 | }; 134 | 135 | 136 | 137 | /** 138 | * Implementation of Round Robin strategy. 139 | * Assigns work in round-robin order per nodes defined by the client initialization. 140 | * 141 | * @params nodeCount count of nodes to be balanced 142 | */ 143 | var RoundRobin = exports.RoundRobin = function(nodeCount) { 144 | // parent constructor 145 | var returned = LBStrategy.call(this, nodeCount); 146 | if (returned instanceof Error) { return returned; } 147 | }; 148 | 149 | // inheritance 150 | util.inherits(RoundRobin, LBStrategy); 151 | 152 | 153 | /** 154 | * @inheritedDoc 155 | */ 156 | RoundRobin.prototype.nextIndex = function () { 157 | if (this.lastIndex === null || this.lastIndex === undefined) { this.lastIndex = -1; } 158 | 159 | this.lastIndex = (this.lastIndex + 1) % this.nodeCount; 160 | if (this.badNodes.hasOwnProperty(this.lastIndex)) { 161 | this.lastIndex = this._searchNextGood(this.lastIndex); 162 | } 163 | return this.lastIndex; 164 | }; 165 | -------------------------------------------------------------------------------- /test/test-job.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | sinon = require('sinon'), 3 | events = require('events'), 4 | gearmanode = require('../lib/gearmanode'), 5 | Job = require('../lib/gearmanode/job').Job, 6 | protocol = require('../lib/gearmanode/protocol'); 7 | 8 | 9 | describe('Job', function() { 10 | var c, j; 11 | beforeEach(function() { 12 | c = gearmanode.client(); 13 | j = new Job(c, { name: 'reverse', payload: 'hi' }); 14 | j.emit = sinon.spy(); 15 | }); 16 | 17 | describe('#constructor', function() { 18 | it('should return default instance of Job', function() { 19 | j.should.be.an.instanceof(Job); 20 | j.clientOrWorker.should.be.an.instanceof(gearmanode.Client); 21 | j.name.should.equal('reverse'); 22 | j.payload.should.equal('hi'); 23 | j.background.should.be.false; 24 | j.priority.should.equal('NORMAL'); 25 | j.encoding.should.equal('utf8'); 26 | should.not.exist(j.unique); 27 | should.exist(j.uuid); 28 | should.not.exist(j.jobServer); 29 | should.not.exist(j.toStringEncoding); 30 | }) 31 | it('should store additional options', function() { 32 | var job = new Job(c, 33 | { name: 'reverse', payload: 'hi', background: true, priority: 'HIGH', toStringEncoding: 'ascii', encoding: 'ascii', unique: 'foo' } 34 | ); 35 | job.should.be.an.instanceof(Job); 36 | job.name.should.equal('reverse'); 37 | job.payload.should.equal('hi'); 38 | job.background.should.be.true; 39 | job.priority.should.equal('HIGH'); 40 | job.encoding.should.equal('ascii'); 41 | job.toStringEncoding.should.equal('ascii'); 42 | job.unique.should.equal('foo'); 43 | }) 44 | it('should return error when missing mandatory options', function() { 45 | var job = new Job(); 46 | job.should.be.an.instanceof(Error); 47 | job = new Job(null); 48 | job.should.be.an.instanceof(Error); 49 | job = new Job(c); 50 | job.should.be.an.instanceof(Error); 51 | job = new Job(c, true); 52 | job.should.be.an.instanceof(Error); 53 | job = new Job(c, {}); 54 | job.should.be.an.instanceof(Error); 55 | job = new Job(c, { name: 'foo' }); 56 | job.should.be.an.instanceof(Error); 57 | job = new Job(c, { payload: 'foo' }); 58 | job.should.be.an.instanceof(Error); 59 | }) 60 | it('should return error when incorrect options', function() { 61 | var job = new Job(c, { name: 'foo', payload: 'bar', background: 'baz' }); 62 | job.should.be.an.instanceof(Error); 63 | job = new Job(c, { name: 'foo', payload: 'bar', priority: 'baz' }); 64 | job.should.be.an.instanceof(Error); 65 | job = new Job(c, { name: 'foo', payload: 'bar', encoding: 'baz' }); 66 | job.should.be.an.instanceof(Error); 67 | job = new Job(c, { name: 'foo', payload: 'bar', toStringEncoding: 'baz' }); 68 | job.should.be.an.instanceof(Error); 69 | }) 70 | }) 71 | 72 | 73 | describe('#close', function() { 74 | it('should clean up object', function() { 75 | j.handle = 'H:localhost:22'; 76 | j.processing = true; 77 | j.clientOrWorker.jobs[j.getUid()] = j; 78 | j.close(); 79 | j.processing.should.be.false; 80 | j.closed.should.be.true; 81 | should.not.exist(c.jobs[j.getUid()]); 82 | }) 83 | it('should emit event on itself', function() { 84 | j.close(); 85 | j.emit.calledOnce.should.be.true; 86 | j.emit.calledWith('close').should.be.true; 87 | }) 88 | it('should remove all listeners', function() { 89 | j.on('created', function() {}); 90 | j.on('close', function() {}); 91 | events.EventEmitter.listenerCount(j, 'created').should.equal(1); 92 | events.EventEmitter.listenerCount(j, 'close').should.equal(1); 93 | j.close(); 94 | events.EventEmitter.listenerCount(j, 'created').should.equal(0); 95 | events.EventEmitter.listenerCount(j, 'close').should.equal(0); 96 | }) 97 | it('should emit `close` event on incomplete job when associated Client is closed', function() { 98 | j.handle = 'handle'; 99 | c.jobs[j.handle] = j; 100 | c.close(); 101 | j.emit.calledOnce.should.be.true; 102 | j.emit.calledWith('close').should.be.true; 103 | }) 104 | }) 105 | 106 | 107 | describe('#getPacketType', function() { 108 | it('should return correct packet type', function() { 109 | var job = new Job(c, { name: 'reverse', payload: 'hi' }); 110 | job.getPacketType().should.equal(protocol.PACKET_TYPES.SUBMIT_JOB); 111 | job = new Job(c, { name: 'reverse', payload: 'hi', priority: 'LOW' }); 112 | job.getPacketType().should.equal(protocol.PACKET_TYPES.SUBMIT_JOB_LOW); 113 | job = new Job(c, { name: 'reverse', payload: 'hi', priority: 'HIGH' }); 114 | job.getPacketType().should.equal(protocol.PACKET_TYPES.SUBMIT_JOB_HIGH); 115 | job = new Job(c, { name: 'reverse', payload: 'hi', background: true }); 116 | job.getPacketType().should.equal(protocol.PACKET_TYPES.SUBMIT_JOB_BG); 117 | job = new Job(c, { name: 'reverse', payload: 'hi', background: true, priority: 'LOW' }); 118 | job.getPacketType().should.equal(protocol.PACKET_TYPES.SUBMIT_JOB_LOW_BG); 119 | job = new Job(c, { name: 'reverse', payload: 'hi', background: true, priority: 'HIGH' }); 120 | job.getPacketType().should.equal(protocol.PACKET_TYPES.SUBMIT_JOB_HIGH_BG); 121 | }) 122 | }) 123 | 124 | }) 125 | -------------------------------------------------------------------------------- /lib/gearmanode/job.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This script introduces a class representing a Gearman job 3 | * from both client's and worker's perspective. 4 | * 5 | * (C) 2013 Vaclav Sykora 6 | * Apache License, Version 2.0, http://www.apache.org/licenses/ 7 | * 8 | */ 9 | 10 | var winston = require('winston'), 11 | util = require('util'), 12 | events = require('events'), 13 | protocol = require('./protocol'), 14 | common = require('./common'); 15 | 16 | 17 | /** 18 | * @class Job 19 | * @classdesc A warapper representing a gearman task (job). 20 | * @constructor 21 | * 22 | * @param options literal representing the job 23 | * @param {string} options.name job name 24 | * @param {Buffer|string} options.payload job data 25 | * @param {string} options.unique unique identifiter for this job, the identifier is assigned by the client, Optional 26 | * TODO complete all parameters 27 | */ 28 | var Job = exports.Job = function (clientOrWorker, options) { 29 | var self = this; 30 | var pattern, returned; 31 | 32 | options = options || {}; 33 | 34 | this.uuid = common.createUUID(); 35 | 36 | if (!clientOrWorker || !clientOrWorker.hasOwnProperty('_type')) { 37 | return new Error('neither Client nor Worker reference'); 38 | } 39 | 40 | // VALIDATION - common 41 | pattern = { 42 | name: 'mandatory', payload: 'mandatory', unique: 'optional' 43 | }; 44 | returned = common.verifyOptions({ name: options.name, payload: options.payload }, pattern); 45 | if (returned instanceof Error) { return returned; } 46 | 47 | this.clientOrWorker = clientOrWorker; 48 | this.name = options.name; 49 | this.payload = options.payload; 50 | if (options.hasOwnProperty('unique')) { 51 | this.unique = options.unique; 52 | } 53 | 54 | // Client ========================= 55 | if (this.clientOrWorker._type === 'Client') { 56 | // VALIDATION 57 | common.mixin({ 58 | background: false, priority: 'NORMAL', toStringEncoding: 'optional', encoding: 'utf8' }, pattern); // TODO remove 'encoding' in next release after 03/2015 59 | returned = common.verifyAndSanitizeOptions(options, pattern); 60 | if (returned instanceof Error) { return returned; } 61 | returned = common.verifyOptions( 62 | { background: options.background, priority: options.priority, encoding: options.encoding }, 63 | { background: [true, false], priority: ['LOW', 'NORMAL', 'HIGH'], encoding: ['utf8', 'ascii'] } 64 | ); 65 | if (returned instanceof Error) { return returned; } 66 | // validate encoding 67 | if (options.toStringEncoding && !Buffer.isEncoding(options.toStringEncoding)) { 68 | return new Error('invalid encoding: ' + options.toStringEncoding); 69 | } 70 | if (options.toStringEncoding) { 71 | this.toStringEncoding = options.toStringEncoding; 72 | } 73 | 74 | this.background = options.background; 75 | this.priority = options.priority; 76 | if (options.hasOwnProperty('unique')) { 77 | this.unique = options.unique; 78 | } 79 | this.encoding = options.encoding; // TODO remove 'encoding' in next release after 03/2015 80 | // timeout 81 | 82 | this.processing = false; 83 | } 84 | 85 | // Worker ========================= 86 | if (this.clientOrWorker._type === 'Worker') { 87 | // VALIDATION 88 | common.mixin({ handle: 'mandatory', jobServerUid: 'mandatory' }, pattern); 89 | common.verifyAndSanitizeOptions(options, pattern); 90 | if (returned instanceof Error) { return returned; } 91 | 92 | this.handle = options.handle; 93 | this.jobServerUid = options.jobServerUid; 94 | } 95 | 96 | events.EventEmitter.call(this); 97 | }; 98 | 99 | // inheritance 100 | util.inherits(Job, events.EventEmitter); 101 | 102 | // static logger 103 | Job.logger = winston.loggers.get('Job'); 104 | 105 | 106 | /** 107 | * Closes the job for future processing by Gearman and releases this object's resources immediately. 108 | * Sets property 'closed' to 'true'. 109 | */ 110 | Job.prototype.close = function () { 111 | if (this.hasOwnProperty('processing')) { // [Client] 112 | this.processing = false; 113 | } 114 | if (this.hasOwnProperty('clientOrWorker')) { 115 | if (this.clientOrWorker.hasOwnProperty('jobs')) { // [Client] delete this job list in case of Client 116 | delete this.clientOrWorker.jobs[this.getUid()]; 117 | } 118 | // AAA delete this.clientOrWorker; // bidirectional association management 119 | } 120 | this.closed = true; 121 | this.emit('close'); // trigger event 122 | this.removeAllListeners(); 123 | }; 124 | 125 | 126 | /** 127 | * Gets packet type for submiting a job. according to job's options. 128 | */ 129 | Job.prototype.getPacketType = function () { 130 | if (this.background) { 131 | if (this.priority == 'LOW') { return protocol.PACKET_TYPES.SUBMIT_JOB_LOW_BG; } 132 | if (this.priority == 'NORMAL') { return protocol.PACKET_TYPES.SUBMIT_JOB_BG; } 133 | if (this.priority == 'HIGH') { return protocol.PACKET_TYPES.SUBMIT_JOB_HIGH_BG; } 134 | } else { 135 | if (this.priority == 'LOW') { return protocol.PACKET_TYPES.SUBMIT_JOB_LOW; } 136 | if (this.priority == 'NORMAL') { return protocol.PACKET_TYPES.SUBMIT_JOB; } 137 | if (this.priority == 'HIGH') { return protocol.PACKET_TYPES.SUBMIT_JOB_HIGH; } 138 | } 139 | }; 140 | 141 | 142 | /** 143 | * Returns a human readable string representation of the object. 144 | * 145 | * @method 146 | * @returns {string} object description 147 | */ 148 | Job.prototype.toString = function() { // #unit: not needed 149 | return 'Job(uuid=' + this.uuid + ', handle=' + this.handle + ', server=' + this.jobServerUid + ')'; 150 | }; 151 | 152 | 153 | /** 154 | * Returns unique identification of this job which consists of 155 | * job server UID, '_' delimiter and job handle. 156 | * 157 | * @method 158 | * @returns {string} unique identification of the job 159 | */ 160 | Job.prototype.getUid = function () { // #unit: not needed 161 | // if (!common.isString(this.handle) || !common.isString(this.jobServerUid)) { 162 | // Job.logger.log('error', 'unknown handle or server, job=%s', this.toString()); 163 | // this.emit('error', new Error('unknown handle or server')); 164 | // } 165 | 166 | return this.jobServerUid + '#' + this.handle; 167 | }; 168 | -------------------------------------------------------------------------------- /test/test-common.js: -------------------------------------------------------------------------------- 1 | 2 | var should = require('should'), 3 | util = require('util'), 4 | common = require('../lib/gearmanode/common'); 5 | 6 | describe('common', function() { 7 | 8 | describe('#verifyOptions()', function() { 9 | var pattern = { foo: 'mandatory', baz: 'optional', bar: [1, 2, 3] }; 10 | 11 | it('should return error when options not presented', function() { 12 | var undf; 13 | common.verifyOptions(undf, pattern).should.be.an.instanceof(Error); 14 | common.verifyOptions(null, pattern).should.be.an.instanceof(Error); 15 | }) 16 | it('should return error when pattern not presented', function() { 17 | var undf; 18 | common.verifyOptions({}, undf).should.be.an.instanceof(Error); 19 | common.verifyOptions({}, null).should.be.an.instanceof(Error); 20 | }) 21 | 22 | it('should return error when an unknown option found', function() { 23 | common.verifyOptions({ pipapo: 1 }, pattern).should.be.an.instanceof(Error); 24 | }) 25 | it('should return error when an option not in set of allowed values', function() { 26 | common.verifyOptions({ foo: true, bar: 11111 }, pattern).should.be.an.instanceof(Error); 27 | common.verifyOptions({ foo: true, bar: null }, pattern).should.be.an.instanceof(Error); 28 | common.verifyOptions({ foo: true, bar: undefined }, pattern).should.be.an.instanceof(Error); 29 | }) 30 | 31 | it('should return error when missing a mandatory option/Array', function() { 32 | common.verifyOptions({ bar: 1 }, pattern).should.be.an.instanceof(Error); 33 | common.verifyOptions({ foo: true }, pattern).should.be.an.instanceof(Error); 34 | common.verifyOptions({ foo: null, bar: 1 }, pattern).should.be.an.instanceof(Error); 35 | common.verifyOptions({ foo: undefined, bar: 1 }, pattern).should.be.an.instanceof(Error); 36 | }) 37 | 38 | describe('OK', function() { 39 | var opts = { foo: true, baz: 'here', bar: 1 }; 40 | var rslt = common.verifyOptions(opts, pattern); 41 | 42 | it('should return the options when all validations OK', function() { 43 | rslt.should.equal(opts); 44 | }) 45 | it('should not modify options', function() { 46 | Object.keys(opts).length.should.equal(3); 47 | opts.foo.should.equal(true); 48 | opts.baz.should.equal('here'); 49 | opts.bar.should.equal(1); 50 | }) 51 | }) 52 | 53 | }) 54 | 55 | describe('#verifyAndSanitizeOptions()', function() { 56 | var pattern = { alpha: 'bravo', charly: 'delta' }; 57 | 58 | it('should behave like #verifyOptions in validation', function() { 59 | common.verifyAndSanitizeOptions({ alpha: true, charly: true, pipapo: 1 }, pattern).should.be.an.instanceof(Error); 60 | }) 61 | it('should not modify options when all provided', function() { 62 | var opts = { alpha: true, charly: false }; 63 | var rslt = common.verifyAndSanitizeOptions(opts, pattern); 64 | Object.keys(opts).length.should.equal(2); 65 | opts.alpha.should.equal(true); 66 | opts.charly.should.equal(false); 67 | }) 68 | it('should not modify options when value is null', function() { 69 | var opts = { alpha: null }; 70 | var rslt = common.verifyAndSanitizeOptions(opts, pattern); 71 | Object.keys(opts).length.should.equal(2); 72 | should.strictEqual(opts.alpha, null); 73 | opts.charly.should.equal('delta'); 74 | }) 75 | it('should modify options when not provided', function() { 76 | var opts = { charly: false }; 77 | var rslt = common.verifyAndSanitizeOptions(opts, pattern); 78 | opts.alpha.should.equal('bravo'); 79 | opts.charly.should.equal(false); 80 | // -- 81 | opts = { alpha: true }; 82 | rslt = common.verifyAndSanitizeOptions(opts, pattern); 83 | opts.alpha.should.be.true; 84 | opts.charly.should.equal('delta'); 85 | // -- 86 | opts = {}; 87 | rslt = common.verifyAndSanitizeOptions(opts, pattern); 88 | Object.keys(opts).length.should.equal(2); 89 | opts.alpha.should.equal('bravo'); 90 | opts.charly.should.equal('delta'); 91 | }) 92 | it('should modify options when value is undefined', function() { 93 | var opts = { alpha: undefined, charly: true }; 94 | var rslt = common.verifyAndSanitizeOptions(opts, pattern); 95 | Object.keys(opts).length.should.equal(2); 96 | should.strictEqual(opts.alpha, 'bravo'); 97 | opts.charly.should.be.true; 98 | }) 99 | 100 | // BF ----------------------------------- 101 | 102 | it('should modify options when default value is boolean', function() { 103 | var opts = {}; 104 | var rslt = common.verifyAndSanitizeOptions(opts, { foo: true, bar: false }); 105 | Object.keys(opts).length.should.equal(2); 106 | opts.foo.should.be.true; 107 | opts.bar.should.be.false; 108 | }) 109 | }) 110 | 111 | 112 | describe('#isString()', function() { 113 | it('should identify given object as a String', function() { 114 | common.isString('string literal').should.be.true; 115 | common.isString(' ').should.be.true; 116 | common.isString('').should.be.true; 117 | common.isString(new String('String object')).should.be.true; 118 | common.isString(1).should.be.false; // number literal 119 | common.isString(true).should.be.false; // boolean literal 120 | common.isString({}).should.be.false; // object 121 | common.isString(null).should.be.false; 122 | common.isString(undefined).should.be.false; 123 | }) 124 | }) 125 | 126 | 127 | describe('#isNumber()', function() { 128 | it('should identify given object as a Number', function() { 129 | common.isNumber(5).should.be.true; 130 | common.isNumber(new Number(5)).should.be.true; 131 | common.isNumber('').should.be.false; 132 | common.isNumber('123').should.be.false; // string literal 133 | common.isNumber(true).should.be.false; // boolean literal 134 | common.isNumber({}).should.be.false; // object 135 | common.isNumber(null).should.be.false; 136 | common.isNumber(undefined).should.be.false; 137 | }) 138 | }) 139 | 140 | 141 | describe('#clone()', function() { 142 | it('should clone a simple object', function() { 143 | var from = {alpha: 'bravo', charly: 'delta'}; 144 | var cloned = common.clone(from); 145 | from.should.not.equal(cloned); 146 | Object.keys(cloned).length.should.equal(2); 147 | cloned.alpha.should.equal('bravo'); 148 | cloned.charly.should.equal('delta'); 149 | from.foo = 'bar'; 150 | from.foo.should.equal('bar'); 151 | should.not.exist(cloned.foo); 152 | }) 153 | it('should clone an Array', function() { 154 | var from = ['first', 'second']; 155 | var cloned = common.clone(from); 156 | cloned.should.be.an.instanceof(Array); 157 | from.should.not.equal(cloned); 158 | cloned.length.should.equal(2); 159 | cloned[0].should.equal('first'); 160 | cloned[1].should.equal('second'); 161 | from.push('third'); 162 | from.length.should.equal(3); 163 | cloned.length.should.equal(2); 164 | }) 165 | }) 166 | 167 | }) -------------------------------------------------------------------------------- /test/test-load-balancing.js: -------------------------------------------------------------------------------- 1 | 2 | var should = require('should'), 3 | util = require('util'), 4 | sinon = require('sinon'), 5 | gearmanode = require('../lib/gearmanode'), 6 | Sequence = require('../lib/gearmanode/load-balancing').Sequence, 7 | RoundRobin = require('../lib/gearmanode/load-balancing').RoundRobin; 8 | 9 | 10 | describe('load-balancing', function() { 11 | var lb; 12 | beforeEach(function() { 13 | lb = new Sequence(2); 14 | }); 15 | 16 | 17 | describe('#LBStrategy', function() { 18 | 19 | 20 | describe('#constructor', function() { 21 | it('should return default instance of Sequence', function() { 22 | lb.should.be.an.instanceof(Sequence); 23 | lb.nodeCount.should.equal(2); 24 | Object.keys(lb.badNodes).length.should.equal(0); 25 | lb.recoverTime.should.equal(30000); 26 | }) 27 | it('should return error when violated validation', function() { 28 | // duplicate servers 29 | lb = new Sequence(); 30 | lb.should.be.an.instanceof(Error); 31 | lb = new Sequence('foo'); 32 | lb.should.be.an.instanceof(Error); 33 | lb = new RoundRobin(); 34 | lb.should.be.an.instanceof(Error); 35 | lb = new RoundRobin('foo'); 36 | lb.should.be.an.instanceof(Error); 37 | }) 38 | }) 39 | 40 | 41 | describe('#badOne', function() { 42 | it('should store given index with timestamp', function() { 43 | Object.keys(lb.badNodes).length.should.equal(0); 44 | lb.badOne(1); 45 | Object.keys(lb.badNodes).length.should.equal(1); 46 | lb.badNodes.should.have.ownProperty(1); 47 | lb.badNodes[1].should.be.an.instanceof(Date); 48 | (new Date >= lb.badNodes[1]).should.be.true; 49 | }) 50 | it('should ignore index bigger than node count', function() { 51 | Object.keys(lb.badNodes).length.should.equal(0); 52 | lb.badOne(2); 53 | Object.keys(lb.badNodes).length.should.equal(0); 54 | }) 55 | it('should accept only number', function() { 56 | Object.keys(lb.badNodes).length.should.equal(0); 57 | lb.badOne('0'); 58 | Object.keys(lb.badNodes).length.should.equal(0); 59 | lb.badOne(null); 60 | Object.keys(lb.badNodes).length.should.equal(0); 61 | lb.badOne(false); 62 | Object.keys(lb.badNodes).length.should.equal(0); 63 | }) 64 | }) 65 | }) 66 | 67 | 68 | describe('#Sequence', function() { 69 | 70 | 71 | describe('#nextIndex', function() { 72 | it('should return corresponding index if everything OK', function() { 73 | lb.nextIndex().should.equal(0); 74 | lb.nextIndex().should.equal(0); 75 | lb.nextIndex().should.equal(0); 76 | }) 77 | it('should return next index if the current fails', function() { 78 | lb.badOne(0); 79 | lb.nextIndex().should.equal(1); 80 | lb.nextIndex().should.equal(1); 81 | }) 82 | it('should return null if all nodes fails', function() { 83 | lb.nextIndex().should.equal(0); 84 | lb.badOne(0); 85 | lb.badOne(1); 86 | should.not.exist(lb.nextIndex()); 87 | }) 88 | }) 89 | 90 | }) 91 | 92 | 93 | describe('#RoundRobin', function() { 94 | 95 | 96 | describe('#nextIndex', function() { 97 | it('should return corresponding index if everything OK', function() { 98 | lb = new RoundRobin(2); 99 | lb.nextIndex().should.equal(0); 100 | lb.nextIndex().should.equal(1); 101 | lb.nextIndex().should.equal(0); 102 | lb.nextIndex().should.equal(1); 103 | }) 104 | it('should return next index if the current fails', function() { 105 | lb = new RoundRobin(2); 106 | lb.badOne(0); 107 | lb.nextIndex().should.equal(1); 108 | lb.nextIndex().should.equal(1); 109 | }) 110 | it('should return null if all nodes fails', function() { 111 | lb = new RoundRobin(2); 112 | lb.nextIndex().should.equal(0); 113 | lb.badOne(0); 114 | lb.badOne(1); 115 | should.not.exist(lb.nextIndex()); 116 | }) 117 | }) 118 | 119 | }) 120 | 121 | 122 | describe('#recoverTime', function() { 123 | it('should try failed nodes again after recover time', function(node) { 124 | lb.recoverTime = 20; 125 | lb.badOne(0); 126 | lb.badOne(1); 127 | Object.keys(lb.badNodes).length.should.equal(2); 128 | lb.badNodes.hasOwnProperty(0).should.be.true; 129 | lb.badNodes.hasOwnProperty(1).should.be.true; 130 | should.not.exist(lb.nextIndex()); 131 | setTimeout(function() { 132 | lb.badNodes.hasOwnProperty(0).should.be.true; 133 | lb.badNodes.hasOwnProperty(1).should.be.true; 134 | lb.nextIndex(); 135 | Object.keys(lb.badNodes).length.should.equal(0); 136 | lb.badNodes.hasOwnProperty(0).should.be.false; 137 | lb.badNodes.hasOwnProperty(1).should.be.false; 138 | node(); 139 | }, 30); 140 | }) 141 | }) 142 | 143 | 144 | 145 | describe('#BF', function() { 146 | it('should try to submit job through second job server if the first not running', function(done) { 147 | var c = gearmanode.client({servers: [{port: 4731}, {}]}); 148 | var job = c.submitJob('reverse', 'hello world!'); 149 | 150 | c.once('socketError', function(uid, err) { 151 | uid.should.equal('localhost:4731'); 152 | should.exist(err); 153 | err.should.be.an.instanceof(Error); 154 | err.code.should.be.equal('ECONNREFUSED'); 155 | done(); 156 | }); 157 | job.once('submited', function() { 158 | job.jobServerUid.should.equal('localhost:4730'); 159 | }); 160 | }) 161 | it('#4: failure of existing server connection should cause load balancing to other one', function(done) { 162 | var c = gearmanode.client({servers: [{}, {port: 4731}]}); 163 | c.jobServers[0].connect(function(err) { 164 | should.not.exist(err); // successfully connection 165 | c.jobServers[0].connected.should.be.true; 166 | c.jobServers[0].socket.emit('end', {code: 'EPIPE'}); // simulate termination of connection by other end 167 | }); 168 | c.once('error', function(err) { // all servers fails -> there should be emitted an error 169 | should.exist(err); 170 | err.should.be.an.instanceof(Error); 171 | }); 172 | c.once('socketDisconnect', function(uid30) { // invoked if connection forcely terminated by my `emit('end')` 173 | uid30.should.equal('localhost:4730'); 174 | c.once('socketError', function(uid31, err) { // connection failure is expected 175 | uid31.should.equal('localhost:4731'); 176 | should.exist(err); 177 | err.should.be.an.instanceof(Error); 178 | err.code.should.be.equal('ECONNREFUSED'); 179 | done(); 180 | }); 181 | c.submitJob('reverse', 'hi'); 182 | }); 183 | }) 184 | }) 185 | 186 | }) -------------------------------------------------------------------------------- /lib/gearmanode/protocol.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This script represents an operator over Gearman protocol. 3 | * {@see http://gearman.org/protocol} 4 | * 5 | * (C) 2013 Vaclav Sykora 6 | * Apache License, Version 2.0, http://www.apache.org/licenses/ 7 | * 8 | */ 9 | 10 | 11 | var winston = require('winston'), 12 | util = require('util'); 13 | 14 | 15 | // various constants used in protocol 16 | var constants = exports.CONSTANTS = { 17 | TYPE_REQ: 1, 18 | TYPE_RESP: 2, 19 | // -- 20 | HEADER_REQ: 0x00524551, 21 | HEADER_RESP: 0x00524553, 22 | HEADER_LEN: 12, 23 | // -- 24 | UNKNOWN_OPTION: 'UNKNOWN_OPTION' 25 | }; 26 | var logger = winston.loggers.get('protocol'); 27 | 28 | 29 | /** @enum */ 30 | exports.DEFINITION = { 31 | CAN_DO: [1, constants.TYPE_REQ], // W->J: FUNC 32 | CANT_DO: [2, constants.TYPE_REQ], // W->J: FUNC 33 | RESET_ABILITIES: [3, constants.TYPE_REQ], // # W->J: -- 34 | PRE_SLEEP: [4, constants.TYPE_REQ], // W->J: -- 35 | // 5 (unused) 36 | NOOP: [6, constants.TYPE_RESP, ''], // J->W: -- 37 | SUBMIT_JOB: [7, constants.TYPE_REQ], // C->J: FUNC[0]UNIQ[0]ARGS 38 | JOB_CREATED: [8, constants.TYPE_RESP, 'L'], // J->C: HANDLE 39 | GRAB_JOB: [9, constants.TYPE_REQ], // W->J: -- 40 | NO_JOB: [10, constants.TYPE_RESP, ''], // J->W: -- 41 | JOB_ASSIGN: [11, constants.TYPE_RESP, 'nnb'], // J->W: HANDLE[0]FUNC[0]ARG 42 | WORK_STATUS: [12, constants.TYPE_REQ | constants.TYPE_RESP, 'nnL'], // W->J/C: HANDLE[0]NUMERATOR[0]DENOMINATOR 43 | WORK_COMPLETE: [13, constants.TYPE_REQ | constants.TYPE_RESP, 'nb'], // W->J/C: HANDLE[0]RES 44 | WORK_FAIL: [14, constants.TYPE_REQ | constants.TYPE_RESP, 'L'], // W->J/C: HANDLE 45 | GET_STATUS: [15, constants.TYPE_REQ], // C->J: HANDLE 46 | ECHO_REQ: [16, constants.TYPE_REQ], // C/W->J: TEXT 47 | ECHO_RES: [17, constants.TYPE_RESP, 'L'], // J->C/W: TEXT 48 | SUBMIT_JOB_BG: [18, constants.TYPE_REQ], // C->J: FUNC[0]UNIQ[0]ARGS 49 | ERROR: [19, constants.TYPE_RESP, 'nL'], // J->C/W: ERRCODE[0]ERR_TEXT 50 | STATUS_RES: [20, constants.TYPE_RESP, 'nnnnL'], // C->J: HANDLE[0]KNOWN[0]RUNNING[0]NUM[0]DENOM 51 | SUBMIT_JOB_HIGH: [21, constants.TYPE_REQ], // C->J: FUNC[0]UNIQ[0]ARGS 52 | SET_CLIENT_ID: [22, constants.TYPE_REQ], // W->J: STRING_NO_WHITESPACE 53 | CAN_DO_TIMEOUT: [23, constants.TYPE_REQ], // W->J: FUNC[0]TIMEOUT 54 | // 24 ALL_YOURS (Not yet implemented) 55 | WORK_EXCEPTION: [25, constants.TYPE_REQ | constants.TYPE_RESP, 'nb'], // W->J/C: HANDLE[0]ARG 56 | OPTION_REQ: [26, constants.TYPE_REQ], // C->J: TEXT 57 | OPTION_RES: [27, constants.TYPE_RESP, 'L'], // J->C: TEXT 58 | WORK_DATA: [28, constants.TYPE_REQ | constants.TYPE_RESP, 'nb'], // W->J/C: HANDLE[0]RES 59 | WORK_WARNING: [29, constants.TYPE_REQ | constants.TYPE_RESP, 'nb'], // W->J/C: HANDLE[0]MSG 60 | GRAB_JOB_UNIQ: [30, constants.TYPE_REQ], // W->J: -- 61 | JOB_ASSIGN_UNIQ: [31, constants.TYPE_RESP, 'nnnb'], // J->W: HANDLE[0]FUNC[0]ARG 62 | SUBMIT_JOB_HIGH_BG: [32, constants.TYPE_REQ], // C->J: FUNC[0]UNIQ[0]ARGS 63 | SUBMIT_JOB_LOW: [33, constants.TYPE_REQ], // C->J: FUNC[0]UNIQ[0]ARGS 64 | SUBMIT_JOB_LOW_BG: [34, constants.TYPE_REQ], // C->J: FUNC[0]UNIQ[0]ARGS 65 | }; 66 | 67 | 68 | // desc=>code - {CAN_DO: 1} 69 | exports.PACKET_TYPES = {}; 70 | // code=>desc - {1: CAN_DO} 71 | exports.PACKET_CODES = {}; 72 | // code=>format for RESP - {19: 'nL'} 73 | exports.PACKET_RESP_FORMAT = {}; 74 | 75 | 76 | var def; 77 | for (var i in exports.DEFINITION) { 78 | if (exports.DEFINITION && exports.DEFINITION.hasOwnProperty(i)) { 79 | def = exports.DEFINITION[i]; 80 | 81 | exports.PACKET_TYPES[i] = def[0]; 82 | exports.PACKET_CODES[def[0]] = i; 83 | 84 | if (constants.TYPE_RESP === def[1]) { 85 | exports.PACKET_RESP_FORMAT[def[0]] = def[2]; 86 | } 87 | } 88 | } 89 | 90 | 91 | /** 92 | * Parses given buffer according to defined format. 93 | * 94 | * *format* 95 | * *N* NULL byte terminated string (default encoding) 96 | * *n* NULL byte terminated string (ASCII encoding) 97 | * *L* last segment of buffer (default encoding) 98 | * *b* last segment of buffer (as Buffer) 99 | * 100 | * return array 101 | * *rslt[0]* number of processed bytes 102 | * *rslt[1..]* chunks with order defined by format 103 | */ 104 | exports.parsePacket = function (buff, format) { 105 | var i, j, key; 106 | var rslt = []; 107 | var offset = constants.HEADER_LEN; 108 | var packetType = buff.readUInt32BE(4); 109 | var packetLength = offset + buff.readUInt32BE(8); 110 | 111 | format = format || ''; 112 | 113 | for (i = 0; i < format.length; i ++) { 114 | key = format.charAt(i); 115 | 116 | if ('N' == key || 'n' == key) { 117 | // find next NULL 118 | for (j = offset; j < buff.length; j ++) { 119 | if (buff[j] == 0) { 120 | break; 121 | } 122 | } 123 | rslt[i + 1] = buff.toString('n' == key ? 'ascii' : undefined, offset, j); 124 | offset = j + 1; // +1 == skip NULL 125 | } else if ('L' == key) { // LAST segment up to packetLength as String 126 | rslt[i + 1] = buff.toString(undefined, offset, packetLength); 127 | offset = packetLength; 128 | } else if ('b' == key) { // LAST segment up to packetLength as Buffer 129 | rslt[i + 1] = buff.slice(offset); 130 | offset = packetLength; 131 | } else { 132 | return new Error('unknow format: ' + format); 133 | } 134 | } 135 | 136 | rslt[0] = offset; 137 | if (logger.isLevelEnabled('verbose')) { 138 | logger.log('verbose', 'packet parsed, bytes=%d, type=%s', offset, exports.PACKET_CODES[packetType]); 139 | } 140 | return rslt; 141 | }; 142 | 143 | 144 | exports.encodePacket = function (packetType, args) { 145 | var i, buff; 146 | var packetLength = 0; 147 | var offset = constants.HEADER_LEN; 148 | 149 | // default values 150 | args = args || []; 151 | 152 | // compute packet length 153 | for (i = 0; i < args.length; i ++) { 154 | if (args[i].constructor.name === 'String') { 155 | packetLength += Buffer.byteLength(args[i]); 156 | } else if (args[i].constructor.name === 'Buffer') { 157 | packetLength += args[i].length; 158 | } else { 159 | packetLength += Buffer.byteLength(args[i].toString()); 160 | } 161 | } 162 | if (args.length > 0) { 163 | packetLength += args.length - 1; // NULL byte terminations 164 | } 165 | 166 | buff = new Buffer(constants.HEADER_LEN + packetLength); 167 | 168 | buff.writeUInt32BE(constants.HEADER_REQ, 0); // \0REQ 169 | buff.writeUInt32BE(packetType, 4); 170 | buff.writeUInt32BE(packetLength, 8); 171 | 172 | // construct packet 173 | for (i = 0; i < args.length; i ++) { 174 | if (args[i].constructor.name === 'String') { 175 | buff.write(args[i], offset); 176 | offset += Buffer.byteLength(args[i]); 177 | } else if (args[i].constructor.name === 'Buffer') { 178 | args[i].copy(buff, offset); 179 | offset += args[i].length; 180 | } else { 181 | buff.write(args[i].toString(), offset); 182 | offset += Buffer.byteLength(args[i].toString()); 183 | } 184 | 185 | if (i < (args.length - 1)) { 186 | buff.writeUInt8(0, offset); // NULL byte terminated chunk 187 | offset ++; 188 | } 189 | } 190 | 191 | logger.log('debug', 'packet encoded, type=%s, buffer.size=%d', 192 | exports.PACKET_CODES[packetType], packetLength + constants.HEADER_LEN); 193 | return buff; 194 | }; 195 | -------------------------------------------------------------------------------- /lib/gearmanode/common.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The GearmaNode Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | * @fileoverview This script represents a set of utilities, helpers and convenience functions for internal use. 17 | * @author vaclav.sykora@google.com (Vaclav Sykora) 18 | * @namespace gearmanode/common 19 | */ 20 | 21 | var util = require('util'); 22 | 23 | 24 | /** 25 | * Verifies given options against a pattern that defines checks applied on each option. 26 | *
27 | * Usage:
28 | * verifyOptions(optionsToBeVerified, {foo: 'mandatory', bar: [true, false], baz: 'optional'}); 29 | * 30 | * @function verifyOptions 31 | * @memberof gearmanode/common 32 | * @param options object to be verified 33 | * @param pattern object where property is an expected option's property and value can be 38 | * @returns {error|object} error if expectation not accompished, otherwise the options 39 | */ 40 | exports.verifyOptions = function (options, pattern) { 41 | if (options === undefined || options == null) { 42 | return new Error('options not presented'); 43 | } 44 | if (pattern === undefined || pattern == null) { 45 | return new Error('pattern not presented'); 46 | } 47 | 48 | // unknown key? 49 | for (var key in options) { 50 | if (options.hasOwnProperty(key)) { 51 | if (!pattern.hasOwnProperty(key)) { 52 | return new Error('unknow option: ' + key); 53 | } 54 | 55 | // option in a set of allowed values 56 | if (util.isArray(pattern[key]) && pattern[key].indexOf(options[key]) === -1) { 57 | return new Error("value '" + options[key] + "' not in defined range, key=" + key); 58 | } 59 | } 60 | } 61 | 62 | // missing mandatory option? 63 | for (var key in pattern) { 64 | if (pattern.hasOwnProperty(key)) { 65 | if (pattern[key] === 'mandatory' 66 | && (!options.hasOwnProperty(key) || options[key] === undefined || options[key] == null)) { 67 | return new Error('missing mandatory option: ' + key); 68 | } 69 | if (util.isArray(pattern[key]) && !options.hasOwnProperty(key)) { 70 | return new Error('missing mandatory Array option: ' + key); 71 | } 72 | } 73 | } 74 | 75 | return options; 76 | }; 77 | 78 | 79 | /** 80 | * The same as verifyOptions with opportunity to define default values 81 | * of paramaters that will be set if missing in options. 82 | * Special values are: 86 | * Usage:
87 | * verifyAndSanitizeOptions(optionsToBeVerified, {foo: 'defaultValue', bar: 100}); 88 | * 89 | * @function verifyAndSanitizeOptions 90 | * @memberof gearmanode/common 91 | * @param options object to be verified 92 | * @param pattern object where property is an expected option's property and value can be 98 | * @returns {error|object} error if expectation not accompished, otherwise the options 99 | */ 100 | exports.verifyAndSanitizeOptions = function (options, pattern) { 101 | var returned = this.verifyOptions(options, pattern); 102 | if (returned instanceof Error) { return returned; } 103 | 104 | // set default values if missing in options 105 | for (var key in pattern) { 106 | if (pattern.hasOwnProperty(key)) { 107 | if (pattern[key] !== undefined && pattern[key] !== 'optional' 108 | && pattern[key] !== 'mandatory' && options[key] === undefined) { 109 | options[key] = pattern[key]; 110 | } 111 | } 112 | } 113 | 114 | return options; 115 | }; 116 | 117 | 118 | /** 119 | * Checks if the given value is a string literal or String object. 120 | * 121 | * @function isString 122 | * @memberof gearmanode/common 123 | * @param o object to be checked 124 | * @returns {boolean} true if the parameter is a string, otherwise false 125 | */ 126 | exports.isString = function (o) { 127 | return o !== undefined && o != null && (typeof o == 'string' || (typeof o == 'object' && o.constructor === String)); 128 | }; 129 | 130 | 131 | /** 132 | * Checks if the given value is a number literal or Number object. 133 | * 134 | * @function isNumber 135 | * @memberof gearmanode/common 136 | * @param o object to be checked 137 | * @returns {boolean} true if the parameter is a number, otherwise false 138 | */ 139 | exports.isNumber = function (o) { 140 | return o !== undefined && o != null && (typeof o == 'number' || (typeof o == 'object' && o.constructor === Number)); 141 | }; 142 | 143 | 144 | /** 145 | * Creates a shallow copy of an object. 146 | * 147 | * @function clone 148 | * @memberof gearmanode/common 149 | * @param from object to be copied 150 | * @returns shallow copy of the input argument 151 | */ 152 | exports.clone = function (from) { 153 | if (from !== Object(from)) { return from; } // not an object 154 | if (Array.isArray(from)) { return from.slice(); } 155 | return exports.mixin(from, {}); 156 | }; 157 | 158 | 159 | /** 160 | * Mixes in properties from source to destination 161 | * and so allows objects to borrow (or inherit) functionality from them with a minimal amount of complexity. 162 | * 163 | * @function mixin 164 | * @memberof gearmanode/common 165 | * @param source object where properties will be copied from 166 | * @param destination object where properties will be copied to 167 | * @returns the destination object 168 | */ 169 | exports.mixin = function (source, destination) { // #unit: TODO test it 170 | for (var k in source) { 171 | if (source.hasOwnProperty(k)) { 172 | destination[k] = source[k]; 173 | } 174 | } 175 | return destination; 176 | }; 177 | 178 | 179 | /** 180 | * Converts given buffer to space separated string of hexadecimal values of byte array. 181 | * 182 | * @function bufferAsHex 183 | * @memberof gearmanode/common 184 | * @param {Buffer} buff to be converted 185 | * @param {Number} maxLen 186 | * @returns {string} textual representation of given buffer 187 | */ 188 | exports.bufferAsHex = function (buff, maxLen) { // #unit: TODO test it 189 | var rslt = ''; 190 | if (maxLen === undefined || maxLen == null) { maxLen = 40; } 191 | for (var i = 0; i < buff.length && i < maxLen; i ++) { 192 | var num = new Number(buff.readUInt8(i)); 193 | rslt += num.toString(16); 194 | if (i < (buff.length - 1)) { rslt += ' '} 195 | } 196 | return rslt; 197 | }; 198 | 199 | 200 | /** 201 | * Null function used for default values of callbacks, etc. 202 | * 203 | * @function nullFunction 204 | * @memberof gearmanode/common 205 | * @type {Function} 206 | * @returns {void} Nothing 207 | */ 208 | exports.nullFunction = function() {}; // #unit: not needed 209 | 210 | 211 | /** 212 | * Can be used as a default implementation of an abstract method. 213 | * There's no need to define such function in JS, but I do anyway because of importance of documentation. 214 | * 215 | * @function abstractMethod 216 | * @memberof gearmanode/common 217 | * @type {Function} 218 | * @return {void} Nothing 219 | * @throws {Error} when invoked to indicate the method should be overridden 220 | */ 221 | exports.abstractMethod = function() { // #unit: not needed 222 | throw new Error('unimplemented abstract method'); 223 | }; 224 | 225 | 226 | /** 227 | * Get UUID based on RFC 4122, section 4.4 (Algorithms for Creating a UUID from Truly Random or Pseudo-Random Number). 228 | * 229 | * @function createUUID 230 | * @memberof gearmanode/common 231 | * @returns {string} textual UUID 232 | */ 233 | exports.createUUID = function () { 234 | // http://www.ietf.org/rfc/rfc4122.txt 235 | var s = []; 236 | var hexDigits = "0123456789abcdef"; 237 | for (var i = 0; i < 36; i++) { 238 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); 239 | } 240 | s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010 241 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01 242 | s[8] = s[13] = s[18] = s[23] = "-"; 243 | 244 | return uuid = s.join(""); 245 | }; 246 | -------------------------------------------------------------------------------- /test/test-client.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | sinon = require('sinon'), 3 | events = require('events'), 4 | gearmanode = require('../lib/gearmanode'), 5 | lb = require('../lib/gearmanode/load-balancing'), 6 | Client = gearmanode.Client, 7 | Job = gearmanode.Job, 8 | JobServer = require('../lib/gearmanode/job-server').JobServer; 9 | 10 | 11 | describe('Client', function() { 12 | var c, js; 13 | beforeEach(function() { 14 | c = gearmanode.client(); 15 | c.emit = sinon.spy(); 16 | js = c.jobServers[0]; 17 | }); 18 | 19 | 20 | describe('#factory', function() { 21 | it('should return default instance of Client', function() { 22 | c.should.be.an.instanceof(Client); 23 | c._type.should.equal('Client'); 24 | should.exist(c.jobServers); 25 | should.exist(c.jobs); 26 | Object.keys(c.jobs).length.should.equal(0); 27 | }) 28 | it('should return error when violated validation', function() { 29 | // duplicate servers 30 | c = gearmanode.client({ servers: [{host: 'localhost'}, {host: 'localhost'}] }); 31 | c.should.be.an.instanceof(Error); 32 | // unknown load balancing strategy 33 | c = gearmanode.client({ loadBalancing: 'AlfaBravo' }); 34 | c.should.be.an.instanceof(Error); 35 | // recoverTime not number 36 | c = gearmanode.client({ recoverTime: '10' }); 37 | c.should.be.an.instanceof(Error); 38 | // unknown 'toStringEncoding' 39 | c = gearmanode.client({ toStringEncoding: 'NonSence' }); 40 | c.should.be.an.instanceof(Error); 41 | }) 42 | it('should set correct load balancer', function() { 43 | should.exist(c.loadBalancer); 44 | c.loadBalancer.should.be.an.instanceof(lb.Sequence); 45 | c.loadBalancer.recoverTime.should.equal(30000); 46 | c = gearmanode.client({ loadBalancing: 'RoundRobin', recoverTime: 100 }); 47 | c.loadBalancer.should.be.an.instanceof(lb.RoundRobin); 48 | c.loadBalancer.recoverTime.should.equal(100); 49 | }) 50 | }) 51 | 52 | 53 | describe('#close', function() { 54 | it('should clean up object', function() { 55 | c.jobs['H:lima:207'] = new Job(c, { name: 'reverse', payload: 'hi' }); // mock the jobs 56 | c.on('submit', function() {}); 57 | events.EventEmitter.listenerCount(c, 'submit').should.equal(1); 58 | Object.keys(c.jobs).length.should.equal(1); 59 | c.close(); 60 | c.closed.should.be.true; 61 | Object.keys(c.jobs).length.should.equal(0); 62 | events.EventEmitter.listenerCount(c, 'submit').should.equal(0); 63 | }) 64 | it('should emit event on itself', function() { 65 | c.close(); 66 | c.emit.calledTwice.should.be.true; // diconnect + close 67 | c.emit.getCall(0).args[0].should.equal('socketDisconnect'); 68 | c.emit.getCall(1).args[0].should.equal('close'); 69 | }) 70 | }) 71 | 72 | 73 | describe('#submitJob', function() { 74 | it('should return job instance', function() { 75 | var job = c.submitJob('reverse', 'hi'); 76 | should.exist(job); 77 | job.should.be.an.instanceof(Job); 78 | job.name.should.equal('reverse'); 79 | job.payload.should.equal('hi'); 80 | job.name.should.equal('reverse'); 81 | job.processing.should.be.true; 82 | should.not.exist(job.jobServerUid); 83 | }) 84 | it('should return error if bad parameters', function() { 85 | c.submitJob().should.be.an.instanceof(Error); 86 | c.submitJob('reverse').should.be.an.instanceof(Error); 87 | }) 88 | it('should emit `submited` if job sent to server', function(done) { 89 | var job = c.submitJob('reverse', 'hi'); 90 | js.jobsWaiting4Created.length.should.equal(0); 91 | job.once('submited', function() { 92 | job.jobServerUid.should.equal(js.getUid()); 93 | js.jobsWaiting4Created.length.should.equal(1); 94 | js.jobsWaiting4Created[0].should.equal(job); 95 | done(); 96 | }); 97 | }) 98 | it('should emit error if submiting fails', function(done) { 99 | c = gearmanode.client({port: 1}); 100 | c.submitJob('reverse', 'hi'); 101 | c.once('error', function(err) { 102 | should.exist(err); 103 | err.should.be.an.instanceof(Error); 104 | done(); 105 | }) 106 | }) 107 | it('should emit `submited` when job server already connected (BF #17)', function(done) { 108 | js.connect(function() { 109 | var job = c.submitJob('reverse', 'hi'); 110 | job.once('submited', function() { 111 | job.jobServerUid.should.equal(js.getUid()); 112 | job.processing.should.be.true; 113 | done(); 114 | }); 115 | }); 116 | 117 | }) 118 | }) 119 | 120 | 121 | describe('#_getJobServer', function() { 122 | it('should return JobServer according to Sequence balancing strategy', function() { 123 | c = gearmanode.client({ servers: [{port: 4730}, {port: 4731}] }); 124 | c._getJobServer().should.equal(c.jobServers[0]); 125 | c._getJobServer().should.equal(c.jobServers[0]); 126 | c._getJobServer().should.equal(c.jobServers[0]); 127 | }) 128 | it('should return JobServer according to RoundRobin balancing strategy', function() { 129 | c = gearmanode.client({ servers: [{port: 4730}, {port: 4731}], loadBalancing: 'RoundRobin' }); 130 | c._getJobServer().should.equal(c.jobServers[0]); 131 | c._getJobServer().should.equal(c.jobServers[1]); 132 | c._getJobServer().should.equal(c.jobServers[0]) 133 | ; }) 134 | }) 135 | 136 | 137 | describe('#Job', function() { 138 | 139 | 140 | describe('#getStatus', function() { 141 | it('should send packet to job server', function() { 142 | var j = new Job(c, {name: 'NAME', payload: 'PAYLOAD', background: true}); 143 | js.send = sinon.spy(); 144 | j.handle = 'HANDLE'; 145 | j.jobServerUid = js.getUid(); 146 | j.getStatus(); 147 | js.send.calledOnce.should.be.true; 148 | }) 149 | it('should validate job to be background', function() { 150 | var j = new Job(c, {name: 'NAME', payload: 'PAYLOAD'}); 151 | js.send = sinon.spy(); 152 | j.getStatus(function(err) { err.should.be.an.instanceof(Error); }) 153 | j.background = true; 154 | j.getStatus(function(err) { err.should.be.an.instanceof(Error); }) 155 | j.handle = 'HANDLE'; 156 | j.getStatus(function(err) { err.should.be.an.instanceof(Error); }) 157 | j.jobServerUid = js.getUid(); 158 | j.getStatus(function(err){}); 159 | js.send.calledOnce.should.be.true; 160 | }) 161 | }) 162 | }) 163 | 164 | 165 | describe('#LoadBalancer', function() { 166 | 167 | 168 | describe('#_getJobServer', function() { 169 | it('should return corresponding job server (Sequence)', function() { 170 | c = gearmanode.client({servers: [{port: 4730}, {port: 4731}]}); 171 | c._getJobServer().should.equal(c.jobServers[0]); 172 | c._getJobServer().should.equal(c.jobServers[0]); 173 | c._getJobServer().should.equal(c.jobServers[0]); 174 | }) 175 | it('should return corresponding job server (RoundRobin)', function() { 176 | c = gearmanode.client({servers: [{port: 4730}, {port: 4731}], loadBalancing: 'RoundRobin'}); 177 | c._getJobServer().should.equal(c.jobServers[0]); 178 | c._getJobServer().should.equal(c.jobServers[1]); 179 | c._getJobServer().should.equal(c.jobServers[0]); 180 | }) 181 | it('should emit error if all job server invalid', function() { 182 | c = gearmanode.client({port: 1}); 183 | c.emit = sinon.spy(); 184 | c.loadBalancer.badOne(0); 185 | c.emit.callCount.should.equal(0); 186 | c._getJobServer(); 187 | c.emit.calledOnce.should.be.true; 188 | }) 189 | }) 190 | 191 | 192 | describe('#(error handling)', function() { 193 | it('should mark job server as bad when connection fails', function(done) { 194 | c = gearmanode.client({port: 1}); 195 | c.emit = sinon.spy(); 196 | c.loadBalancer.badOne = sinon.spy(); 197 | c.jobServers[0].connect(function(err) { 198 | should.exist(err); 199 | err.should.be.an.instanceof(Error); 200 | c.loadBalancer.badOne.calledOnce.should.be.true; 201 | c.loadBalancer.badOne.calledWith(0).should.be.true; 202 | c.emit.calledTwice.should.be.true; // error + disconnect 203 | done(); 204 | }) 205 | }) 206 | }) 207 | }) 208 | 209 | }) 210 | -------------------------------------------------------------------------------- /test/test-all-stack.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | sinon = require('sinon'), 3 | events = require('events'), 4 | gearmanode = require('../lib/gearmanode'), 5 | protocol = require('../lib/gearmanode/protocol'), 6 | Worker = gearmanode.Worker, 7 | Job = gearmanode.Job, 8 | JobServer = require('../lib/gearmanode/job-server').JobServer; 9 | 10 | 11 | describe('Client/Worker', function() { 12 | var w, c; 13 | 14 | beforeEach(function() { 15 | w = gearmanode.worker(); 16 | c = gearmanode.client(); 17 | }); 18 | afterEach(function() { 19 | w.resetAbilities(); 20 | w.close(); 21 | c.close(); 22 | }); 23 | 24 | 25 | describe('#submitJob#complete', function() { 26 | it('should return expected data', function(done) { 27 | w.addFunction('reverse', function (job) { 28 | job.payload.should.be.an.instanceof(Buffer); 29 | job.payload.toString().should.equal('123'); 30 | job.workComplete(job.payload.toString().split("").reverse().join("")) 31 | }); 32 | var job = c.submitJob('reverse', '123'); 33 | job.on('complete', function() { 34 | job.response.should.be.an.instanceof(Buffer); 35 | job.response.toString().should.equal('321'); 36 | done(); 37 | }); 38 | }) 39 | it('should return expected data sent as binary', function(done) { 40 | w.addFunction('reverse', function (job) { 41 | job.payload.should.be.an.instanceof(Buffer); 42 | job.payload.toString().should.equal('123'); 43 | job.workComplete(job.payload.toString().split("").reverse().join("")) 44 | }); 45 | var job = c.submitJob('reverse', new Buffer([49, 50, 51])); 46 | job.on('complete', function() { 47 | job.response.should.be.an.instanceof(Buffer); 48 | job.response.toString().should.equal('321'); 49 | done(); 50 | }); 51 | }) 52 | it('should return expected data with diacritic', function(done) { 53 | w.addFunction('reverse', function (job) { 54 | job.payload.should.be.an.instanceof(Buffer); 55 | job.payload.toString().should.equal('žluťoučký kůň'); 56 | job.workComplete(job.payload.toString().split("").reverse().join("")) 57 | }); 58 | var job = c.submitJob('reverse', 'žluťoučký kůň'); 59 | job.on('complete', function() { 60 | job.response.should.be.an.instanceof(Buffer); 61 | job.response.toString().should.equal('ňůk ýkčuoťulž'); 62 | done(); 63 | }); 64 | }) 65 | it('should return expected data as String', function(done) { 66 | w.addFunction('reverse', function (job) { 67 | job.payload.should.be.an.instanceof(String); 68 | job.payload.should.equal('123'); 69 | job.workComplete(job.payload.toString().split("").reverse().join("")) 70 | }, {toStringEncoding: 'ascii'}); 71 | var job = c.submitJob('reverse', Buffer('123', 'ascii'), {toStringEncoding: 'ascii'}); 72 | job.on('complete', function() { 73 | job.response.should.be.an.instanceof(String); 74 | job.response.should.equal('321'); 75 | done(); 76 | }); 77 | }) 78 | it('should be Buffer on Client and String on Worker', function(done) { 79 | w.addFunction('reverse', function (job) { 80 | job.payload.should.be.an.instanceof(String); 81 | job.payload.should.equal('123'); 82 | job.workComplete(job.payload.split("").reverse().join("")) 83 | }, {toStringEncoding: 'ascii'}); 84 | var job = c.submitJob('reverse', new Buffer([49, 50, 51])); // '123' 85 | job.on('complete', function() { 86 | job.response.should.be.an.instanceof(Buffer); 87 | job.response.toString().should.equal('321'); 88 | done(); 89 | }); 90 | }) 91 | }) 92 | 93 | 94 | describe('#submitJob#workData', function() { 95 | it('should return expected data', function(done) { 96 | w.addFunction('dummy', function (job) { 97 | job.sendWorkData('456'); 98 | job.workComplete() 99 | }); 100 | var job = c.submitJob('dummy', '123'); 101 | job.on('workData', function(data) { 102 | data.should.be.an.instanceof(Buffer); 103 | data.toString().should.equal('456'); 104 | done(); 105 | }); 106 | }) 107 | it('should return expected data sent as Buffer', function(done) { 108 | w.addFunction('dummy', function (job) { 109 | job.sendWorkData(new Buffer([52, 53, 54])); 110 | job.workComplete() 111 | }); 112 | var job = c.submitJob('dummy', '123'); 113 | job.on('workData', function(data) { 114 | data.should.be.an.instanceof(Buffer); 115 | data.toString().should.equal('456'); 116 | done(); 117 | }); 118 | }) 119 | it('should return expected data received as String', function(done) { 120 | w.addFunction('dummy', function (job) { 121 | job.sendWorkData(new Buffer([52, 53, 54])); 122 | job.workComplete() 123 | }); 124 | var job = c.submitJob('dummy', '123', {toStringEncoding: 'ascii'}); 125 | job.on('workData', function(data) { 126 | data.should.be.an.instanceof(String); 127 | data.should.equal('456'); 128 | done(); 129 | }); 130 | }) 131 | }) 132 | 133 | 134 | describe('#submitJob#warning', function() { 135 | it('should return expected data', function(done) { 136 | w.addFunction('dummy', function (job) { 137 | job.reportWarning('456'); 138 | job.workComplete() 139 | }); 140 | var job = c.submitJob('dummy', '123'); 141 | job.on('warning', function(data) { 142 | data.should.be.an.instanceof(Buffer); 143 | data.toString().should.equal('456'); 144 | done(); 145 | }); 146 | }) 147 | it('should return expected data sent as Buffer', function(done) { 148 | w.addFunction('dummy', function (job) { 149 | job.reportWarning(new Buffer([52, 53, 54])); 150 | job.workComplete() 151 | }); 152 | var job = c.submitJob('dummy', '123'); 153 | job.on('warning', function(data) { 154 | data.should.be.an.instanceof(Buffer); 155 | data.toString().should.equal('456'); 156 | done(); 157 | }); 158 | }) 159 | it('should return expected data received as String', function(done) { 160 | w.addFunction('dummy', function (job) { 161 | job.reportWarning(new Buffer([52, 53, 54])); 162 | job.workComplete() 163 | }); 164 | var job = c.submitJob('dummy', '123', {toStringEncoding: 'ascii'}); 165 | job.on('warning', function(data) { 166 | data.should.be.an.instanceof(String); 167 | data.should.equal('456'); 168 | done(); 169 | }); 170 | }) 171 | }) 172 | 173 | 174 | describe('#submitJob#exception', function() { 175 | it('should return expected data', function(done) { 176 | c.jobServers[0].setOption('exceptions', function(){}); 177 | w.addFunction('dummy', function (job) { 178 | job.reportException(new Buffer([52, 53, 54])); 179 | }); 180 | var job = c.submitJob('dummy', '123'); 181 | job.on('exception', function(data) { 182 | data.should.be.an.instanceof(Buffer); 183 | data.toString().should.equal('456'); 184 | done(); 185 | }); 186 | }) 187 | it('should return expected data sent as Buffer', function(done) { 188 | c.jobServers[0].setOption('exceptions', function(){}); 189 | w.addFunction('dummy', function (job) { 190 | job.reportException(new Buffer([52, 53, 54])); 191 | }); 192 | var job = c.submitJob('dummy', '123'); 193 | job.on('exception', function(data) { 194 | data.should.be.an.instanceof(Buffer); 195 | data.toString().should.equal('456'); 196 | done(); 197 | }); 198 | }) 199 | it('should return expected data received as String', function(done) { 200 | c.jobServers[0].setOption('exceptions', function(){}); 201 | w.addFunction('dummy', function (job) { 202 | job.reportException(new Buffer([52, 53, 54])); 203 | }); 204 | var job = c.submitJob('dummy', '123', {toStringEncoding: 'ascii'}); 205 | job.on('exception', function(data) { 206 | data.should.be.an.instanceof(String); 207 | data.should.equal('456'); 208 | done(); 209 | }); 210 | }) 211 | }) 212 | 213 | 214 | describe('#submitJob#unique', function() { 215 | it('should propagate unique to worker', function(done) { 216 | w = gearmanode.worker({withUnique: true}); 217 | w.addFunction('reverse', function (job) { 218 | job.unique.should.be.an.instanceof(String); 219 | job.unique.should.equal('foo'); 220 | job.workComplete('ok'); 221 | done(); 222 | }); 223 | var job = c.submitJob('reverse', 'alfa', {unique: 'foo'}); 224 | }) 225 | it('should propagate empty unique if not provided by client', function(done) { 226 | w = gearmanode.worker({withUnique: true}); 227 | w.addFunction('reverse', function (job) { 228 | job.unique.should.be.an.instanceof(String); 229 | job.unique.should.equal(''); 230 | job.workComplete('ok'); 231 | done(); 232 | }); 233 | var job = c.submitJob('reverse', 'alfa'); 234 | }) 235 | it('should NOT propagate unique to worker', function(done) { // worker with {withUnique: false} by default 236 | w.addFunction('reverse', function (job) { 237 | should.not.exist(job.unique); 238 | job.workComplete('ok'); 239 | done(); 240 | }); 241 | var job = c.submitJob('reverse', 'alfa', {unique: 'foo'}); 242 | }) 243 | }) 244 | 245 | 246 | }) 247 | -------------------------------------------------------------------------------- /test/test-worker.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | sinon = require('sinon'), 3 | events = require('events'), 4 | gearmanode = require('../lib/gearmanode'), 5 | protocol = require('../lib/gearmanode/protocol'), 6 | Worker = gearmanode.Worker, 7 | Job = gearmanode.Job, 8 | JobServer = require('../lib/gearmanode/job-server').JobServer; 9 | 10 | 11 | describe('Worker', function() { 12 | var w, j; 13 | beforeEach(function() { 14 | w = gearmanode.worker(); 15 | w.emit = sinon.spy(); 16 | w.jobServers[0].send = sinon.spy(); 17 | w._preSleep = sinon.spy(); 18 | j = new Job(w, {handle: 'HANDLE', name: 'NAME', payload: 'PAYLOAD', jobServerUid: 'UID'}); 19 | j.jobServerUid = w.jobServers[0].getUid(); 20 | }); 21 | 22 | 23 | describe('#factory', function() { 24 | it('should return default instance of Worker', function() { 25 | w.should.be.an.instanceof(Worker); 26 | w.withUnique.should.be.false; 27 | w.recoverTime.should.equal(30000); 28 | w.recoverLimit.should.equal(3); 29 | w._type.should.equal('Worker'); 30 | should.exist(w.jobServers); 31 | should.exist(w.functions); 32 | Object.keys(w.functions).length.should.equal(0); 33 | }) 34 | it('should store additional options', function() { 35 | w = gearmanode.worker({ withUnique: true, recoverTime: 1, recoverLimit: 1 }); 36 | w.withUnique.should.be.true; 37 | w.recoverTime.should.equal(1); 38 | w.recoverLimit.should.equal(1); 39 | }) 40 | }) 41 | 42 | 43 | describe('#close', function() { 44 | it('should clean up object', function() { 45 | w.functions['reverse'] = [function() {}, {}]; // mock the functions 46 | Object.keys(w.functions).length.should.equal(1); 47 | w.on('error', function() {}); 48 | events.EventEmitter.listenerCount(w, 'error').should.equal(1); 49 | w.close(); 50 | w.closed.should.be.true; 51 | Object.keys(w.functions).length.should.equal(0); 52 | events.EventEmitter.listenerCount(w, 'error').should.equal(0); 53 | }) 54 | it('should emit event on itself', function() { 55 | w.close(); 56 | w.emit.calledTwice.should.be.true; // diconnect + close 57 | w.emit.getCall(0).args[0].should.equal('socketDisconnect'); 58 | w.emit.getCall(1).args[0].should.equal('close'); 59 | }) 60 | }) 61 | 62 | 63 | describe('#addFunction', function() { 64 | it('should set many managing values', function() { 65 | w.addFunction('reverse', function() {}); 66 | Object.keys(w.functions).length.should.equal(1); 67 | should.exist(w.functions.reverse); 68 | w.functions.reverse.should.be.an.instanceof(Array); 69 | w.functions.reverse.length.should.equal(2); 70 | w.functions.reverse[0].should.be.an.instanceof(Function); 71 | Object.keys(w.functions.reverse[1]).length.should.equal(0); // empty options: {} 72 | w.jobServers[0].send.calledOnce.should.be.true; 73 | w._preSleep.calledOnce.should.be.true; 74 | }) 75 | it('should store additional options', function() { 76 | w.addFunction('reverse', function() {}, {timeout: 10, toStringEncoding: 'ascii'}); 77 | Object.keys(w.functions.reverse[1]).length.should.equal(2); 78 | w.functions.reverse[1].timeout.should.equal(10); 79 | w.functions.reverse[1].toStringEncoding.should.equal('ascii'); 80 | }) 81 | it('should return error when invalid function name', function() { 82 | w.addFunction(undefined, function() {}).should.be.an.instanceof(Error); 83 | w.addFunction(null, function() {}).should.be.an.instanceof(Error); 84 | w.addFunction('', function() {}).should.be.an.instanceof(Error); 85 | }) 86 | it('should return error when invalid options', function() { 87 | w.addFunction('foo', function() {}, {foo: true}).should.be.an.instanceof(Error); 88 | w.addFunction('foo', function() {}, {toStringEncoding: 'InVaLiD'}).should.be.an.instanceof(Error); 89 | }) 90 | it('should return error when no callback given', function() { 91 | w.addFunction('reverse').should.be.an.instanceof(Error); 92 | }) 93 | it('should send corresponding packet', function(done) { 94 | w.jobServers[0].send = function(buff) { 95 | var packetType = buff.readUInt32BE(4); 96 | packetType.should.equal(protocol.PACKET_TYPES.CAN_DO); 97 | done(); 98 | } 99 | w.addFunction('reverse', function() {}); 100 | }) 101 | it('with timeout should send corresponding packet', function(done) { 102 | w.jobServers[0].send = function(buff) { 103 | var packetType = buff.readUInt32BE(4); 104 | packetType.should.equal(protocol.PACKET_TYPES.CAN_DO_TIMEOUT); 105 | done(); 106 | } 107 | w.addFunction('reverse', function() {}, {timeout: 10}); 108 | }) 109 | }) 110 | 111 | 112 | describe('#removeFunction', function() { 113 | it('should unset many managing values', function() { 114 | w.addFunction('reverse', function() {}); 115 | w.removeFunction('reverse'); 116 | Object.keys(w.functions).length.should.equal(0); 117 | should.not.exist(w.functions.reverse); 118 | w.jobServers[0].send.calledTwice.should.be.true; // addRunction + removeFunction 119 | }) 120 | it('should return error when function name not known', function() { 121 | w.addFunction('foo', function() {}); 122 | w.removeFunction('bar').should.be.an.instanceof(Error); 123 | }) 124 | it('should return error when invalid function name', function() { 125 | w.removeFunction(undefined).should.be.an.instanceof(Error); 126 | w.removeFunction(null).should.be.an.instanceof(Error); 127 | w.removeFunction('').should.be.an.instanceof(Error); 128 | }) 129 | it('should send packet to job server', function() { 130 | w.addFunction('foo', function() {}); 131 | w.removeFunction('foo'); 132 | w.jobServers[0].send.calledTwice.should.be.true; // add + remove 133 | }) 134 | }) 135 | 136 | 137 | describe('#resetAbilities', function() { 138 | it('should send packet to job server', function() { 139 | w.resetAbilities(); 140 | w.jobServers[0].send.calledOnce.should.be.true; 141 | }) 142 | }) 143 | 144 | 145 | describe('#setWorkerId', function() { 146 | it('should return error when invalid workerId', function() { 147 | w.setWorkerId().should.be.an.instanceof(Error); 148 | w.setWorkerId(null).should.be.an.instanceof(Error); 149 | w.setWorkerId('').should.be.an.instanceof(Error); 150 | should.not.exist(w.setWorkerId('id')); // returns 'null' if validation ok 151 | }) 152 | it('should set workerId option on worker', function() { 153 | should.not.exist(w.workerId); 154 | w.setWorkerId('id'); 155 | w.workerId.should.equal('id'); 156 | }) 157 | it('should invoke `send` on job server', function() { 158 | w.setWorkerId('id'); 159 | w.jobServers[0].send.calledOnce.should.be.true; 160 | }) 161 | it('should send corresponding packet to job server', function(done) { 162 | w.jobServers[0].send = function(buff) { 163 | var packetType = buff.readUInt32BE(4); 164 | packetType.should.equal(protocol.PACKET_TYPES.SET_CLIENT_ID); 165 | done(); 166 | } 167 | w.setWorkerId('id'); 168 | }) 169 | }) 170 | 171 | describe('#_response', function() { 172 | it('should send GRAB_JOB when NOOP received', function(done) { 173 | w.jobServers[0].send = function(buff) { 174 | var packetType = buff.readUInt32BE(4); 175 | packetType.should.equal(protocol.PACKET_TYPES.GRAB_JOB); 176 | done(); 177 | } 178 | w._response(w.jobServers[0], protocol.PACKET_TYPES.NOOP); 179 | }) 180 | it('should send GRAB_JOB_UNIQ when NOOP received', function(done) { 181 | w.withUnique = true; 182 | w.jobServers[0].send = function(buff) { 183 | var packetType = buff.readUInt32BE(4); 184 | packetType.should.equal(protocol.PACKET_TYPES.GRAB_JOB_UNIQ); 185 | done(); 186 | } 187 | w._response(w.jobServers[0], protocol.PACKET_TYPES.NOOP); 188 | }) 189 | }) 190 | 191 | 192 | describe('#Job', function() { 193 | 194 | describe('#workComplete', function() { 195 | it('should send packets to job server', function() { 196 | j.workComplete(); 197 | w.jobServers[0].send.calledOnce.should.be.true; 198 | w._preSleep.calledOnce.should.be.true; 199 | w.jobServers[0].send.calledBefore(w._preSleep).should.be.true; 200 | j.closed.should.be.true; 201 | }) 202 | }) 203 | 204 | 205 | describe('#sendWorkData', function() { 206 | it('should send packet to job server', function() { 207 | j.sendWorkData('foo'); 208 | w.jobServers[0].send.calledOnce.should.be.true; 209 | should.not.exist(j.closed); 210 | }) 211 | }) 212 | 213 | 214 | describe('#reportStatus', function() { 215 | it('should send packet to job server', function() { 216 | j.reportStatus(1, 2); 217 | w.jobServers[0].send.calledOnce.should.be.true; 218 | should.not.exist(j.closed); 219 | }) 220 | it('should validate given parameters', function() { 221 | j.reportStatus().should.be.an.instanceof(Error); 222 | j.reportStatus(1).should.be.an.instanceof(Error); 223 | j.reportStatus(1, null).should.be.an.instanceof(Error); 224 | j.reportStatus(1, '').should.be.an.instanceof(Error); 225 | j.reportStatus('1', '2').should.be.an.instanceof(Error); 226 | w.jobServers[0].send.called.should.be.false; 227 | }) 228 | }) 229 | 230 | 231 | describe('#reportWarning', function() { 232 | it('should send packet to job server', function() { 233 | j.reportWarning('foo'); 234 | w.jobServers[0].send.calledOnce.should.be.true; 235 | should.not.exist(j.closed); 236 | }) 237 | }) 238 | 239 | 240 | describe('#reportError', function() { 241 | it('should send packet to job server', function() { 242 | j.reportError(); 243 | w.jobServers[0].send.calledOnce.should.be.true; 244 | w._preSleep.calledOnce.should.be.true; 245 | w.jobServers[0].send.calledBefore(w._preSleep).should.be.true; 246 | j.closed.should.be.true; 247 | }) 248 | }) 249 | 250 | 251 | describe('#reportException', function() { 252 | it('should send packet to job server', function() { 253 | j.reportException('NullPointerException#something cannot be null'); 254 | w.jobServers[0].send.calledOnce.should.be.true; 255 | w._preSleep.calledOnce.should.be.true; 256 | w.jobServers[0].send.calledBefore(w._preSleep).should.be.true; 257 | j.closed.should.be.true; 258 | }) 259 | }) 260 | }) 261 | 262 | }) 263 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /lib/gearmanode/client.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The GearmaNode Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | * @fileoverview This script represents class communicating as a client with Gearman job server. 17 | * @author vaclav.sykora@google.com (Vaclav Sykora) 18 | */ 19 | 20 | var util = require('util'), 21 | events = require('events'), 22 | winston = require('winston'), 23 | ServerManager = require('./server-manager').ServerManager, 24 | Job = require('./job').Job, 25 | lb = require('./load-balancing'), 26 | protocol = require('./protocol'), 27 | common = require('./common'), 28 | JS_CONSTANTS = require('./job-server').CONSTANTS; 29 | 30 | 31 | /** 32 | * @class Client 33 | * @classdesc A client for communicating with Gearman job servers. 34 | * @constructor 35 | * @augments events.EventEmitter 36 | * @mixes ServerManager 37 | * 38 | * @param options literal representing the client 39 | * @param {string} options.host hostname of single job server 40 | * @param {number} options.port port of single job server 41 | * @param {array} options.servers array of host,port pairs of multiple job servers 42 | * @param {string} [options.loadBalancing=Sequence] name of load balancing strategy 43 | * @param {number} [options.recoverTime=30000] delay in milliseconds before retrying the downed job server 44 | */ 45 | var Client = exports.Client = function(options) { 46 | var returned, clientOptions; 47 | 48 | options = options || {}; 49 | clientOptions = { loadBalancing: options.loadBalancing, recoverTime: options.recoverTime }; 50 | 51 | if (options.hasOwnProperty('loadBalancing')) { delete options.loadBalancing; } 52 | if (options.hasOwnProperty('recoverTime')) { delete options.recoverTime; } 53 | 54 | returned = common.verifyAndSanitizeOptions(clientOptions, { loadBalancing: 'Sequence', recoverTime: JS_CONSTANTS.DEFAULT_RECOVER_TIME }); 55 | if (returned instanceof Error) { return returned; } 56 | 57 | this._type = 'Client'; 58 | // call ServerManager initialization 59 | returned = this.initServers(options); 60 | if (returned instanceof Error) { return returned; } 61 | 62 | // Table of Jobs submited by this client. 63 | // A Job is inserted after JOB_CREATED packet. 64 | // A Job is removed: 65 | // * a non-background job after WORK_COMPLETE 66 | // * a background job by call of Job#close() 67 | this.jobs = {}; 68 | 69 | // load balancing 70 | switch (clientOptions.loadBalancing) { 71 | case 'RoundRobin': 72 | this.loadBalancer = new lb.RoundRobin(this.jobServers.length); 73 | break; 74 | case 'Sequence': 75 | this.loadBalancer = new lb.Sequence(this.jobServers.length); 76 | break; 77 | default: 78 | return new Error('unknow load balancing strategy: ' + clientOptions.loadBalancing); 79 | } 80 | if (!common.isNumber(clientOptions.recoverTime)) { return new Error('option recoverTime is not number'); } 81 | this.loadBalancer.recoverTime = clientOptions.recoverTime; 82 | 83 | events.EventEmitter.call(this); 84 | Client.logger.log('info', 'client initialized with %d job server(s)', this.jobServers.length); 85 | Client.logger.log('debug', 'load balancing: strategy=%s, recoverTime=%d[ms]', clientOptions.loadBalancing, clientOptions.recoverTime); 86 | }; 87 | 88 | // inheritance 89 | util.inherits(Client, events.EventEmitter); 90 | // mixes ServerManager 91 | ServerManager.mixin(Client); 92 | 93 | // static logger 94 | Client.logger = winston.loggers.get('Client'); 95 | 96 | 97 | /** 98 | * Ends the client and all its associated resources, e.g. socket connections. 99 | * Sets property 'closed' to 'true'. 100 | * Removes all registered listeners on the object. 101 | * 102 | * @method 103 | * @fires Client#close 104 | * @returns {void} nothing 105 | */ 106 | Client.prototype.close = function () { 107 | this.closed = true; 108 | 109 | this.closeServers(); 110 | 111 | // clear submited & incomplete jobs 112 | for (var i in this.jobs) { 113 | if (this.jobs.hasOwnProperty(i)) { 114 | this.jobs[i].close(); 115 | delete this.jobs[i]; 116 | } 117 | } 118 | 119 | this.emit('close'); // trigger event 120 | this.removeAllListeners(); 121 | }; 122 | 123 | 124 | /** 125 | * Submits given job to a job server according to load balancing strategy. 126 | * 127 | * @method 128 | * @param {string} name function name 129 | * @param {string|Buffer} payload opaque data that is given to the function as an argument 130 | * @param options literal representing additional job configuration 131 | * @param {boolean} options.background flag whether the job should be processed in background/asynchronous 132 | * @param {string} options.priority priority in job server queue, 'HIGH'|'NORMAL'|'LOW' 133 | * @param {string} options.unique unique identifiter for this job, the identifier is assigned by the client 134 | * @param {string} options.toStringEncoding if given received payload will be converted to String with this encoding, otherwise payload turned over as Buffer 135 | * @returns {Job} newly created job 136 | * @fires Job#submited 137 | */ 138 | Client.prototype.submitJob = function(name, payload, options) { 139 | var self = this, job, jobServer, packet, tryToSend; 140 | 141 | options = options || {}; 142 | common.mixin({'name': name, 'payload': payload}, options); 143 | 144 | job = new Job(this, options); 145 | if (job instanceof Error) { return job; } 146 | job.processing = true; 147 | 148 | packet = protocol.encodePacket(job.getPacketType(), [job.name, (job.unique ? job.unique : ''), job.payload]); 149 | 150 | var jsSendCallback = function(err) { 151 | if (err instanceof Error) { 152 | Client.logger.log('warn', 'failed to submit job, server=%s, error:', jobServer.getUid(), err); 153 | tryToSend(); 154 | } else { 155 | jobServer.jobsWaiting4Created.push(job); // add to queue of submited jobs waiting for confirmation 156 | job.jobServerUid = jobServer.getUid(); // store job server UID on job to later usage 157 | process.nextTick(function() { job.emit('submited'); }); 158 | Client.logger.log('debug', 'job submited, name=%s, unique=%s', job.name, job.unique); 159 | } 160 | }; 161 | 162 | tryToSend = function() { 163 | jobServer = self._getJobServer(); 164 | if (!jobServer) { // problem escalated by '_getJobServer' on Client too 165 | return job.emit('error', new Error('all job servers fail')); 166 | } 167 | 168 | jobServer.send(packet, jsSendCallback); 169 | }; 170 | 171 | tryToSend(); 172 | return job; 173 | }; 174 | 175 | 176 | /** 177 | * @method 178 | * @access protected 179 | * @inheritDoc 180 | */ 181 | Client.prototype._response = function (jobServer, packetType, parsedPacket) { // #unit: not needed 182 | var packetCode = protocol.PACKET_CODES[packetType]; 183 | var handle = parsedPacket[1]; 184 | var job, jobStatus; 185 | var jobUid = jobServer.getUid() + '#' + handle; 186 | 187 | if (!this.jobs.hasOwnProperty(jobUid)) { 188 | this._unrecoverableError('unknown job, uid=' + jobUid); 189 | return; 190 | } 191 | job = this.jobs[jobUid]; 192 | 193 | if (Client.logger.isLevelEnabled('debug')) { 194 | Client.logger.log('debug', 'response for client: type=%s, uid=%s', packetCode, job.getUid()); 195 | } 196 | 197 | switch (packetType) { 198 | case protocol.PACKET_TYPES.JOB_CREATED: 199 | job.emit('created'); // trigger event 200 | 201 | break; 202 | 203 | case protocol.PACKET_TYPES.WORK_COMPLETE: 204 | case protocol.PACKET_TYPES.WORK_FAIL: 205 | job.processing = false; 206 | delete this.jobs[job.getUid()]; // remove it from table of submited & incomplete jobs 207 | 208 | job.response = parsedPacket[2]; 209 | // encoding of payload; only for WORK_COMPLETE which have data 210 | if (packetType === protocol.PACKET_TYPES.WORK_COMPLETE && job.toStringEncoding) { 211 | job.response = job.response.toString(job.toStringEncoding); 212 | } 213 | 214 | job.emit(packetType === protocol.PACKET_TYPES.WORK_COMPLETE ? 'complete' : 'failed'); // trigger event 215 | break; 216 | 217 | case protocol.PACKET_TYPES.WORK_DATA: 218 | case protocol.PACKET_TYPES.WORK_WARNING: 219 | var data = parsedPacket[2]; 220 | // encoding of data 221 | if (job.toStringEncoding) { data = data.toString(job.toStringEncoding); } 222 | job.emit(packetType == protocol.PACKET_TYPES.WORK_DATA ? 'workData' : 'warning', data); // trigger event 223 | break; 224 | 225 | case protocol.PACKET_TYPES.WORK_EXCEPTION: 226 | var data = parsedPacket[2]; 227 | // encoding of data 228 | if (job.toStringEncoding) { data = data.toString(job.toStringEncoding); } 229 | job.processing = false; 230 | delete this.jobs[job.getUid()]; // remove it from table of submited & incomplete jobs 231 | job.emit('exception', data); // trigger event 232 | break; 233 | 234 | 235 | case protocol.PACKET_TYPES.STATUS_RES: 236 | jobStatus = {}; 237 | jobStatus.known = '1' == parsedPacket[2]; 238 | jobStatus.running = '1' == parsedPacket[3]; 239 | jobStatus.percent_done_num = parsedPacket[4]; 240 | jobStatus.percent_done_den = parsedPacket[5]; 241 | if (Client.logger.isLevelEnabled('verbose')) { 242 | Client.logger.log('verbose', 'job status, handle=%s, known=%d, running=%d, num=%d, den=%d', 243 | handle, jobStatus.known, jobStatus.running, jobStatus.percent_done_num, jobStatus.percent_done_den); 244 | } 245 | 246 | job.emit('status', jobStatus); // trigger event 247 | break; 248 | 249 | case protocol.PACKET_TYPES.WORK_STATUS: 250 | jobStatus = {}; 251 | jobStatus.percent_done_num = parsedPacket[2]; 252 | jobStatus.percent_done_den = parsedPacket[3]; 253 | if (Client.logger.isLevelEnabled('verbose')) { 254 | Client.logger.log('verbose', 'job status, handle=%s, num=%d, den=%d', 255 | handle, jobStatus.percent_done_num, jobStatus.percent_done_den); 256 | } 257 | 258 | job.emit('status', jobStatus); // trigger event 259 | break; 260 | } 261 | }; 262 | 263 | 264 | /** 265 | * Returns a human readable string representation of the object. 266 | * 267 | * @method 268 | * @returns {string} object description 269 | */ 270 | Client.prototype.toString = function() { // #unit: not needed 271 | return 'Client(jobServers=' + util.inspect(this.jobServers) + ')'; 272 | }; 273 | 274 | 275 | /** 276 | * @method 277 | * @fires Client#error 278 | * @access protected 279 | * @inheritDoc 280 | */ 281 | Client.prototype._unrecoverableError = function (msg) { // #unit: not needed 282 | Client.logger.log('error', msg); 283 | this.emit('error', new Error(msg)); // trigger event 284 | }; 285 | 286 | 287 | /** 288 | * Gets a job server according to load balancing strategy. 289 | * 290 | * @method 291 | * @access private 292 | */ 293 | Client.prototype._getJobServer = function() { 294 | var idx = this.loadBalancer.nextIndex(); 295 | if (idx === null) { 296 | this._unrecoverableError('failed to obtain job server from load balancer (all servers invalid?)'); 297 | return null; 298 | } 299 | return this.jobServers[idx]; 300 | }; 301 | 302 | 303 | 304 | /** 305 | * @class Job 306 | */ 307 | common.mixin({ 308 | /** 309 | * Sends request to get status of a background job. 310 | *
311 | * See Gearman Documentation: 312 | *
313 | * This is used by clients that have submitted a job with SUBMIT_JOB_BG to see if the 314 | * job has been completed, and if not, to get the percentage complete. 315 | * 316 | * @method 317 | * @memberof Job 318 | * @fires Job#status when response successfully arrived 319 | * @returns {void} nothing 320 | */ 321 | getStatus: function() { 322 | var jobServer, packet; 323 | 324 | if (!this.background) { return new Error('this is not a background job'); } 325 | if (!this.handle) { return new Error("no job's handle (no created job?)"); } 326 | if (!this.jobServerUid) { return new Error('no associated job server (never ever submited job?)'); } 327 | 328 | jobServer = this.clientOrWorker._getJobServerByUid(this.jobServerUid); 329 | if (!jobServer) { return new Error('job server not found by UID, uid=' + this.jobServerUid); } 330 | 331 | packet = protocol.encodePacket(protocol.PACKET_TYPES.GET_STATUS, [this.handle]); 332 | jobServer.send(packet); 333 | } 334 | }, Job.prototype); 335 | -------------------------------------------------------------------------------- /test/test-job-server.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | sinon = require('sinon'), 3 | util = require('util'), 4 | net = require('net'), 5 | events = require('events'), 6 | gearmanode = require('../lib/gearmanode'), 7 | JobServer = require('../lib/gearmanode/job-server').JobServer, 8 | Job = require('../lib/gearmanode/job').Job, 9 | protocol = require('../lib/gearmanode/protocol'); 10 | 11 | 12 | describe('JobServer', function() { 13 | var c, js; 14 | beforeEach(function() { 15 | c = gearmanode.client(); 16 | js = c.jobServers[0]; 17 | }); 18 | 19 | 20 | describe('#constructor', function() { 21 | it('should return unconnected instance', function() { 22 | js = new JobServer({ host: 'localhost', port: 4730 }); 23 | js.should.be.an.instanceof(JobServer); 24 | js.connected.should.be.false; 25 | should.not.exist(js.socket); 26 | should.not.exist(js.clientOrWorker); 27 | js.jobsWaiting4Created.length.should.equal(0); 28 | js.getUid().should.equal('localhost:4730'); 29 | js.wrongDisconnectAt.should.be.equal(0); 30 | js.failedConnectionCount.should.be.equal(0); 31 | }) 32 | it('should return error when missing mandatory options', function() { 33 | js = new JobServer(); 34 | js.should.be.an.instanceof(Error); 35 | js = new JobServer({ host: 'localhost' }); 36 | js.should.be.an.instanceof(Error); 37 | js = new JobServer({ port: 4730 }); 38 | js.should.be.an.instanceof(Error); 39 | }) 40 | }) 41 | 42 | 43 | describe('#connect', function() { 44 | it('should change inner state when connection OK', function(done) { 45 | js.connect(function(err) { 46 | js.connected.should.be.true; 47 | js.wrongDisconnectAt.should.be.equal(0); 48 | js.failedConnectionCount.should.be.equal(0); 49 | should.exist(js.socket); 50 | js.socket.should.be.an.instanceof(net.Socket); 51 | done(); 52 | }) 53 | }) 54 | it('should return socket when connection OK', function() { 55 | var socket = js.connect(function() {}); 56 | should.exist(socket); 57 | socket.should.be.an.instanceof(net.Socket); 58 | }) 59 | it('should call success callback when connection OK', function(done) { 60 | js.connect(function(err) { 61 | should.not.exist(err); 62 | done(); 63 | }) 64 | }) 65 | it('should emit event on client when connection OK', function(done) { 66 | js.clientOrWorker.emit = sinon.spy(); 67 | js.connect(function() { 68 | js.clientOrWorker.emit.calledOnce.should.be.true; 69 | js.clientOrWorker.emit.calledWith('socketConnect').should.be.true; 70 | js.clientOrWorker.emit.getCall(0).args[1].should.equal(js.getUid()); 71 | done(); 72 | }) 73 | }) 74 | it('should fire error when connection fails', function(done) { 75 | js.port = 1; 76 | js.clientOrWorker.emit = sinon.spy(); 77 | js.connect(function(err) { 78 | should.exist(err); 79 | err.should.be.an.instanceof(Error); 80 | err.code.should.be.equal('ECONNREFUSED'); 81 | js.connected.should.be.false; 82 | js.wrongDisconnectAt.should.be.greaterThan(0); 83 | js.failedConnectionCount.should.be.greaterThan(0); 84 | should.not.exist(js.socket); 85 | done(); 86 | }) 87 | }) 88 | it('should disconnect connection whenever error occurs', function(done) { 89 | js.port = 1; 90 | js.clientOrWorker.emit = sinon.spy(); // to blind emitting of error which terminated the test case 91 | js.disconnect = sinon.spy(); 92 | js.connect(function() { 93 | js.disconnect.calledOnce.should.be.true; 94 | done(); 95 | }) 96 | }) 97 | it('BF9: should connect once if two `send` invoked on unconnected server', function() { 98 | var w = gearmanode.worker(); 99 | sinon.spy(w.jobServers[0], 'send'); // proxies original method 100 | sinon.spy(w.jobServers[0], 'connect'); 101 | w.setWorkerId('foo'); 102 | w.setWorkerId('bar'); 103 | w.jobServers[0].send.calledTwice.should.be.true; 104 | w.jobServers[0].connect.calledOnce.should.be.true; 105 | }) 106 | }) 107 | 108 | 109 | describe('#disconnect', function() { 110 | it('should properly change inner state', function(done) { 111 | var socket = js.connect(function(err, jobServer) { 112 | should.not.exist(err); 113 | js.connected.should.be.true; 114 | js.socket.listeners('connect').length.should.equal(2); // one is mine, the other from some infrastructure 115 | js.disconnect(); 116 | js.connected.should.be.false; 117 | js.wrongDisconnectAt.should.be.equal(0); 118 | js.failedConnectionCount.should.be.equal(0); 119 | should.not.exist(js.socket); 120 | should.exist(js.clientOrWorker); 121 | js.jobsWaiting4Created.length.should.equal(0); 122 | done(); 123 | }) 124 | }) 125 | it('should set `wrongDisconnectAt` when disconnect caused by a problem', function(done) { 126 | var socket = js.connect(function(err, jobServer) { 127 | should.not.exist(err); 128 | js.disconnect(true); // true => simulate an error object 129 | js.wrongDisconnectAt.should.be.greaterThan(0); 130 | js.failedConnectionCount.should.be.greaterThan(0); 131 | done(); 132 | }) 133 | }) 134 | it('should emit event on client/worker', function(done) { 135 | js.clientOrWorker.emit = sinon.spy(); 136 | js.connect(function() { 137 | js.disconnect(); 138 | js.clientOrWorker.emit.calledTwice.should.be.true; // connect + disconnect 139 | js.clientOrWorker.emit.getCall(1).args[0].should.equal('socketDisconnect'); 140 | js.clientOrWorker.emit.getCall(1).args[1].should.equal(js.getUid()); 141 | done(); 142 | }) 143 | }) 144 | }) 145 | 146 | 147 | describe('#echo #setOption', function() { 148 | it('should return error when invalid options', function() { 149 | js.echo().should.be.an.instanceof(Error); 150 | js.echo(null).should.be.an.instanceof(Error); 151 | js.echo(1).should.be.an.instanceof(Error); 152 | should.not.exist(js.echo('1')); 153 | }) 154 | }) 155 | 156 | 157 | describe('#echo', function() { 158 | it('should return echoed data in response', function(done) { 159 | js.once('echo', function(response) { 160 | response.should.equal('ping'); 161 | done(); 162 | }); 163 | js.echo('ping'); 164 | }) 165 | }) 166 | 167 | 168 | describe('#setOption', function() { 169 | it('should return name of the option that was set', function(done) { 170 | js.once('option', function(response) { 171 | response.should.equal('exceptions'); 172 | done(); 173 | }); 174 | js.setOption('exceptions'); 175 | }) 176 | it('should emit server error on itself if option is unknown', function(done) { 177 | js.clientOrWorker.emit = sinon.spy(); 178 | js.once('jobServerError', function(code, msg) { 179 | code.toUpperCase().should.equal(protocol.CONSTANTS.UNKNOWN_OPTION); // toUpperCase -> some version of gearmand returns message as lower case 180 | js.clientOrWorker.emit.calledTwice.should.be.true; // connect + error, emit on Job Server after emit on Client/Worker 181 | js.clientOrWorker.emit.getCall(1).args[0].should.equal('jobServerError'); 182 | js.clientOrWorker.emit.getCall(1).args[1].should.equal(js.getUid()); 183 | js.clientOrWorker.emit.getCall(1).args[2].toUpperCase().should.equal(protocol.CONSTANTS.UNKNOWN_OPTION); 184 | done(); 185 | }); 186 | js.setOption('foo'); 187 | }) 188 | it('should emit server error on client/worker if option is unknown', function(done) { 189 | sinon.spy(js, 'emit'); // proxies original method 190 | c.once('jobServerError', function(uid, code, msg) { 191 | uid.should.equal(js.getUid()); 192 | code.toUpperCase().should.equal(protocol.CONSTANTS.UNKNOWN_OPTION); 193 | js.emit.callCount.should.equal(1); // internal event to signal successful connection 194 | done(); 195 | }); 196 | js.setOption('foo'); 197 | }) 198 | }) 199 | 200 | 201 | describe('#send', function() { 202 | var hiPacket = protocol.encodePacket(protocol.PACKET_TYPES.ECHO_REQ, ['hi']); 203 | it('should autoconnect when not connected before', function() { 204 | js.connect = sinon.spy(); 205 | js.send(hiPacket); 206 | js.connect.calledOnce.should.be.true; 207 | }) 208 | it('should not autoconnect again when connected before', function() { 209 | js.connect = sinon.spy(); 210 | js.connect(function() { 211 | js.connect.calledOnce.should.be.true; 212 | js.send(hiPacket); 213 | js.connect.calledOnce.should.be.true; 214 | }) 215 | }) 216 | it('should emit `socketError` on client when sending fails due to connection', function(done) { 217 | js.port = 1; 218 | js.clientOrWorker.once('socketError', function(uid, err) { 219 | uid.should.equal('localhost:1'); 220 | should.exist(err); 221 | err.should.be.an.instanceof(Error); 222 | err.code.should.be.equal('ECONNREFUSED'); 223 | done(); 224 | }); 225 | js.send(hiPacket); 226 | }) 227 | it('should call error callback when data not a Buffer', function() { 228 | js.send('TEXT NOT ALLOWED').should.be.an.instanceof(Error); 229 | }) 230 | }) 231 | 232 | 233 | describe('#_processData', function() { 234 | it('should process NOOP message correctly', function() { 235 | var chunk = new Buffer([0x00, 0x52, 0x45, 0x53, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00]); // NOOP 236 | js.clientOrWorker._response = sinon.spy(); 237 | js.connected = true; 238 | js._processData(chunk); 239 | should.not.exist(js.segmentedPacket); 240 | should.not.exist(js.headerfrag); 241 | js.clientOrWorker._response.calledOnce.should.be.true; 242 | js.clientOrWorker._response.getCall(0).args[1].should.equal(protocol.PACKET_TYPES.NOOP); 243 | }) 244 | it('should process one packet with two messages correctly', function(done) { 245 | var chunk1 = new Buffer([0x00, 0x52, 0x45, 0x53, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00]); // NOOP 246 | var chunk2 = new Buffer([0x00, 0x52, 0x45, 0x53, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00]); // NO_JOB 247 | var chunk = Buffer.concat([chunk1, chunk2]); 248 | js.clientOrWorker._response = sinon.spy(); 249 | js.connected = true; 250 | js._processData(chunk); 251 | // '_processData' works recursively in this case; the second call is via 'process.nextTick' 252 | // so the asserts must be in the next tick 253 | process.nextTick(function() { 254 | should.not.exist(js.segmentedPacket); 255 | should.not.exist(js.headerfrag); 256 | js.clientOrWorker._response.calledTwice.should.be.true; 257 | js.clientOrWorker._response.getCall(0).args[1].should.equal(protocol.PACKET_TYPES.NOOP); 258 | js.clientOrWorker._response.getCall(1).args[1].should.equal(protocol.PACKET_TYPES.NO_JOB); 259 | done(); 260 | }) 261 | }) 262 | it('should process more packet with one messages correctly', function() { 263 | var chunk1 = new Buffer([0x00, 0x52, 0x45, 0x53, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x14]); // JOB_ASSIGN 264 | var chunk2 = new Buffer([0x48, 0x3a, 0x6c, 0x61, 0x70, 0x3a, 0x31, 0x00, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x00, 265 | 0x74, 0x65, 0x73, 0x74]); 266 | js.clientOrWorker._response = sinon.spy(); 267 | js.connected = true; 268 | js._processData(chunk1); 269 | should.exist(js.segmentedPacket); 270 | should.not.exist(js.headerfrag); 271 | js.clientOrWorker._response.called.should.be.false; 272 | js._processData(chunk2); 273 | should.not.exist(js.segmentedPacket); 274 | should.not.exist(js.headerfrag); 275 | js.clientOrWorker._response.calledOnce.should.be.true; 276 | js.clientOrWorker._response.getCall(0).args[1].should.equal(protocol.PACKET_TYPES.JOB_ASSIGN); 277 | }) 278 | it('PR 41: should concatenate one message with header splitted into two packets', function() { 279 | var chunk1 = new Buffer([0x00, 0x52, 0x45]); 280 | var chunk2 = new Buffer([0x53, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, // NOOP 281 | 0x00, 0x00, 0x00]); // + 3 bytes to be >= 12 282 | js.clientOrWorker._response = sinon.spy(); 283 | js.connected = true; 284 | js._processData(chunk1); 285 | should.not.exist(js.segmentedPacket); 286 | should.exist(js.headerfrag); 287 | js._processData(chunk2); 288 | should.not.exist(js.segmentedPacket); 289 | should.exist(js.headerfrag); // because last 3 bytes are identified as splitted header again 290 | js.clientOrWorker._response.calledOnce.should.be.true; 291 | js.clientOrWorker._response.getCall(0).args[1].should.equal(protocol.PACKET_TYPES.NOOP); 292 | }) 293 | }) 294 | 295 | }) 296 | -------------------------------------------------------------------------------- /lib/gearmanode/worker.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The GearmaNode Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | * @fileoverview This script represents class communicating as a worker with Gearman job server. 17 | * @author vaclav.sykora@google.com (Vaclav Sykora) 18 | */ 19 | 20 | var util = require('util'), 21 | events = require('events'), 22 | winston = require('winston'), 23 | ServerManager = require('./server-manager').ServerManager, 24 | Job = require('./job').Job, 25 | protocol = require('./protocol'), 26 | common = require('./common'), 27 | JS_CONSTANTS = require('./job-server').CONSTANTS; 28 | 29 | 30 | /** 31 | * @class Worker 32 | * @classdesc A Worker for communicating with Gearman job servers. 33 | * @constructor 34 | * @augments events.EventEmitter 35 | * @mixes ServerManager 36 | * 37 | * @param options literal representing the client 38 | * @param {string} options.host hostname of single job server 39 | * @param {number} options.port port of single job server 40 | * @param {array} options.servers array of host,port pairs of multiple job servers 41 | * @param {boolean} options.withUnique flag whether a job will be grabbed with the client assigned unique ID 42 | * @param {number} [options.recoverTime=30000] delay in milliseconds before retrying the downed connection to job server 43 | * @param {number} [options.recoverLimit=3] how many attempts to retrying the downed connection to job server 44 | */ 45 | var Worker = exports.Worker = function(options) { 46 | var returned; 47 | 48 | options = options || {}; 49 | workerOptions = { withUnique: options.withUnique, recoverTime: options.recoverTime, recoverLimit: options.recoverLimit }; 50 | delete options.withUnique; 51 | delete options.recoverTime; 52 | delete options.recoverLimit; 53 | 54 | // VALIDATION 55 | returned = common.verifyAndSanitizeOptions(workerOptions, { withUnique: false, recoverTime: JS_CONSTANTS.DEFAULT_RECOVER_TIME, recoverLimit: 3 }); 56 | if (returned instanceof Error) { return returned; } 57 | returned = common.verifyOptions(workerOptions, { withUnique: [true, false], recoverTime: 'madatory', recoverLimit: 'madatory' }); 58 | if (returned instanceof Error) { return returned; } 59 | this.withUnique = workerOptions.withUnique; 60 | this.recoverTime = workerOptions.recoverTime; 61 | this.recoverLimit = workerOptions.recoverLimit; 62 | 63 | this._type = 'Worker'; 64 | // call ServerManager initialization 65 | returned = this.initServers(options); 66 | if (returned instanceof Error) { return returned; } 67 | 68 | // table of functions successfully registered on a job server 69 | // in form: {'name': [func, options]} 70 | this.functions = {}; 71 | 72 | events.EventEmitter.call(this); 73 | Worker.logger.log('info', 'worker initialized with %d job server(s)', this.jobServers.length); 74 | }; 75 | 76 | // inheritance 77 | util.inherits(Worker, events.EventEmitter); 78 | // mixes ServerManager 79 | ServerManager.mixin(Worker); 80 | 81 | // static logger 82 | Worker.logger = winston.loggers.get('Worker'); 83 | 84 | 85 | /** 86 | * Registers a function name with the job server and specifies a callback corresponding to that function. 87 | * It tries to connect to ALL job servers and fires 'error' if one registration fails. 88 | * 89 | * @method 90 | * @param {string} name name of a function 91 | * @param {Function} callback the function to be run when a job is received, gets {@link Job} as parameter 92 | * @param options literal representing additional options 93 | * @param {number} options.timeout timeout value, the job server will mark the job as failed and notify any listening clients 94 | * @param {string} options.toStringEncoding if given received payload will be converted to String with this encoding, otherwise payload turned over as Buffer 95 | * @fires Worker#error 96 | * @returns {void} nothing 97 | */ 98 | Worker.prototype.addFunction = function(name, callback, options) { 99 | var jobServer, pattern, returned; 100 | if (!name) { return new Error('undefined function name'); } 101 | if (!(callback instanceof Function)) { return new Error('invalid callback (not a function)'); } 102 | 103 | // validate options 104 | options = options || {}; 105 | pattern = { timeout: 'optional', toStringEncoding: 'optional' } 106 | returned = common.verifyOptions(options, pattern); 107 | if (returned instanceof Error) { return returned; } 108 | 109 | // validate encoding 110 | if (options.toStringEncoding && !Buffer.isEncoding(options.toStringEncoding)) { 111 | return new Error('invalid encoding: ' + options.toStringEncoding); 112 | } 113 | 114 | for (var i = 0; i < this.jobServers.length; i ++) { // TODO iterate only servers without any previous error 115 | jobServer = this.jobServers[i]; 116 | if (options.hasOwnProperty('timeout')) { 117 | jobServer.send(protocol.encodePacket(protocol.PACKET_TYPES.CAN_DO_TIMEOUT, [name, options['timeout']])); 118 | } else { 119 | jobServer.send(protocol.encodePacket(protocol.PACKET_TYPES.CAN_DO, [name])); 120 | } 121 | this._preSleep(jobServer); 122 | this.functions[name] = [callback, options]; 123 | Worker.logger.log('info', 'registered function name=%s on server uid=%s', name, jobServer.getUid()); 124 | } 125 | }; 126 | 127 | 128 | /** 129 | * Removes registration of given function from job server - worker is no longer 130 | * able to perform the given function. 131 | * 132 | * @method 133 | * @param {string} name name of the function to be removed 134 | * @returns {void} nothing 135 | */ 136 | Worker.prototype.removeFunction = function(name) { 137 | var jobServer; 138 | if (!name) { return new Error('undefined function name'); } 139 | if (!this.functions.hasOwnProperty(name)) { return new Error('function not registered, name=' + name); } 140 | for (var i = 0; i < this.jobServers.length; i ++) { // TODO iterate only servers without any previous error 141 | jobServer = this.jobServers[i]; 142 | jobServer.send(protocol.encodePacket(protocol.PACKET_TYPES.CANT_DO, [name])); 143 | delete this.functions[name]; 144 | Worker.logger.log('debug', 'unregistered function name=%s on server uid=%s', name, jobServer.getUid()); 145 | } 146 | }; 147 | 148 | 149 | /** 150 | * This is sent to notify the server that the worker is no longer able to do any functions 151 | * it previously registered with CAN_DO or CAN_DO_TIMEOUT. 152 | * 153 | * @method 154 | * @return {void} nothing 155 | */ 156 | Worker.prototype.resetAbilities = function() { 157 | var jobServer; 158 | for (var i = 0; i < this.jobServers.length; i ++) { // TODO iterate only servers without any previous error 159 | jobServer = this.jobServers[i]; 160 | jobServer.send(protocol.encodePacket(protocol.PACKET_TYPES.RESET_ABILITIES)); 161 | } 162 | Worker.logger.log('debug', 'RESET_ABILITIES on all servers'); 163 | }; 164 | 165 | 166 | /** 167 | * Sets the worker ID in a job server so monitoring and reporting 168 | * commands can uniquely identify the various workers. 169 | * 170 | * @method 171 | * @param {string} workerId the worker ID to be set in all job servers 172 | * @return {void} nothing 173 | */ 174 | Worker.prototype.setWorkerId = function(workerId) { 175 | var jobServer; 176 | 177 | // VALIDATION 178 | if (!common.isString(workerId)) { return new Error('worker ID not a string'); } 179 | if (workerId.length == 0) { return new Error('worker ID cannot be blank'); } 180 | 181 | this.workerId = workerId; 182 | 183 | for (var i = 0; i < this.jobServers.length; i ++) { // TODO iterate only servers without any previous error 184 | jobServer = this.jobServers[i]; 185 | jobServer.send(protocol.encodePacket(protocol.PACKET_TYPES.SET_CLIENT_ID, [this.workerId])); 186 | } 187 | Worker.logger.log('debug', 'SET_CLIENT_ID on all servers, workerId=%s', this.workerId); 188 | }; 189 | 190 | 191 | /** 192 | * Ends the worker and all its associated resources, e.g. socket connections. 193 | * Sets property 'closed' to 'true'. 194 | * Removes all registered listeners on the object. 195 | * 196 | * @method 197 | * @fires Worker#close 198 | * @returns {void} nothing 199 | */ 200 | Worker.prototype.close = function () { 201 | this.closed = true; 202 | 203 | this.closeServers(); 204 | 205 | // clear registered functions 206 | for (var i in this.functions) { 207 | if (this.functions.hasOwnProperty(i)) { 208 | delete this.functions[i]; 209 | } 210 | } 211 | 212 | this.emit('close'); // trigger event 213 | this.removeAllListeners(); 214 | }; 215 | 216 | 217 | /** 218 | * @method 219 | * @access protected 220 | * @inheritDoc 221 | */ 222 | Worker.prototype._response = function (jobServer, packetType, parsedPacket) { // #unit: not needed 223 | var job, funcAndOpts, fnRslt; 224 | 225 | switch (packetType) { 226 | case protocol.PACKET_TYPES.NOOP: 227 | jobServer.send(this.withUnique 228 | ? protocol.encodePacket(protocol.PACKET_TYPES.GRAB_JOB_UNIQ) 229 | : protocol.encodePacket(protocol.PACKET_TYPES.GRAB_JOB)); 230 | break; 231 | case protocol.PACKET_TYPES.JOB_ASSIGN: 232 | case protocol.PACKET_TYPES.JOB_ASSIGN_UNIQ: 233 | var jobOpts = { handle: parsedPacket[1], name: parsedPacket[2], jobServerUid: jobServer.getUid() }; 234 | if (packetType === protocol.PACKET_TYPES.JOB_ASSIGN) { 235 | jobOpts.payload = parsedPacket[3]; 236 | } else { 237 | jobOpts.unique = parsedPacket[3]; 238 | jobOpts.payload = parsedPacket[4]; 239 | } 240 | job = new Job(this, jobOpts); 241 | //Object.freeze(job); 242 | // get the function and its options 243 | funcAndOpts = this._getFunction(job.name); 244 | // encoding of payload 245 | if (funcAndOpts[1].toStringEncoding) { job.payload = job.payload.toString(funcAndOpts[1].toStringEncoding); } 246 | fnRslt = funcAndOpts[0](job); 247 | Worker.logger.log('debug', 'function finished, name=%s, result=' + fnRslt, job.name); 248 | break; 249 | case protocol.PACKET_TYPES.NO_JOB: 250 | this._preSleep(jobServer); 251 | break; 252 | } 253 | }; 254 | 255 | 256 | /** 257 | * Returns a human readable string representation of the object. 258 | * 259 | * @method 260 | * @returns {string} object description 261 | */ 262 | Worker.prototype.toString = function () { // #unit: not needed 263 | return 'Worker(jobServers=' + util.inspect(this.jobServers) + ')'; 264 | }; 265 | 266 | 267 | /** 268 | * Gets a registered function and its options or escalates an error if function not found. 269 | * 270 | * @method 271 | * @access private 272 | */ 273 | Worker.prototype._getFunction = function (name) { // #unit: not needed 274 | if (this.functions.hasOwnProperty(name)) { 275 | return this.functions[name]; 276 | } else { 277 | Worker.logger.log('error', 'function not found in locale register, name=%s', name); 278 | this.emit('error', new Error('function not found in locale register, name=' + name)); 279 | } 280 | }; 281 | 282 | 283 | /** 284 | * Sends info to notify the server that the worker is about to sleep. 285 | * 286 | * @access private 287 | */ 288 | Worker.prototype._preSleep = function (jobServer) { // #unit: not needed 289 | jobServer.send(protocol.encodePacket(protocol.PACKET_TYPES.PRE_SLEEP)); 290 | }; 291 | 292 | 293 | /** 294 | * @method 295 | * @fires Worker#error 296 | * @access protected 297 | * @inheritDoc 298 | */ 299 | Worker.prototype._unrecoverableError = function (msg) { // #unit: not needed 300 | Worker.logger.log('error', msg); 301 | this.emit('error', new Error(msg)); // trigger event 302 | } 303 | 304 | 305 | 306 | /** 307 | * @class Job 308 | */ 309 | common.mixin({ 310 | /** 311 | * Sends a notification to the server (and any listening clients) that the job completed successfully. 312 | * 313 | * @method 314 | * @memberof Job 315 | * @param {string|Buffer} data to be sent to client 316 | * @returns {void} nothing 317 | */ 318 | workComplete: function (data) { 319 | data = data || ''; 320 | this._sendAndClose(protocol.PACKET_TYPES.WORK_COMPLETE, [this.handle, data.toString()]); 321 | }, 322 | /** 323 | * This is sent to update the client with data from a running job. 324 | * A worker should use this when it needs to send updates, 325 | * send partial results, or flush data during long running jobs. 326 | * 327 | * @method 328 | * @memberof Job 329 | * @param {string|Buffer} data to be sent to client 330 | * @returns {void} nothing 331 | */ 332 | sendWorkData: function (data) { 333 | data = data || ''; 334 | var jobServer = this.clientOrWorker._getJobServerByUid(this.jobServerUid); 335 | Worker.logger.log('debug', 'work data, handle=%s, data=%s', this.handle, data.toString()); 336 | jobServer.send(protocol.encodePacket(protocol.PACKET_TYPES.WORK_DATA, [this.handle, data])); 337 | }, 338 | /** 339 | * This is sent to update the server (and any listening clients) of the status of a running job. 340 | * 341 | * @method 342 | * @memberof Job 343 | * @param {number} numerator percent complete numerator 344 | * @param {number} denominator percent complete denominator 345 | * @returns {void} nothing 346 | */ 347 | reportStatus: function (numerator, denominator) { 348 | if (!common.isNumber(numerator) || !common.isNumber(denominator)) { 349 | return new Error('numerator or denominator not a number'); 350 | } 351 | var jobServer = this.clientOrWorker._getJobServerByUid(this.jobServerUid); 352 | jobServer.send(protocol.encodePacket(protocol.PACKET_TYPES.WORK_STATUS, [this.handle, numerator, denominator])); 353 | }, 354 | /** 355 | * This is to notify the server (and any listening clients) that the job failed. 356 | * The job will be closed for additional processing in worker. 357 | * 358 | * @method 359 | * @memberof Job 360 | * @returns {void} nothing 361 | */ 362 | reportError: function () { 363 | Worker.logger.log('warn', 'work failed, handle=%s', this.handle); 364 | this._sendAndClose(protocol.PACKET_TYPES.WORK_FAIL, [this.handle]); 365 | }, 366 | /** 367 | * This is to notify the server (and any listening clients) that the job failed with the given exception. 368 | * The job will be closed for additional processing in worker. 369 | * 370 | * @deprecated https://bugs.launchpad.net/gearmand/+bug/405732 371 | * @method 372 | * @memberof Job 373 | * @param {object} data to be sent to client as text via #toString() invoked on the given object 374 | * @returns {void} nothing 375 | */ 376 | reportException: function (data) { 377 | data = data || '[no more details provided by worker]'; 378 | Worker.logger.log('warn', 'work failed with exception, handle=%s, exception=%s', this.handle, data.toString()); 379 | this._sendAndClose(protocol.PACKET_TYPES.WORK_EXCEPTION, [this.handle, data]); 380 | }, 381 | /** 382 | * This is sent to update the client with a warning. 383 | * 384 | * @method 385 | * @memberof Job 386 | * @param {object} data to be sent to client as text via #toString() invoked on the given object 387 | * @returns {void} nothing 388 | */ 389 | reportWarning: function (data) { 390 | data = data || '[no more details provided by worker]'; 391 | var jobServer = this.clientOrWorker._getJobServerByUid(this.jobServerUid); 392 | Worker.logger.log('warn', 'worker warning, handle=%s, msg=%s', this.handle, data.toString()); 393 | jobServer.send(protocol.encodePacket(protocol.PACKET_TYPES.WORK_WARNING, [this.handle, data])); 394 | }, 395 | 396 | /** 397 | * Re-usable helper method to send data to client and close this job. 398 | * 399 | * @method 400 | * @access private 401 | */ 402 | _sendAndClose: function (packetType, packetData) { 403 | var jobServer = this.clientOrWorker._getJobServerByUid(this.jobServerUid); 404 | jobServer.send(protocol.encodePacket(packetType, packetData)); 405 | this.clientOrWorker._preSleep(jobServer); 406 | this.close(); 407 | } 408 | }, Job.prototype); 409 | -------------------------------------------------------------------------------- /lib/gearmanode/job-server.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The GearmaNode Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | * @fileoverview This script introduces a class representing a Gearman job server 17 | * and knowledge about protocol to communicate with it. 18 | * @author vaclav.sykora@google.com (Vaclav Sykora) 19 | */ 20 | 21 | var net = require('net'), 22 | util = require('util'), 23 | events = require('events'), 24 | winston = require('winston'), 25 | Job = require('./job').Job, 26 | protocol = require('./protocol'), 27 | common = require('./common'); 28 | 29 | 30 | // constants 31 | var constants = exports.CONSTANTS = { 32 | DEFAULT_RECOVER_TIME: 30000 33 | }; 34 | 35 | 36 | /** 37 | * @class JobServer 38 | * @classdesc A class representing an abstraction to Gearman job server (gearmand). 39 | * @constructor 40 | * @augments events.EventEmitter 41 | * 42 | * @param options literal representing the client 43 | * @param {string} options.host hostname of single job server 44 | * @param {number} options.port port of single job server 45 | */ 46 | var JobServer = exports.JobServer = function (options) { 47 | options = options || {}; 48 | 49 | // VALIDATION 50 | var pattern = { host: 'mandatory', port: 'mandatory' }; 51 | var returned = common.verifyOptions(options, pattern); 52 | if (returned instanceof Error) { return returned; } 53 | 54 | this.host = options.host; 55 | this.port = options.port; 56 | 57 | this.connected = false; 58 | this.jobsWaiting4Created = []; // submited jobs waiting for JOB_CREATED response 59 | this.wrongDisconnectAt = 0; 60 | this.failedConnectionCount = 0; 61 | }; 62 | 63 | // inheritance 64 | util.inherits(JobServer, events.EventEmitter); 65 | 66 | // static logger 67 | JobServer.logger = winston.loggers.get('JobServer'); 68 | 69 | 70 | /** 71 | * Method to establish a socket connection to a job server. 72 | * It uses a callback parameter as a synchronisation hook for 'after connect' actions. 73 | * 74 | * @param {function} callback called as error callback when connection failed (error as paramater) or success callback if connection opened (err=undefined) 75 | * @method 76 | * @fires JobServer#js_connect 77 | * @returns {net.Socket} if success 78 | */ 79 | JobServer.prototype.connect = function (callback) { 80 | var self = this; 81 | var eventNames; 82 | 83 | if (!(callback instanceof Function)) { return new Error('invalid callback (not a function)'); } 84 | 85 | if (this.connected) { 86 | callback(); 87 | } else { 88 | this.socket = net.createConnection(this.port, this.host); 89 | 90 | // fallback event registration, only for debugging purposes 91 | eventNames = [ 'lookup', 'timeout', 'drain' ]; 92 | eventNames.forEach(function (name) { 93 | self.socket.on(name, function() { 94 | JobServer.logger.log('warn', 'unhandled event, name=%s', name); 95 | }); 96 | }); 97 | 98 | // emitted when a socket connection is successfully established 99 | this.socket.on('connect', function () { 100 | self.socket.setKeepAlive(true); 101 | self.connected = true; 102 | self.wrongDisconnectAt = 0; 103 | self.failedConnectionCount = 0 104 | self.emit('ConnectInternal140319214558'); 105 | self.clientOrWorker.emit('socketConnect', self.getUid()); // trigger event 106 | JobServer.logger.log('debug', 'connection established, uid=%s', self.getUid()); 107 | callback(); 108 | }); 109 | 110 | // emitted when an error occurs 111 | this.socket.on('error', function (err) { 112 | JobServer.logger.log('error', 'socket error:', err); 113 | // ensures that no more I/O activity happens on this socket 114 | self.socket.destroy(); 115 | 116 | // inform load balancer that this server is invalid 117 | if (self.clientOrWorker._type === 'Client') { 118 | self.clientOrWorker.loadBalancer.badOne(self.clientOrWorker._getJobServerIndexByUid(self.getUid())); 119 | } 120 | 121 | // ECONNREFUSED, EPIPE, ... 122 | self.clientOrWorker.emit('socketError', self.getUid(), err); // trigger event 123 | self.disconnect(err); 124 | 125 | callback(err); 126 | }); 127 | 128 | this.socket.on('data', function (chunk) { 129 | // BF#6 var orig = null; 130 | // if (chunk.length > 500) { 131 | // JobServer.logger.log('verbose', '======================================='); 132 | // orig = chunk; 133 | // chunk = chunk.slice(0, 500); 134 | // } 135 | if (JobServer.logger.isLevelEnabled('verbose')) { 136 | JobServer.logger.log('verbose', 'received packet, len=%d', chunk.length); 137 | JobServer.logger.log('verbose', 'received packet: %s', common.bufferAsHex(chunk)); 138 | } 139 | self._processData(chunk); 140 | // BF#6 if (orig != null) { 141 | // self.socket.emit('data', orig.slice(500)); 142 | // } 143 | }); 144 | 145 | // emitted once the socket is fully closed 146 | this.socket.on('close', function (had_error) { 147 | if (had_error) { // if the socket was closed due to a transmission error 148 | JobServer.logger.log('warn', 'connection closed due to an transmission error, uid=%s', self.getUid()); 149 | // inform load balancer that this server is invalid 150 | if (self.clientOrWorker._type === 'Client') { 151 | self.clientOrWorker.loadBalancer.badOne(self.clientOrWorker._getJobServerIndexByUid(self.getUid())); 152 | } 153 | self.disconnect(true); // true => simulates an error to set `wrongDisconnectAt` attribute 154 | } 155 | }); 156 | 157 | // emitted when the other end of the socket sends a FIN packet (termination of other end) 158 | this.socket.on('end', function (err) { 159 | JobServer.logger.log('warn', 'connection terminated, uid=%s', self.getUid()); 160 | // inform load balancer that this server is invalid 161 | if (self.clientOrWorker._type === 'Client') { 162 | self.clientOrWorker.loadBalancer.badOne(self.clientOrWorker._getJobServerIndexByUid(self.getUid())); 163 | } 164 | self.disconnect(err); 165 | }); 166 | } 167 | 168 | return this.socket; 169 | }; 170 | 171 | 172 | /** 173 | * Ends connection with job server and releases associated resources, 174 | * e.g. underlaying socket connection. 175 | * Sets property 'connected' to 'false'. 176 | * 177 | * @method 178 | * @param {Error} err (optional) error causing the disconnection if any 179 | * @returns {void} nothing 180 | */ 181 | JobServer.prototype.disconnect = function (err) { 182 | var i, eventNames; 183 | 184 | this.connected = false; 185 | this.removeAllListeners(); 186 | if (err !== undefined) { 187 | this.wrongDisconnectAt = new Date(); 188 | this.failedConnectionCount += 1; 189 | } 190 | 191 | // close jobs waiting for packet JOB_CREATED 192 | for (i = 0; i < this.jobsWaiting4Created.length; i ++) { 193 | this.jobsWaiting4Created[i].close(); 194 | } 195 | this.jobsWaiting4Created.length = 0; 196 | 197 | if (this.socket) { 198 | // remove listeners from socket 199 | this.socket.removeAllListeners(); 200 | 201 | this.socket.unref(); // allow the program to exit if this is the only active socket in the event system 202 | this.socket.end(); 203 | this.socket.destroy(); 204 | delete this.socket; 205 | } 206 | 207 | this.clientOrWorker.emit('socketDisconnect', this.getUid(), err); // trigger event 208 | JobServer.logger.log('debug', 'connection closed, uid=%s', this.getUid()); 209 | }; 210 | 211 | 212 | /** 213 | * Sends the job server request that will be echoed back in response. 214 | * 215 | * @method 216 | * @param {string} data opaque data that is echoed back in response 217 | * @returns {void} nothing 218 | */ 219 | JobServer.prototype.echo = function (data) { 220 | if (!common.isString(data)) { return new Error('data to be echoed is not a text'); } 221 | 222 | packet = protocol.encodePacket(protocol.PACKET_TYPES.ECHO_REQ, [data]); 223 | this.send(packet); 224 | }; 225 | 226 | 227 | /** 228 | * Sends the job server request to set an option for the connection in the job server. 229 | * 230 | * @method 231 | * @param {string} optionName name of the option to set 232 | * @returns {void} nothing 233 | */ 234 | JobServer.prototype.setOption = function (optionName) { 235 | if (!common.isString(optionName)) { return new Error('option is not a text'); } 236 | 237 | packet = protocol.encodePacket(protocol.PACKET_TYPES.OPTION_REQ, [optionName]); 238 | this.send(packet); 239 | }; 240 | 241 | 242 | /** 243 | * Sends given data as Buffer through socket connection. 244 | * Underlaying socket connection with job server will be created when does not exist. 245 | * 246 | * @method 247 | * @param {Buffer} data to be sent 248 | * @param {function} callback called as error callback when sending failed (error as paramater) or success callback if sending OK (err=undefined) 249 | * @fires Client#error on associated client when something goes wrong 250 | * @fires Worker#error on associated worker when something goes wrong 251 | * @returns {void} nothing 252 | */ 253 | JobServer.prototype.send = function (data, callback) { 254 | var self = this; 255 | var connectCb, sendCb; 256 | 257 | // VALIDATION 258 | if (!(data instanceof Buffer)) { return new Error('data has to be object of Buffer'); } 259 | if (data.length < protocol.CONSTANTS.HEADER_LEN) { return new Error('short data'); } 260 | if (protocol.CONSTANTS.HEADER_REQ != data.readUInt32BE(0)) { return new Error('no gearman packet'); } 261 | 262 | // invoked after connection established 263 | connectCb = function (connectErr) { 264 | if (connectErr instanceof Error) { // if Error, the problem has been already propagated by this.socket.on('error', ... 265 | self.removeAllListeners('ConnectInternal140319214558'); // remove function waiting for connection 266 | if (callback instanceof Function) { callback(connectErr); } 267 | } 268 | }; 269 | 270 | sendCb = function () { 271 | self.socket.write(data); 272 | if (JobServer.logger.isLevelEnabled('verbose')) { 273 | JobServer.logger.log('verbose', 'packet sent, type=%s, len=%d', protocol.PACKET_CODES[data.readUInt32BE(4)], data.length); 274 | } 275 | if (callback instanceof Function) { callback(); } 276 | }; 277 | 278 | if (this.connected) { 279 | sendCb(); 280 | } else { 281 | JobServer.logger.log('debug', 'unconnected job server, uid=%s', this.getUid()); 282 | if (events.EventEmitter.listenerCount(this, 'ConnectInternal140319214558') == 0) { // connect only if there are no already waiting 'send' function (see BF #9) 283 | this.connect(connectCb); 284 | } 285 | this.once('ConnectInternal140319214558', sendCb); 286 | } 287 | }; 288 | 289 | 290 | /** 291 | * Processes data received from socket. 292 | * 293 | * @method 294 | * @param {Buffer} chunk data readed from socket 295 | * @access private 296 | */ 297 | JobServer.prototype._processData = function (chunk) { 298 | if (!this.connected) { 299 | JobServer.logger.log('warn', 'trying to process data from disconnected job server (disconnect before all packet received?)'); 300 | return; 301 | } 302 | 303 | if (chunk.length < protocol.CONSTANTS.HEADER_LEN) { // it's only header fragment, the rest should come in next packet 304 | this.headerfrag = chunk; 305 | return; 306 | } 307 | 308 | if (chunk.readUInt32BE(0) !== protocol.CONSTANTS.HEADER_RESP) { 309 | if (this.hasOwnProperty('segmentedPacket')) { 310 | chunk = Buffer.concat([this.segmentedPacket, chunk]); 311 | } else if (this.hasOwnProperty('headerfrag')) { 312 | chunk = Buffer.concat([this.headerfrag, chunk]); 313 | delete this.headerfrag; 314 | } else { // not previous packet stored to be concatenated -> it must be error 315 | // OUT OF SYNC! 316 | this.clientOrWorker._unrecoverableError('out of sync with server'); 317 | return; 318 | } 319 | } 320 | 321 | var packetType = chunk.readUInt32BE(4); 322 | var packetCode = protocol.PACKET_CODES[packetType]; 323 | var responseLength = protocol.CONSTANTS.HEADER_LEN + chunk.readUInt32BE(8); 324 | 325 | // store chunk if data is segmented into more packets to be concatenated later 326 | if (chunk.length < responseLength) { 327 | if (JobServer.logger.isLevelEnabled('verbose')) { 328 | JobServer.logger.log('verbose', 'segmented packet found, responseSize=%d, packetLen=%d', responseLength, chunk.length); 329 | } 330 | this.segmentedPacket = chunk; 331 | return; 332 | } 333 | 334 | // split chunk if there are more responses in one packet 335 | if (chunk.length > responseLength) { 336 | if (JobServer.logger.isLevelEnabled('verbose')) { 337 | JobServer.logger.log('verbose', 'joined packet found, responseSize=%d, packetLen=%d', responseLength, chunk.length); 338 | } 339 | nextChunk = new Buffer(chunk.slice(responseLength)); 340 | if (nextChunk.length >= protocol.CONSTANTS.HEADER_LEN) { // header complete 341 | var self = this; 342 | process.nextTick(function() { 343 | self._processData(nextChunk); 344 | }); 345 | } else { // it's only header fragment, the rest should come in next packet 346 | this.headerfrag = nextChunk; 347 | } 348 | 349 | chunk = chunk.slice(0, responseLength); 350 | } 351 | 352 | // parse packet if it is a response 353 | var parsedPacket; 354 | if (protocol.DEFINITION[packetCode] !== undefined 355 | && (protocol.CONSTANTS.TYPE_RESP & protocol.DEFINITION[packetCode][1])) { 356 | delete this.segmentedPacket; // clear cached previous segments if exist or not 357 | parsedPacket = protocol.parsePacket(chunk, protocol.DEFINITION[packetCode][2]); 358 | } 359 | 360 | 361 | var handle, job, processed, status; 362 | 363 | switch (packetType) { 364 | 365 | // CLIENT/WORKER ========================================================== 366 | 367 | case protocol.PACKET_TYPES.ERROR: 368 | JobServer.logger.log('warn', 'job server error, uid=%s, code=%s, msg=%s', 369 | this.getUid(), parsedPacket[1], parsedPacket[2]); 370 | 371 | this.clientOrWorker.emit('jobServerError', this.getUid(), parsedPacket[1], parsedPacket[2]); // trigger event 372 | this.emit('jobServerError', parsedPacket[1], parsedPacket[2]); // trigger event 373 | break; 374 | 375 | case protocol.PACKET_TYPES.ECHO_RES: 376 | case protocol.PACKET_TYPES.OPTION_RES: 377 | this.emit(packetType === protocol.PACKET_TYPES.ECHO_RES ? 'echo' : 'option', parsedPacket[1]); 378 | break; 379 | 380 | 381 | // CLIENT ================================================================= 382 | 383 | case protocol.PACKET_TYPES.JOB_CREATED: 384 | handle = parsedPacket[1]; 385 | 386 | // remove it from queue of jobs waiting for JOB_CREATED 387 | if (this.jobsWaiting4Created.length === 0) { 388 | this.clientOrWorker._unrecoverableError('empty stack of job waiting 4 packet JOB_CREATED'); 389 | return; 390 | } 391 | job = this.jobsWaiting4Created.shift(); 392 | job.handle = handle; 393 | 394 | // and put it into hash of created jobs on Client 395 | this.clientOrWorker.jobs[job.getUid()] = job; 396 | 397 | if (this.jobsWaiting4Created.length === 0) { 398 | JobServer.logger.log('debug', 'no more a job waiting 4 state JOB_CREATED'); 399 | } 400 | 401 | this.clientOrWorker._response(this, packetType, parsedPacket); 402 | break; 403 | 404 | case protocol.PACKET_TYPES.WORK_COMPLETE: 405 | case protocol.PACKET_TYPES.WORK_DATA: // for non-background jobs, the server forwards this packet from the worker to clients (Gearman Docu) 406 | case protocol.PACKET_TYPES.WORK_WARNING: // for non-background jobs, the server forwards this packet from the worker to clients (Gearman Docu) 407 | case protocol.PACKET_TYPES.STATUS_RES: // for background jobs in response to a GET_STATUS request (Gearman Docu) 408 | case protocol.PACKET_TYPES.WORK_STATUS: // for non-background jobs, the server forwards this packet from the worker to clients (Gearman Docu) 409 | case protocol.PACKET_TYPES.WORK_FAIL: // for non-background jobs, the server forwards this packet from the worker to clients (Gearman Docu) 410 | case protocol.PACKET_TYPES.WORK_EXCEPTION: // for non-background jobs, the server forwards this packet from the worker to clients (Gearman Docu) 411 | 412 | this.clientOrWorker._response(this, packetType, parsedPacket); 413 | break; 414 | 415 | // WORKER ================================================================= 416 | 417 | case protocol.PACKET_TYPES.NOOP: 418 | case protocol.PACKET_TYPES.JOB_ASSIGN: 419 | case protocol.PACKET_TYPES.JOB_ASSIGN_UNIQ: 420 | case protocol.PACKET_TYPES.NO_JOB: 421 | 422 | this.clientOrWorker._response(this, packetType, parsedPacket); 423 | break; 424 | 425 | // ======================================================================== 426 | 427 | default: 428 | JobServer.logger.log('warn', 'unknown packet, type=%d', packetType); 429 | processed = chunk.length; // stop processing of rest of the chunk 430 | } 431 | 432 | 433 | if (parsedPacket) { 434 | processed = parsedPacket[0]; 435 | } 436 | 437 | // recursive approach when more packets in buffer 438 | if (processed < chunk.length) { 439 | this._processData(chunk.slice(processed)); 440 | } 441 | }; 442 | 443 | 444 | /** 445 | * Gets an unique identifier of job server represented by the URL address. 446 | * 447 | * @method 448 | * @returns {string} unique identifier 449 | */ 450 | JobServer.prototype.getUid = function () { // #unit: not needed 451 | return this.host + ':' + this.port; 452 | }; 453 | 454 | 455 | /** 456 | * Returns a human readable string representation of the object. 457 | * 458 | * @method 459 | * @returns {string} object description 460 | */ 461 | JobServer.prototype.toString = function () { // #unit: not needed 462 | return 'JobServer(' + this.getUid() + ')'; 463 | }; 464 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ____ _ _ _ 2 | / ___| ___ __ _ _ __ _ __ ___ __ _| \ | | ___ __| | ___ 3 | | | _ / _ \/ _` | '__| '_ ` _ \ / _` | \| |/ _ \ / _` |/ _ \ 4 | | |_| | __/ (_| | | | | | | | | (_| | |\ | (_) | (_| | __/ 5 | \____|\___|\__,_|_| |_| |_| |_|\__,_|_| \_|\___/ \__,_|\___| 6 | 7 | 8 | Node.js library for the [Gearman](http://gearman.org/) distributed job system with support for multiple servers. 9 | 10 | 11 | [![npm version](https://badge.fury.io/js/gearmanode.svg)](http://badge.fury.io/js/gearmanode) 12 | [![Build Status](https://secure.travis-ci.org/veny/GearmaNode.png)](http://travis-ci.org/veny/GearmaNode) 13 | 14 | #### Breaking API change 15 | * v0.2.0 16 | * payload given back to client as `job.response` in `complete`, `workData`, `warning` and `exception` events: is instance of `Buffer` now, unless you provide `toStringEncoding` option in `submitJob` 17 | 18 | ## Features 19 | * fully implemented [Gearman Protocol](http://gearman.org/protocol/) 20 | * support for multiple job servers 21 | * load balancing strategy (`sequence` or `round-robin`) 22 | * recover time (when a server node is down due to maintenance or a crash, load balancer will use the recover-time as a delay before retrying the downed job server) 23 | * support for binary data and miscellaneous string encoding 24 | * careful API documentation 25 | * rock solid tests 26 | * currently more than 130 test scenarios and 400 asserts 27 | * in depth tested with gearman clients and workers written in other languages (Ruby, PHP, Java) 28 | 29 | 30 | ## Installation 31 | 32 | > npm install gearmanode 33 | 34 | * Node package published here: https://npmjs.org/package/gearmanode 35 | 36 | 37 | ## Changelog 38 | See [version.js](https://github.com/veny/GearmaNode/tree/master/lib/gearmanode/version.js) for detailed changelog. 39 | 40 | 41 | ## Usage 42 | 43 | * **Client** 44 | 45 | ```javascript 46 | var gearmanode = require('gearmanode'); 47 | var client = gearmanode.client(); 48 | 49 | var job = client.submitJob('reverse', 'hello world!'); 50 | job.on('workData', function(data) { 51 | console.log('WORK_DATA >>> ' + data); 52 | }); 53 | job.on('complete', function() { 54 | console.log('RESULT >>> ' + job.response); 55 | client.close(); 56 | }); 57 | ``` 58 | 59 | * **Worker** 60 | 61 | ```javascript 62 | var gearmanode = require('gearmanode'); 63 | var worker = gearmanode.worker(); 64 | 65 | worker.addFunction('reverse', function (job) { 66 | job.sendWorkData(job.payload); // mirror input as partial result 67 | job.workComplete(job.payload.toString().split("").reverse().join("")); 68 | }); 69 | ``` 70 | 71 | ### TOC 72 | 73 | See [Geaman Manual](http://gearman.org/manual) to understand generic Gearman concepts. 74 | See [example](https://github.com/veny/GearmaNode/tree/master/example) folder for more detailed samples. 75 | 76 | * [Client](#client) 77 | * [Submit job](#submit-job) 78 | * [Client events](#client-events) 79 | * [Worker](#worker) 80 | * [Register function](#register-function) 81 | * [Set Worker ID](#set-worker-id) 82 | * [Worker events](#worker-events) 83 | * [Job](#job) 84 | * [Job events](#job-events) 85 | * [Job server](#job-server) 86 | * [Job server events](#job-server-events) 87 | * [Binary data](#binary-data) 88 | * [Multiple servers](#multiple-servers) 89 | * [Error handling](#error-handling) 90 | * [Configuration](#configuration) 91 | * [Logger](#logger) 92 | 93 | ### Client 94 | *The client is responsible for creating a job to be run and sending it to a job server. The job server will find a suitable worker that can run the job and forwards the job on.* 95 | -- Gearman Documentation -- 96 | 97 | Instance of class `Client` must be created to connect a Gearman job server(s) and to make requests to perform some function on provided data. 98 | 99 | ```javascript 100 | var gearmanode = require('gearmanode'); 101 | var client = gearmanode.client(); 102 | ``` 103 | 104 | By default, the job server is expected on `localhost:4730`. Following options can be used for detailed configuration of the client: 105 | 106 | * **host** {string} hostname of single job server 107 | * **port** {number} port of single job server 108 | * **servers** {array} array of host,port pairs of multiple job servers 109 | * **loadBalancing** {'Sequence'|'RoundRobin'} name of load balancing strategy 110 | * **recoverTime** {number} delay in milliseconds before retrying the downed job server 111 | 112 | ```javascript 113 | // special port 114 | client = gearmanode.client({port: 4732}); 115 | 116 | // two servers: foo.com:4731, bar.com:4732 117 | client = gearmanode.client({servers: [{host: 'foo.com', port: 4731}, {host: 'bar.com', port: 4732}]}); 118 | 119 | // two servers with default values: foo.com:4730, localhost:4731 120 | client = gearmanode.client({servers: [{host: 'foo.com'}, {port: 4731}]}); 121 | ``` 122 | 123 | #### Submit job 124 | 125 | Client submits job to a Gearman server and futher processed by a worker via `client#submitJob(name, payload, options)` 126 | where `name` is name of registered function a worker is to execute, `payload` is data to be processed 127 | and `options` are additional options as follows: 128 | 129 | * **background** {boolean} flag whether the job should be processed in background/asynchronous 130 | * **priority** {'HIGH'|'NORMAL'|'LOW'} priority in job server queue 131 | * **encoding** - {string} encoding if string data used, **DEPRECATED**: ignored, will be removed in next release, use Buffer with corresponding string encoding as payload 132 | * **unique** {string} unique identifiter for the job 133 | * **toStringEncoding** {string} if given received response will be converted to `String` with this encoding, otherwise payload turned over as `Buffer` 134 | 135 | ```javascript 136 | // by default foreground job with normal priority 137 | var job = client.submitJob('reverse', 'hello world!'); 138 | 139 | // background job 140 | var job = client.submitJob('reverse', 'hello world!', {background: true}); 141 | 142 | // full configured job 143 | var job = client.submitJob('reverse', 'hello world!', {background: false, priority: 'HIGH', unique: 'FooBazBar', toStringEncoding: 'ascii'}); 144 | ``` 145 | 146 | Client-side processing of job is managed via emitted events. See [Job events](#job-events) for more info. 147 | 148 | ```javascript 149 | var client = gearmanode.client(); 150 | var job = client.submitJob('reverse', 'hi'); 151 | job.on('complete', function() { 152 | console.log('RESULT: ' + job.response); 153 | client.close(); 154 | }); 155 | ``` 156 | 157 | A client object should be closed if no more needed to release all its associated resources and socket connections. See the sample above. 158 | 159 | #### Client events 160 | * **socketConnect** - when a job server connected (physical connection is lazy opened by first data sending), has parameter **job server UID** 161 | * **socketDisconnect** - when connection to a job server terminated, has parameter **job server UID** and optional **Error** in case of an unexpected wrong termination 162 | * **socketError** - when a socket problem occurs (connection failure, broken pipe, connection terminated by other end, ...), has parameter **job server UID** and **Error** 163 | * **jobServerError** - when an associated job server encounters an error and needs to notify the client with packet ERROR (19), has parameters **jobServerUid**, **code**, **message** 164 | * **close** - when Client#close() called to end the client for future use and to release all its associated resources 165 | * **error** - when an unrecoverable error occured (e.g. illegal client's state, malformed data ...), has parameter **Error** 166 | 167 | 168 | ### Worker 169 | *The worker performs the work requested by the client and sends a response to the client through the job server.* 170 | -- Gearman Documentation -- 171 | 172 | Instance of class `Worker` must be created to connect a Gearman job server(s), where it then informs the server(s) of all different functions the Worker is capable of doing. 173 | 174 | ```javascript 175 | var gearmanode = require('gearmanode'); 176 | var worker = gearmanode.worker(); 177 | ``` 178 | By default, the job server is expected on `localhost:4730`. Following options can be used for detailed configuration of the worker: 179 | 180 | * **host** [see Client](#client) 181 | * **port** [see Client](#client) 182 | * **servers** [see Client](#client) 183 | * **withUnique** {boolean} flag whether a job will be grabbed with the client assigned unique ID 184 | 185 | #### Register function 186 | 187 | A function the worker is able to perform can be registered via `worker#addFunction(name, callback, options)` 188 | where `name` is a symbolic name of the function, `callback` is a function to be run when a job will be received 189 | and `options` are additional options as follows: 190 | 191 | * **timeout** {number} timeout value in seconds on how long the job is allowed to run, thereafter the job server will mark the job as failed and notify any listening clients 192 | * **toStringEncoding** {string} if given received payload will be converted to `String` with this encoding, otherwise payload turned over as `Buffer` 193 | 194 | The worker function `callback` gets parameter [Job](#job) which is: 195 | 196 | * job event emitter (see [Job events](#job-events)) 197 | * value object to turn over job's parameters 198 | * interface to send job notification/information to the job server 199 | 200 | ```javascript 201 | worker.addFunction('reverse', function (job) { 202 | var rslt = job.payload.toString().split("").reverse().join(""); 203 | job.workComplete(rslt); 204 | }); 205 | 206 | // or with Timeout and conversion to String 207 | 208 | worker.addFunction('reverse', function (job) { 209 | var rslt = job.payload.toString().split("").reverse().join(""); 210 | job.workComplete(rslt); 211 | }, {timeout: 10, toStringEncoding: 'ascii'}); 212 | 213 | ``` 214 | It tries to connect to ALL job servers and fires `error` if one registration fails. 215 | 216 | A registered function can be unregistered via `worker#removeFunction`. 217 | Call `Worker#resetAbilities` to notify the server(s) that the worker is no longer able to do any functions it previously registered. 218 | 219 | #### Set Worker ID 220 | 221 | This method sets the worker ID in all job servers so monitoring and reporting commands can uniquely identify the various workers. 222 | Parameter `workerId` has to be a non-blank string with no whitespaces. 223 | 224 | ```javascript 225 | worker.setWorkerId('FooBazBar'); 226 | ``` 227 | 228 | #### Worker events 229 | * **socketConnect** - when a job server connected (physical connection is lazy opened by first data sending), has parameter **job server UID** 230 | * **socketDisconnect** - when connection to a job server terminated, has parameter **job server UID** and optional **Error** in case of an unexpected wrong termination 231 | * **socketError** - when a socket problem occurs (connection failure, broken pipe, connection terminated by other end, ...), has parameter **job server UID** and **Error** 232 | * **jobServerError** - whenever an associated job server encounters an error and needs to notify the worker with packet ERROR (19), has parameters **jobServerUid**, **code**, **message** 233 | * **close** - when Worker#close() called to close the worker for future use and to release all its associated resources 234 | * **error** - when a fatal error occurred while processing job (e.g. illegal worker's state, socket problem, ...) or job server encounters an error and needs to notify client, has parameter **Error** 235 | 236 | 237 | ### Job 238 | 239 | The `Job` object is an encapsulation of job's attributes and interface for next communication with job server. 240 | Additionally is the object en emitter of events corresponding to job's life cycle (see [Job events](#job-events)). 241 | 242 | The `job` has following getters 243 | 244 | * **name** - name of the function, [Client/Worker] 245 | * **payload** - transmited/received data (Buffer or String) [Client/Worker] 246 | * **response** - data that is returned to the client as a response if job is done by a worker [Client] 247 | * **jobServerUid** - unique identification (UID) of the job server that transmited the job [Client/Worker] 248 | * **handle** - unique handle assigned by job server when job created [Client/Worker] 249 | * **encoding** - encoding to use [Client] **DEPRECATED**: ignored, will be removed in next release, use Buffer with corresponding string encoding as payload 250 | * **unique** - unique identifier assigned by client [Worker] 251 | 252 | and methods 253 | 254 | * **getStatus** - sends request to get status of a background job [Client] 255 | * **workComplete** - sends a notification to the server (and any listening clients) that the job completed successfully [Worker] 256 | * **sendWorkData** - sends updates or partial results [Worker] 257 | * **reportStatus** - reports job's status to the job server [Worker] 258 | * **reportWarning** - sends a warning explicitly to the job server [Worker] 259 | * **reportError** - to indicate that the job failed [Worker] 260 | * **reportException** - to indicate that the job failed with exception (deprecated, provided for backwards compatibility) [Worker] 261 | 262 | #### Job events 263 | * **submited** - when job submited via a job server; server UID stored on the job; has no parameter [Client] 264 | * **created** - when response to one of the SUBMIT_JOB* packets arrived and job handle assigned; has no parameter [Client] 265 | * **status** - to update status information of a submitted jobs [Client] 266 | * in response to a client's request for a **background** job 267 | * status update propagated from worker to client in case of a **non-background** job 268 | * has parameter **status** with attributes: known, running, percent_done_num, percent_done_den (see protocol specification for more info) 269 | * **workData** - to update the client with partial data from a running job, has parameter **data** [Client] 270 | * **warning** - to update the client with a warning, has parameter **data** [Client] 271 | * **complete** - when the non-background job completed successfully, has no parameter [Client] 272 | * **failed** - when a job has been canceled by invoking Job#reportError on worker side, has no parameter [Client] 273 | * **exception** - when the job failed with the an exception, has parameter **text of exception** [Client] 274 | * **timeout** - when the job has been canceled due to timeout, has no parameter [Client/Worker] 275 | * **close** - when Job#close() called or when the job forcible closed by shutdown of client or worker, has no parameter [Client/Worker] 276 | * **error** - when communication with job server failed, has parameter **Error** object [Client/Worker] 277 | 278 | 279 | ### Job server 280 | Class `JobServer` represents an abstraction to Gearman job server (gearmand). 281 | Accessible job server(s) are stored in array `jobServer` on instance of Client/Worker. 282 | The class introduces following methods: 283 | 284 | * **echo** - sends the job server request that will be echoed back in response 285 | * **setOption** - sends the job server request to set an option for the connection in the job server 286 | 287 | ```javascript 288 | var client = gearmanode.client(); 289 | var js = client.jobServers[0]; 290 | 291 | js.once('echo', function(resp) { 292 | console.log('ECHO: response=' + resp); 293 | client.close(); 294 | }); 295 | js.echo('ping') 296 | ``` 297 | 298 | #### Job server events 299 | * **echo** - when response to ECHO_REQ packet arrived, has parameter **data** which is opaque data echoed back in response 300 | * **option** - issued when an option for the connection in the job server was successfully set, has parameter **name** of the option that was set 301 | * **jobServerError** - whenever the job server encounters an error, has parameters **code**, **message** 302 | 303 | 304 | ### Binary data 305 | Both binary data and text with various encoding are supported. By default the data delivered to client and worker are `Buffer` objects. 306 | You can change this approach by providing `toStringEncoding` option in `Client#submitJob` or `Worker#addFunction`. 307 | See following snippets of code or [test-all-stack.js](https://github.com/veny/GearmaNode/blob/master/test/test-all-stack.js) for more inspiration. 308 | 309 | ```javascript 310 | // send text with default encoding; Job#response will be a Buffer object 311 | client.submitJob('reverse', '123'); 312 | 313 | // send text with given encoding; Job#response will be a Buffer object 314 | client.submitJob('reverse', Buffer('123', 'ascii').toString()); 315 | 316 | // send text with given encoding; Job#response will be a String object with ASCII encoding 317 | client.submitJob('reverse', '123', {toStringEncoding: 'ascii'}); 318 | // and receive text on Worker; Job#payload will be a String object with ASCII encoding 319 | worker.addFunction('reverse', function (job) { 320 | job.workComplete(job.payload.split("").reverse().join("")) 321 | }, {toStringEncoding: 'ascii'}); 322 | 323 | // send binary data 324 | client.submitJob('reverse', new Buffer([49, 50, 51])); 325 | ``` 326 | 327 | 328 | ### Multiple servers 329 | Many of Gearman job servers can be started for both high-availability and load balancing. 330 | 331 | [Client](#client) is able to communicate with multiple servers with one of the following load balancing strategy: 332 | 333 | * default mode is `Sequence` which calls job server nodes in the order of nodes defined by the client initialization (next node will be used if the current one fails) 334 | * `RoundRobin` assigns work in round-robin order per nodes defined by the client initialization. 335 | 336 | ```javascript 337 | // default load balancer 338 | client = gearmanode.client({ servers: [{host: 'foo.com'}, {port: 4731}] }); 339 | 340 | // desired load balancer and recover time 341 | client = gearmanode.client({ servers: [{host: 'foo.com'}, {port: 4731}], loadBalancing: 'RoundRobin', recoverTime: 10000 }); 342 | ``` 343 | 344 | [Worker](#worker) can be initialized with multiple servers in order to register a function on each of them. 345 | 346 | ### Error handling 347 | Although exceptions are supported in JavaScript and they can be used to communicate an error, due to asynchronous concept of Node.js it can be a bad idea. 348 | According to Node.js best practices following error handling is introduced in GearmaNode. 349 | 350 | #### Synchronous errors 351 | A synchronous code returns an `Error` object if something goes wrong. This happens mostly in input value validation. 352 | 353 | #### Asynchronous errors 354 | In asynchronous code an error event will be emitted via `EventEmitter` on corresponding object if something goes wrong. 355 | This happens mostly by network communication failure or if a gearman service fails. 356 | 357 | ### Configuration 358 | 359 | #### Logger 360 | `Winston` library is used for logging. See the [project page](https://github.com/flatiron/winston) for details. 361 | 362 | The `GearmaNode` library registers following loggers: 363 | 364 | * Client 365 | * Worker 366 | * JobServer 367 | * Job 368 | * LBStrategy 369 | * protocol 370 | 371 | You can configure the logger in this way: 372 | 373 | ```javascript 374 | gearmanode.Client.logger.transports.console.level = 'info'; 375 | ```` 376 | 377 | 378 | ## Class diagram 379 | 380 | [![](https://raw.github.com/veny/GearmaNode/master/ooad/Classes.png)](https://raw.github.com/veny/GearmaNode/master/ooad/Classes.png) 381 | 382 | 383 | ## Tests 384 | 385 | > cd /path/to/repository 386 | > mocha 387 | 388 | Make sure before starting the tests: 389 | 390 | * job server is running on localhost:4730 391 | * `mocha` test framework is installed 392 | 393 | 394 | ## Author 395 | 396 | * vaclav.sykora@gmail.com 397 | * https://plus.google.com/115674031373998885915 398 | 399 | 400 | ## License 401 | 402 | * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 403 | * see [LICENSE](https://github.com/veny/GearmaNode/tree/master/LICENSE) file for more details 404 | --------------------------------------------------------------------------------