├── test ├── test.png ├── fixtures │ ├── badconfig.yml │ ├── runner.yml │ ├── async.js │ ├── emitkeys.js │ └── reverse.js ├── test-runner.js ├── test-worker.js └── test-client.js ├── .gitignore ├── examples ├── example-runner.sh ├── exampleconfig.yml ├── example-worker.js ├── stalkerQueue.rb ├── handlers │ ├── emitkeys.js │ └── reverse.js └── emitjobs.js ├── index.js ├── .editorconfig ├── .travis.yml ├── bin ├── beanworker └── clear-testtube.js ├── LICENSE ├── package.json ├── lib ├── runner.js ├── worker.js └── client.js ├── .eslintrc └── README.md /test/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ceejbot/fivebeans/HEAD/test/test.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **log 3 | .DS_Store 4 | test/coverage.html 5 | .nyc_output 6 | -------------------------------------------------------------------------------- /examples/example-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./example-worker.js --id=example --config="exampleconfig.yml" 4 | -------------------------------------------------------------------------------- /test/fixtures/badconfig.yml: -------------------------------------------------------------------------------- 1 | beanstalkd: 2 | host: "localhost" 3 | port: 11300 4 | handlers: 5 | - "./test/fixtures/emitkeys.js" 6 | - "./dontexist.js" 7 | watch: 8 | - 'testtube' 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.client = require('./lib/client'); 2 | exports.LOWEST_PRIORITY = exports.client.LOWEST_PRIORITY; 3 | exports.worker = require('./lib/worker'); 4 | exports.runner = require('./lib/runner'); 5 | -------------------------------------------------------------------------------- /examples/exampleconfig.yml: -------------------------------------------------------------------------------- 1 | beanstalkd: 2 | host: "localhost" 3 | port: 11300 4 | handlers: 5 | - "./handlers/emitkeys.js" 6 | - "./handlers/reverse.js" 7 | watch: 8 | - 'testtube' 9 | - 'example-tube' 10 | ignoreDefault: true 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.js] 8 | indent_style = tab 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | curly_bracket_next_line = true 12 | indent_brace_style = Allman 13 | quote_type = single 14 | -------------------------------------------------------------------------------- /test/fixtures/runner.yml: -------------------------------------------------------------------------------- 1 | beanstalkd: 2 | host: "localhost" 3 | port: 11300 4 | handlers: 5 | - "./test/fixtures/emitkeys.js" 6 | - "./test/fixtures/reverse.js" 7 | - "./test/fixtures/async.js" 8 | watch: 9 | - 'testtube' 10 | ignoreDefault: true 11 | timeout: 1 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - '4.*' 6 | - '5.*' 7 | before_install: 8 | - sudo apt-get update -qq 9 | - sudo apt-get install -qq beanstalkd 10 | - sudo beanstalkd -d -l 127.0.0.1 -p 11300 11 | script: npm run travis 12 | after_success: npm run coverage 13 | -------------------------------------------------------------------------------- /bin/beanworker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var argv = require('yargs') 4 | .usage('Usage: beanworker --id=[ID] --config=[config.yml]') 5 | .default('id', 'defaultID') 6 | .demand(['config']) 7 | .argv; 8 | 9 | var FiveBeans = require('fivebeans'); 10 | 11 | var runner = new FiveBeans.runner(argv.id, argv.config); 12 | runner.go(); 13 | -------------------------------------------------------------------------------- /examples/example-worker.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var argv = require('yargs') 4 | .usage('Usage: beanworker --id=[ID] --config=[config.yml]') 5 | .default('id', 'defaultID') 6 | .demand(['config']) 7 | .argv; 8 | 9 | var FiveBeans = require('../index'); 10 | 11 | var runner = new FiveBeans.runner(argv.id, argv.config); 12 | runner.go(); 13 | -------------------------------------------------------------------------------- /examples/stalkerQueue.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "stalker" 4 | 5 | beanurl = "beanstalk://%s:%d" % ['127.0.0.1', 11300] 6 | $beanstalk = Stalker.connect(beanurl) 7 | 8 | Stalker.enqueue('testtube', { 9 | :type => 'reverse', 10 | :payload => "this is a string that should get reversed", 11 | } 12 | ) 13 | puts "Queued a string reverse job using Stalker.enqueue." 14 | -------------------------------------------------------------------------------- /test/fixtures/async.js: -------------------------------------------------------------------------------- 1 | module.exports = function() 2 | { 3 | function AsyncHandler() 4 | { 5 | this.type = 'longasync'; 6 | } 7 | 8 | AsyncHandler.prototype.timer = null; 9 | 10 | AsyncHandler.prototype.work = function(payload, callback) 11 | { 12 | function finish() 13 | { 14 | callback('success'); 15 | } 16 | 17 | this.timeout = setTimeout(finish, 5000); 18 | }; 19 | 20 | var handler = new AsyncHandler(); 21 | return handler; 22 | }; 23 | -------------------------------------------------------------------------------- /test/fixtures/emitkeys.js: -------------------------------------------------------------------------------- 1 | module.exports = function() 2 | { 3 | function EmitKeysHandler() 4 | { 5 | this.type = 'emitkeys'; 6 | } 7 | 8 | // This is an extremely silly kind of job. 9 | EmitKeysHandler.prototype.work = function(payload, callback) 10 | { 11 | var keys = Object.keys(payload); 12 | for (var i = 0; i < keys.length; i++) 13 | console.log(keys[i]); 14 | callback('success'); 15 | }; 16 | 17 | var handler = new EmitKeysHandler(); 18 | return handler; 19 | }; 20 | -------------------------------------------------------------------------------- /examples/handlers/emitkeys.js: -------------------------------------------------------------------------------- 1 | module.exports = function() 2 | { 3 | function EmitKeysHandler() 4 | { 5 | this.type = 'emitkeys'; 6 | } 7 | 8 | // This is an extremely silly kind of job. 9 | EmitKeysHandler.prototype.work = function(payload, callback) 10 | { 11 | var keys = Object.keys(payload); 12 | for (var i = 0; i < keys.length; i++) 13 | console.log(keys[i]); 14 | callback('success'); 15 | }; 16 | 17 | var handler = new EmitKeysHandler(); 18 | return handler; 19 | }; 20 | -------------------------------------------------------------------------------- /examples/handlers/reverse.js: -------------------------------------------------------------------------------- 1 | module.exports = function() 2 | { 3 | function StringReverser() 4 | { 5 | this.type = 'reverse'; 6 | } 7 | 8 | StringReverser.prototype.work = function(payload, callback) 9 | { 10 | console.log(this.reverseString(payload)); 11 | callback('success'); 12 | }; 13 | 14 | StringReverser.prototype.reverseString = function(input) 15 | { 16 | var letters = input.split(''); 17 | letters.reverse(); 18 | return letters.join(''); 19 | }; 20 | 21 | var handler = new StringReverser(); 22 | return handler; 23 | }; 24 | -------------------------------------------------------------------------------- /test/fixtures/reverse.js: -------------------------------------------------------------------------------- 1 | module.exports = function() 2 | { 3 | function StringReverser() 4 | { 5 | this.type = 'reverse'; 6 | } 7 | 8 | StringReverser.prototype.work = function(payload, callback) 9 | { 10 | console.log(this.reverseString(payload)); 11 | callback('success'); 12 | }; 13 | 14 | StringReverser.prototype.reverseString = function(input) 15 | { 16 | var letters = input.split(''); 17 | letters.reverse(); 18 | return letters.join(''); 19 | }; 20 | 21 | var handler = new StringReverser(); 22 | return handler; 23 | }; 24 | -------------------------------------------------------------------------------- /bin/clear-testtube.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fivebeans = require('../index'); 4 | var client = new fivebeans.client('127.0.0.1', 11300); 5 | 6 | function clearAJob() 7 | { 8 | client.peek_buried(function(err, jobid, payload) 9 | { 10 | if (err && err === 'NOT_FOUND') 11 | { 12 | console.log('done'); 13 | process.exit(0); 14 | } 15 | else if (jobid) 16 | { 17 | console.log('nuking ' + jobid, payload.toString()); 18 | client.destroy(jobid, clearAJob); 19 | } 20 | else 21 | console.log(err); 22 | }); 23 | } 24 | 25 | client.on('connect', function handleConnect() 26 | { 27 | client.use('testtube', function handleWatch(err, response) 28 | { 29 | if (err) throw(err); 30 | clearAJob(); 31 | }); 32 | }).on('error', function(err) 33 | { 34 | throw(err); 35 | }); 36 | 37 | client.connect(); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 C J Silverio 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fivebeans", 3 | "description": "beanstalkd client & worker daemon for node.", 4 | "version": "1.5.0", 5 | "author": "C J Silverio ", 6 | "bin": { 7 | "beanworker": "./bin/beanworker" 8 | }, 9 | "bugs": "http://github.com/ceejbot/fivebeans/issues", 10 | "contributors": [ 11 | "Jon Keating ", 12 | "Jevgenij Tsoi " 13 | ], 14 | "dependencies": { 15 | "js-yaml": "~3.6.1", 16 | "lodash": "~4.15.0", 17 | "semver": "~5.3.0", 18 | "yargs": "~5.0.0" 19 | }, 20 | "devDependencies": { 21 | "coveralls": "~2.11.12", 22 | "eslint": "~2.13.1", 23 | "mocha": "~3.0.2", 24 | "must": "~0.13.2", 25 | "nyc": "~8.1.0" 26 | }, 27 | "homepage": "https://github.com/ceejbot/fivebeans#readme", 28 | "keywords": [ 29 | "beanstalkd", 30 | "jobs", 31 | "work-queue", 32 | "worker" 33 | ], 34 | "license": "MIT", 35 | "main": "index", 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/ceejbot/fivebeans.git" 39 | }, 40 | "scripts": { 41 | "coverage": "nyc report --reporter=text-lcov | coveralls", 42 | "lint": "eslint bin lib test index.js", 43 | "test": "nyc mocha -t 8000 -R spec test/", 44 | "travis": "npm run lint && npm test" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/emitjobs.js: -------------------------------------------------------------------------------- 1 | var fivebeans = require('../index'); 2 | 3 | var host = 'localhost'; 4 | var port = 11300; 5 | var tube = 'example-tube'; 6 | 7 | var job1 = 8 | { 9 | type: 'reverse', 10 | payload: 'a man a plan a canal panama' 11 | }; 12 | 13 | var job2 = 14 | { 15 | type: 'emitkeys', 16 | payload: 17 | { 18 | one: 'bloop', 19 | two: 'blooop', 20 | three: 'bloooop', 21 | four: 'blooooop' 22 | } 23 | }; 24 | 25 | var joblist = 26 | [ 27 | { type: 'reverse', payload: 'madam, I\'m Adam' }, 28 | { type: 'reverse', payload: 'satan oscillate my metallic sonatas' }, 29 | { type: 'reverse', payload: 'able was I ere I saw Elba' } 30 | ]; 31 | 32 | var doneEmittingJobs = function() 33 | { 34 | console.log('We reached our completion callback. Now closing down.'); 35 | emitter.end(); 36 | process.exit(0); 37 | }; 38 | 39 | var continuer = function(err, jobid) 40 | { 41 | console.log('emitted job id: ' + jobid); 42 | if (joblist.length === 0) 43 | return doneEmittingJobs(); 44 | 45 | emitter.put(0, 0, 60, JSON.stringify(['testtube', joblist.shift()]), continuer); 46 | }; 47 | 48 | var emitter = new fivebeans.client(host, port); 49 | emitter.on('connect', function() 50 | { 51 | emitter.use('testtube', function(err, tname) 52 | { 53 | console.log("using " + tname); 54 | emitter.put(0, 0, 60, JSON.stringify(['testtube', job1]), function(err, jobid) 55 | { 56 | console.log('queued a string reverse job in testtube: ' + jobid); 57 | emitter.put(0, 0, 60, JSON.stringify(['testtube', job2]), function(err, jobid) 58 | { 59 | console.log('queued a key emitter job in testtube: ' + jobid); 60 | 61 | // And an example of submitting jobs in a loop. 62 | emitter.put(0, 0, 60, JSON.stringify(['testtube', joblist.shift()]), continuer); 63 | }); 64 | }); 65 | }); 66 | }); 67 | 68 | emitter.connect(); 69 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | var 2 | _ = require('lodash'), 3 | assert = require('assert'), 4 | fs = require('fs'), 5 | yaml = require('js-yaml'), 6 | FiveBeansWorker = require('./worker') 7 | ; 8 | 9 | var FiveBeansRunner = function(id, configpath) 10 | { 11 | assert(id); 12 | assert(configpath); 13 | 14 | this.id = id; 15 | if (configpath[0] !== '/') 16 | configpath = process.cwd() + '/' + configpath; 17 | this.configpath = configpath; 18 | 19 | if (!fs.existsSync(configpath)) 20 | throw(new Error(configpath + ' does not exist')); 21 | 22 | this.worker = null; 23 | return this; 24 | }; 25 | 26 | FiveBeansRunner.prototype.go = function() 27 | { 28 | var self = this; 29 | 30 | this.worker = this.createWorker(); 31 | 32 | process.on('SIGINT', this.handleStop.bind(this)); 33 | process.on('SIGQUIT', this.handleStop.bind(this)); 34 | process.on('SIGHUP', this.handleStop.bind(this)); 35 | 36 | process.on('SIGUSR2', function() 37 | { 38 | self.worker.on('stopped', function() 39 | { 40 | self.worker = self.createWorker(); 41 | }); 42 | self.worker.logInfo('received SIGUSR2; stopping & reloading configuration'); 43 | self.worker.stop(); 44 | }); 45 | 46 | return self; 47 | }; 48 | 49 | FiveBeansRunner.prototype.readConfiguration = function() 50 | { 51 | var fname = this.configpath; 52 | var config = yaml.load(fs.readFileSync(fname, 'utf8')); 53 | var dirprefix = process.cwd() + '/'; 54 | var h; 55 | 56 | var handlers = {}; 57 | for (var i = 0, len = config.handlers.length; i < len; i++) 58 | { 59 | h = require(dirprefix + config.handlers[i])(); 60 | handlers[h.type] = h; 61 | } 62 | config.handlers = handlers; 63 | 64 | return config; 65 | }; 66 | 67 | FiveBeansRunner.prototype.createWorker = function() 68 | { 69 | var config = _.extend({}, this.readConfiguration()); 70 | var worker = new FiveBeansWorker(config); 71 | 72 | var logLevel = config.logLevel; 73 | if (logLevel === 'info') 74 | { 75 | worker.on('info', console.log); 76 | logLevel = 'warning'; 77 | } 78 | 79 | if (logLevel === 'warning') 80 | { 81 | worker.on('warning', console.warn); 82 | logLevel = 'error'; 83 | } 84 | 85 | if (logLevel === 'error') 86 | worker.on('error', console.error); 87 | 88 | worker.start(config.watch); 89 | return worker; 90 | }; 91 | 92 | FiveBeansRunner.prototype.handleStop = function() 93 | { 94 | this.worker.on('stopped', function() 95 | { 96 | process.exit(0); 97 | }); 98 | this.worker.stop(); 99 | }; 100 | 101 | module.exports = FiveBeansRunner; 102 | -------------------------------------------------------------------------------- /test/test-runner.js: -------------------------------------------------------------------------------- 1 | /*global describe:true, it:true, before:true, after:true */ 2 | 3 | var 4 | demand = require('must'), 5 | fivebeans = require('../index') 6 | ; 7 | 8 | //------------------------------------------------------------- 9 | 10 | describe('FiveBeansRunner', function() 11 | { 12 | describe('constructor', function() 13 | { 14 | it('throws when not given an id', function() 15 | { 16 | function shouldThrow() 17 | { 18 | return new fivebeans.runner(); 19 | } 20 | shouldThrow.must.throw(Error); 21 | }); 22 | 23 | it('throws when not given a config path', function() 24 | { 25 | function shouldThrow() 26 | { 27 | return new fivebeans.runner('test'); 28 | } 29 | shouldThrow.must.throw(Error); 30 | }); 31 | 32 | it('throws if given a config path that does not exist', function() 33 | { 34 | function shouldThrow() 35 | { 36 | return new fivebeans.runner('test', '/not/a/real/path.yml'); 37 | } 38 | shouldThrow.must.throw(Error); 39 | }); 40 | 41 | it('creates a runner when given valid options', function() 42 | { 43 | var r = new fivebeans.runner('test', 'test/fixtures/runner.yml'); 44 | 45 | r.must.have.property('worker'); 46 | r.id.must.equal('test'); 47 | r.configpath.must.equal(__dirname + '/fixtures/runner.yml'); 48 | }); 49 | }); 50 | 51 | describe('readConfiguration()', function() 52 | { 53 | it('throws when the config requires non-existing handlers', function() 54 | { 55 | var r = new fivebeans.runner('test', 'test/fixtures/badconfig.yml'); 56 | 57 | function shouldThrow() { r.readConfiguration(); } 58 | shouldThrow.must.throw(Error); 59 | }); 60 | 61 | it('returns a config object for a good config', function() 62 | { 63 | var r = new fivebeans.runner('test', 'test/fixtures/runner.yml'); 64 | var config = r.readConfiguration(); 65 | 66 | config.must.be.an.object(); 67 | config.must.have.property('beanstalkd'); 68 | config.beanstalkd.host.must.equal('localhost'); 69 | config.watch.must.be.an.array(); 70 | config.ignoreDefault.must.equal(true); 71 | }); 72 | }); 73 | 74 | describe('createWorker()', function() 75 | { 76 | var worker; 77 | 78 | it('returns a worker', function(done) 79 | { 80 | var r = new fivebeans.runner('test', 'test/fixtures/runner.yml'); 81 | worker = r.createWorker(); 82 | 83 | worker.must.exist(); 84 | worker.must.be.an.object(); 85 | (worker instanceof fivebeans.worker).must.equal(true); 86 | worker.once('started', done); 87 | }); 88 | 89 | it('started the worker', function(done) 90 | { 91 | worker.stopped.must.equal(false); 92 | worker.client.must.exist(); 93 | worker.once('stopped', done); 94 | worker.stop(); 95 | }); 96 | }); 97 | 98 | describe('go()', function() 99 | { 100 | it('creates and starts a worker', function(done) 101 | { 102 | var r = new fivebeans.runner('test', 'test/fixtures/runner.yml'); 103 | r.go(); 104 | 105 | r.worker.must.exist(); 106 | r.worker.client.must.exist(); 107 | r.worker.on('stopped', done); 108 | r.worker.stop(); 109 | }); 110 | }); 111 | 112 | }); 113 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true 5 | }, 6 | 7 | "globals": { 8 | "crypto": true, 9 | "escape": false, 10 | "unescape": false 11 | }, 12 | 13 | "ecmaFeatures": { 14 | "arrowFunctions": true, 15 | "binaryLiterals": true, 16 | "blockBindings": true, 17 | "defaultParams": true, 18 | "forOf": true, 19 | "generators": true, 20 | "objectLiteralComputedProperties": true, 21 | "objectLiteralDuplicateProperties": false, 22 | "objectLiteralShorthandMethods": true, 23 | "objectLiteralShorthandProperties": true, 24 | "octalLiterals": false, 25 | "regexUFlag": true, 26 | "regexYFlag": true, 27 | "superInFunctions": true, 28 | "templateStrings": true, 29 | "unicodeCodePointEscapes": true, 30 | "globalReturn": true 31 | }, 32 | 33 | "rules": { 34 | "block-scoped-var": 0, 35 | "brace-style": [2, "allman", { "allowSingleLine": true }], 36 | "camelcase": 0, 37 | "comma-spacing": [2, {"before": false, "after": true}], 38 | "comma-style": [2, "last"], 39 | "complexity": 0, 40 | "consistent-return": 0, 41 | "consistent-this": 0, 42 | "curly": 0, 43 | "default-case": 0, 44 | "dot-notation": 0, 45 | "eol-last": 2, 46 | "eqeqeq": [2, "allow-null"], 47 | "func-names": 0, 48 | "func-style": [0, "declaration"], 49 | "generator-star": 0, 50 | "global-strict": 0, 51 | "guard-for-in": 0, 52 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 53 | "indent": [2, "tab"], 54 | "max-depth": 0, 55 | "max-len": 0, 56 | "max-nested-callbacks": 0, 57 | "max-params": 0, 58 | "max-statements": 0, 59 | "new-cap": 0, 60 | "new-parens": 2, 61 | "no-alert": 2, 62 | "no-array-constructor": 2, 63 | "no-bitwise": 0, 64 | "no-caller": 2, 65 | "no-catch-shadow": 0, 66 | "no-cond-assign": 2, 67 | "no-console": 0, 68 | "no-constant-condition": 0, 69 | "no-control-regex": 2, 70 | "no-debugger": 2, 71 | "no-delete-var": 2, 72 | "no-div-regex": 0, 73 | "no-dupe-keys": 2, 74 | "no-else-return": 0, 75 | "no-empty": 0, 76 | "no-empty-character-class": 2, 77 | "no-eq-null": 0, 78 | "no-eval": 2, 79 | "no-ex-assign": 2, 80 | "no-extend-native": 2, 81 | "no-extra-bind": 2, 82 | "no-extra-boolean-cast": 2, 83 | "no-extra-parens": 0, 84 | "no-extra-semi": 2, 85 | "no-fallthrough": 2, 86 | "no-floating-decimal": 2, 87 | "no-func-assign": 2, 88 | "no-implied-eval": 2, 89 | "no-inline-comments": 0, 90 | "no-inner-declarations": [2, "functions"], 91 | "no-invalid-regexp": 2, 92 | "no-irregular-whitespace": 2, 93 | "no-iterator": 2, 94 | "no-label-var": 2, 95 | "no-labels": 0, 96 | "no-lone-blocks": 2, 97 | "no-lonely-if": 0, 98 | "no-loop-func": 0, 99 | "no-mixed-requires": [0, false], 100 | "no-mixed-spaces-and-tabs": [2, false], 101 | "no-multi-str": 2, 102 | "no-multiple-empty-lines": [2, {"max": 1}], 103 | "no-native-reassign": 2, 104 | "no-negated-in-lhs": 2, 105 | "no-nested-ternary": 0, 106 | "no-new": 2, 107 | "no-new-func": 2, 108 | "no-new-object": 2, 109 | "no-new-require": 2, 110 | "no-new-wrappers": 2, 111 | "no-obj-calls": 2, 112 | "no-octal": 0, 113 | "no-octal-escape": 2, 114 | "no-path-concat": 0, 115 | "no-plusplus": 0, 116 | "no-process-env": 0, 117 | "no-process-exit": 0, 118 | "no-proto": 2, 119 | "no-redeclare": 2, 120 | "no-regex-spaces": 2, 121 | "no-reserved-keys": 0, 122 | "no-restricted-modules": 0, 123 | "no-return-assign": 2, 124 | "no-script-url": 2, 125 | "no-self-compare": 2, 126 | "no-sequences": 2, 127 | "no-shadow": 0, 128 | "no-shadow-restricted-names": 2, 129 | "no-space-before-semi": 0, 130 | "no-spaced-func": 2, 131 | "no-sparse-arrays": 2, 132 | "no-sync": 0, 133 | "no-ternary": 0, 134 | "no-trailing-spaces": 2, 135 | "no-undef": 2, 136 | "no-undef-init": 2, 137 | "no-undefined": 0, 138 | "no-underscore-dangle": 0, 139 | "no-unreachable": 2, 140 | "no-unused-expressions": 0, 141 | "no-unused-vars": [2, {"vars": "local", "args": "none", "varsIgnorePattern": "demand"}], 142 | "no-use-before-define": 0, 143 | "no-var": 0, 144 | "no-void": 0, 145 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 146 | "no-with": 2, 147 | "one-var": 0, 148 | "operator-assignment": [0, "always"], 149 | "padded-blocks": 0, 150 | "quote-props": 0, 151 | "quotes": [2, "single", "avoid-escape"], 152 | "radix": 2, 153 | "semi": [2, "always"], 154 | "sort-vars": 0, 155 | "space-before-function-paren": [2, "never"], 156 | "space-before-blocks": [2, "always"], 157 | "space-in-brackets": 0, 158 | "space-in-parens": [2, "never"], 159 | "space-infix-ops": 2, 160 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 161 | "strict": 0, 162 | "use-isnan": 2, 163 | "valid-jsdoc": 0, 164 | "valid-typeof": 2, 165 | "vars-on-top": 0, 166 | "wrap-iife": [2, "any"], 167 | "wrap-regex": 0, 168 | "yoda": 0 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | var 2 | _ = require('lodash'), 3 | Beanstalk = require('./client'), 4 | events = require('events'), 5 | util = require('util') 6 | ; 7 | 8 | /* 9 | Events emitted: 10 | 11 | error: payload is error; execution is halted 12 | close: no payload 13 | 14 | warning: payload is object with error information; execution continues 15 | info: payload is object with action info 16 | 17 | started: no payload 18 | stopped: no payload 19 | 20 | job.reserved: job has been reserved; payload is job id 21 | job.handled: payload is object with job info 22 | job.deleted: payload is jobid 23 | job.buried: payload is jobid 24 | */ 25 | 26 | var FiveBeansWorker = function(options) 27 | { 28 | events.EventEmitter.call(this); 29 | 30 | this.id = options.id; 31 | this.host = options.host; 32 | this.port = options.port; 33 | this.handlers = options.handlers; 34 | this.ignoreDefault = options.ignoreDefault; 35 | this.stopped = false; 36 | this.timeout = options.timeout || 10; 37 | 38 | this.client = null; 39 | }; 40 | util.inherits(FiveBeansWorker, events.EventEmitter); 41 | 42 | FiveBeansWorker.prototype.start = function(tubes) 43 | { 44 | var self = this; 45 | this.stopped = false; 46 | 47 | this.on('next', this.doNext.bind(this)); 48 | 49 | function finishedStarting() 50 | { 51 | self.emit('started'); 52 | self.emit('next'); 53 | } 54 | 55 | this.client = new Beanstalk(this.host, this.port); 56 | 57 | this.client.on('connect', function() 58 | { 59 | self.emitInfo('connected to beanstalkd at ' + self.host + ':' + self.port); 60 | self.watch(tubes, function() 61 | { 62 | if (tubes && tubes.length && self.ignoreDefault) 63 | { 64 | self.ignore(['default'], function() 65 | { 66 | finishedStarting(); 67 | }); 68 | } 69 | else 70 | { 71 | finishedStarting(); 72 | } 73 | }); 74 | }); 75 | 76 | this.client.on('error', function(err) 77 | { 78 | self.emitWarning({message: 'beanstalkd connection error', error: err}); 79 | self.emit('error', err); 80 | }); 81 | 82 | this.client.on('close', function() 83 | { 84 | self.emitInfo('beanstalkd connection closed'); 85 | self.emit('close'); 86 | }); 87 | 88 | this.client.connect(); 89 | }; 90 | 91 | FiveBeansWorker.prototype.watch = function(tubes, callback) 92 | { 93 | var self = this; 94 | var tube; 95 | if (tubes && (tube = tubes[0])) 96 | { 97 | self.emitInfo('watching tube ' + tube); 98 | self.client.watch(tube, function(err) 99 | { 100 | if (err) self.emitWarning({ message: 'error watching tube', tube: tube, error: err }); 101 | self.watch(tubes.slice(1), callback); 102 | }); 103 | } 104 | else 105 | callback(); 106 | }; 107 | 108 | FiveBeansWorker.prototype.ignore = function(tubes, callback) 109 | { 110 | var self = this; 111 | var tube; 112 | if (tubes && (tube = tubes[0])) 113 | { 114 | self.emitInfo('ignoring tube ' + tube); 115 | self.client.ignore(tube, function(err) 116 | { 117 | if (err) self.emitWarning({ message: 'error ignoring tube', tube: tube, error: err }); 118 | self.ignore(tubes.slice(1), callback); 119 | }); 120 | } 121 | else 122 | callback(); 123 | }; 124 | 125 | FiveBeansWorker.prototype.stop = function() 126 | { 127 | this.emitInfo('stopping...'); 128 | this.stopped = true; 129 | }; 130 | 131 | FiveBeansWorker.prototype.doNext = function() 132 | { 133 | var self = this; 134 | if (self.stopped) 135 | { 136 | self.client.end(); 137 | self.emitInfo('stopped'); 138 | self.emit('stopped'); 139 | return; 140 | } 141 | 142 | self.client.reserve_with_timeout(self.timeout, function(err, jobID, payload) 143 | { 144 | if (err) 145 | { 146 | if ('TIMED_OUT' !== err) 147 | self.emitWarning({ message: 'error reserving job', error: err }); 148 | self.emit('next'); 149 | } 150 | else 151 | { 152 | self.emit('job.reserved', jobID); 153 | 154 | var job = null; 155 | try { job = JSON.parse(payload.toString()); } 156 | catch (e) { self.emitWarning({ message: 'parsing job JSON', id: jobID, error: e }); } 157 | if (!job || !_.isObject(job)) 158 | self.buryAndMoveOn(jobID); 159 | else if (job instanceof Array) 160 | self.runJob(jobID, job[1]); 161 | else 162 | self.runJob(jobID, job); 163 | } 164 | }); 165 | }; 166 | 167 | FiveBeansWorker.prototype.runJob = function(jobID, job) 168 | { 169 | var self = this; 170 | var handler = this.lookupHandler(job.type); 171 | if (job.type === undefined) 172 | { 173 | self.emitWarning({ message: 'no job type', id: jobID, job: job }); 174 | self.deleteAndMoveOn(jobID); 175 | } 176 | else if (!handler) 177 | { 178 | self.emitWarning({ message: 'no handler found', id: jobID, type: job.type }); 179 | self.buryAndMoveOn(jobID); 180 | } 181 | else 182 | { 183 | self.callHandler(handler, jobID, job.payload); 184 | } 185 | }; 186 | 187 | FiveBeansWorker.prototype.lookupHandler = function(type) 188 | { 189 | return this.handlers[type]; 190 | }; 191 | 192 | // issue #25 193 | FiveBeansWorker.prototype.callHandler = function callHandler(handler, jobID, jobdata) 194 | { 195 | if (handler.work.length === 3) 196 | { 197 | var patchedHandler = { 198 | work: function(payload, callback) 199 | { 200 | return handler.work(jobID, payload, callback); 201 | } 202 | }; 203 | FiveBeansWorker.prototype.doWork.call(this, patchedHandler, jobID, jobdata); 204 | } 205 | else 206 | { 207 | // pass it right on through 208 | FiveBeansWorker.prototype.doWork.apply(this, arguments); 209 | } 210 | }; 211 | 212 | FiveBeansWorker.prototype.doWork = function doWork(handler, jobID, jobdata) 213 | { 214 | var self = this; 215 | var start = new Date().getTime(); 216 | this.currentJob = jobID; 217 | this.currentHandler = handler; 218 | 219 | try 220 | { 221 | handler.work(jobdata, function(action, delay) 222 | { 223 | var elapsed = new Date().getTime() - start; 224 | 225 | self.emit('job.handled', { id: jobID, type: handler.type, elapsed: elapsed, action: action }); 226 | 227 | switch (action) 228 | { 229 | case 'success': 230 | self.deleteAndMoveOn(jobID); 231 | break; 232 | 233 | case 'release': 234 | self.releaseAndMoveOn(jobID, delay); 235 | break; 236 | 237 | case 'bury': 238 | self.buryAndMoveOn(jobID); 239 | break; 240 | 241 | default: 242 | self.buryAndMoveOn(jobID); 243 | break; 244 | } 245 | }); 246 | } 247 | catch (e) 248 | { 249 | self.emitWarning({ message: 'exception in job handler', id: jobID, handler: handler.type, error: e }); 250 | self.buryAndMoveOn(jobID); 251 | } 252 | }; 253 | 254 | FiveBeansWorker.prototype.buryAndMoveOn = function(jobID) 255 | { 256 | var self = this; 257 | self.client.bury(jobID, Beanstalk.LOWEST_PRIORITY, function(err) 258 | { 259 | if (err) self.emitWarning({ message: 'error burying', id: jobID, error: err }); 260 | self.emit('job.buried', jobID); 261 | self.emit('next'); 262 | }); 263 | }; 264 | 265 | FiveBeansWorker.prototype.releaseAndMoveOn = function(jobID, delay) 266 | { 267 | var self = this; 268 | if (delay === undefined) delay = 30; 269 | 270 | self.client.release(jobID, Beanstalk.LOWEST_PRIORITY, delay, function(err) 271 | { 272 | if (err) self.emitWarning({ message: 'error releasing', id: jobID, error: err }); 273 | self.emit('job.released', jobID); 274 | self.emit('next'); 275 | }); 276 | }; 277 | 278 | FiveBeansWorker.prototype.deleteAndMoveOn = function(jobID) 279 | { 280 | var self = this; 281 | self.client.destroy(jobID, function(err) 282 | { 283 | if (err) self.emitWarning({ message: 'error deleting', id: jobID, error: err }); 284 | self.emit('job.deleted', jobID); 285 | self.emit('next'); 286 | }); 287 | }; 288 | 289 | FiveBeansWorker.prototype.emitInfo = function(message) 290 | { 291 | this.emit('info', { 292 | clientid: this.id, 293 | message: message, 294 | }); 295 | }; 296 | 297 | FiveBeansWorker.prototype.emitWarning = function(data) 298 | { 299 | data.clientid = this.id; 300 | this.emit('warning', data); 301 | }; 302 | 303 | module.exports = FiveBeansWorker; 304 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | var 2 | events = require('events'), 3 | net = require('net'), 4 | util = require('util'), 5 | yaml = require('js-yaml') 6 | ; 7 | 8 | var DEFAULT_HOST = '127.0.0.1'; 9 | var DEFAULT_PORT = 11300; 10 | var LOWEST_PRIORITY = 1000; 11 | 12 | // utilities 13 | 14 | // Turn a function argument hash into an array for slicing. 15 | function argHashToArray(hash) 16 | { 17 | var keys = Object.keys(hash); 18 | var result = []; 19 | for (var i = 0; i < keys.length; i++) 20 | { 21 | result[parseInt(keys[i], 10)] = hash[keys[i]]; 22 | } 23 | return result; 24 | } 25 | 26 | var FiveBeansClient = function(host, port) 27 | { 28 | events.EventEmitter.call(this); 29 | 30 | this.stream = null; 31 | this.handlers = []; 32 | this.buffer = undefined; 33 | this.host = host ? host : DEFAULT_HOST; 34 | this.port = port ? port : DEFAULT_PORT; 35 | }; 36 | util.inherits(FiveBeansClient, events.EventEmitter); 37 | 38 | FiveBeansClient.prototype.connect = function() 39 | { 40 | var self = this, tmp; 41 | 42 | self.stream = net.createConnection(self.port, self.host); 43 | 44 | self.stream.on('data', function(data) 45 | { 46 | if (!self.buffer) 47 | self.buffer = data; 48 | else 49 | { 50 | tmp = new Buffer(self.buffer.length + data.length); 51 | self.buffer.copy(tmp, 0); 52 | data.copy(tmp, self.buffer.length); 53 | self.buffer = tmp; 54 | } 55 | 56 | self.tryHandlingResponse(); 57 | }); 58 | 59 | self.stream.on('connect', function() 60 | { 61 | self.emit('connect'); 62 | }); 63 | 64 | self.stream.on('error', function(err) 65 | { 66 | self.emit('error', err); 67 | }); 68 | 69 | self.stream.on('close', function(err) 70 | { 71 | self.emit('close', err); 72 | }); 73 | }; 74 | 75 | FiveBeansClient.prototype.end = function() 76 | { 77 | if (this.stream) 78 | this.stream.end(); 79 | }; 80 | 81 | FiveBeansClient.prototype.tryHandlingResponse = function() 82 | { 83 | while (true) 84 | { 85 | // Peek at the oldest handler in our list and see if if thinks it's done. 86 | var latest = this.handlers[0]; 87 | if (!latest) break; 88 | 89 | var handler = latest[0]; 90 | var callback = latest[1]; 91 | 92 | if ((handler !== undefined) && (handler !== null)) 93 | { 94 | this.buffer = handler.process(this.buffer); 95 | if (handler.complete) 96 | { 97 | // shift it off & reset 98 | this.handlers.shift(); 99 | if (handler.success) 100 | callback && callback.call.apply(callback, 101 | [null, null].concat(handler.args)); 102 | else 103 | callback && callback.call(null, 104 | handler.args[0]); 105 | 106 | if (typeof handler.remainder !== 'undefined') 107 | { 108 | this.buffer = handler.remainder; 109 | } 110 | } 111 | else 112 | { 113 | handler.reset(); 114 | break; 115 | } 116 | } 117 | else 118 | { 119 | break; 120 | } 121 | } 122 | }; 123 | 124 | // response handlers 125 | 126 | var ResponseHandler = function(expectedResponse) 127 | { 128 | this.expectedResponse = expectedResponse; 129 | return this; 130 | }; 131 | 132 | ResponseHandler.prototype.reset = function() 133 | { 134 | this.complete = false; 135 | this.success = false; 136 | this.args = undefined; 137 | this.header = undefined; 138 | this.body = undefined; 139 | }; 140 | 141 | ResponseHandler.prototype.RESPONSES_REQUIRING_BODY = 142 | { 143 | RESERVED: 'passthrough', 144 | FOUND: 'passthrough', 145 | OK: 'yaml' 146 | }; 147 | 148 | function findInBuffer(buffer, bytes) 149 | { 150 | var ptr = 0, idx = 0; 151 | while (ptr < buffer.length) 152 | { 153 | if (buffer[ptr] === bytes[idx]) 154 | { 155 | idx++; 156 | if (idx === bytes.length) 157 | return (ptr - bytes.length + 1); 158 | } 159 | else 160 | idx = 0; 161 | ptr++; 162 | } 163 | return -1; 164 | } 165 | 166 | var CRLF = new Buffer([0x0d, 0x0a]); 167 | 168 | ResponseHandler.prototype.process = function(data) 169 | { 170 | var eol = findInBuffer(data, CRLF); 171 | 172 | // afaict this is an eslint 2.8.0 bug: it complains about this brace 173 | /*eslint brace-style:0*/ 174 | if(eol > -1) 175 | { 176 | var sliceStart; 177 | 178 | // Header is everything up to the windows line break; 179 | // body is everything after. 180 | this.header = data.toString('utf8', 0, eol); 181 | this.body = data.slice(eol + 2, data.length); 182 | this.args = this.header.split(' '); 183 | 184 | var response = this.args[0]; 185 | if (response === this.expectedResponse) 186 | { 187 | this.success = true; 188 | this.args.shift(); // remove it as redundant 189 | } 190 | if (this.RESPONSES_REQUIRING_BODY[response]) 191 | { 192 | this.parseBody(this.RESPONSES_REQUIRING_BODY[response]); 193 | if (this.complete) 194 | { 195 | sliceStart = eol + 2 + data.length + 2; 196 | if (sliceStart >= data.length) 197 | return new Buffer(0); 198 | return data.slice(eol + 2 + data.length + 2); 199 | } 200 | } 201 | else 202 | { 203 | this.complete = true; 204 | sliceStart = eol + 2; 205 | if (sliceStart >= data.length) 206 | return new Buffer(0); 207 | return data.slice(eol + 2); 208 | } 209 | } 210 | else { 211 | // no response expected (quit) 212 | if ('' === this.expectedResponse) 213 | { 214 | this.success = true; 215 | this.complete = true; 216 | } 217 | } 218 | return data; 219 | }; 220 | 221 | /* 222 | RESERVED \r\n 223 | \r\n 224 | 225 | OK \r\n 226 | \r\n 227 | 228 | Beanstalkd commands like reserve() & stats() return a body. 229 | We must read data in response. 230 | */ 231 | ResponseHandler.prototype.parseBody = function(how) 232 | { 233 | if ((this.body === undefined) || (this.body === null)) 234 | return; 235 | 236 | var expectedLength = parseInt(this.args[this.args.length - 1], 10); 237 | if (this.body.length > (expectedLength + 2)) 238 | { 239 | // Body contains multiple responses. Split off the remaining bytes. 240 | this.remainder = this.body.slice(expectedLength + 2); 241 | this.body = this.body.slice(0, expectedLength + 2); 242 | } 243 | 244 | if (this.body.length === (expectedLength + 2)) 245 | { 246 | this.args.pop(); 247 | var body = this.body.slice(0, expectedLength); 248 | this.complete = true; 249 | 250 | switch (how) 251 | { 252 | case 'yaml': 253 | this.args.push(yaml.load(body.toString())); 254 | break; 255 | 256 | // case 'passthrough': 257 | default: 258 | this.args.push(body); 259 | break; 260 | } 261 | } 262 | }; 263 | 264 | // Implementing the beanstalkd interface. 265 | 266 | function makeBeanstalkCommand(command, expectedResponse, sendsData) 267 | { 268 | // Commands are called as client.COMMAND(arg1, arg2, ... data, callback); 269 | // They're sent to beanstalkd as: COMMAND arg1 arg2 ... 270 | // followed by data. 271 | // So we slice the callback & data from the passed-in arguments, prepend 272 | // the command, then send the arglist otherwise intact. 273 | // We then push a handler for the expected response onto our handler stack. 274 | // Some commands have no args, just a callback (stats, stats-tube, etc); 275 | // That's the case handled when args < 2. 276 | return function() 277 | { 278 | var data, 279 | buffer, 280 | args = argHashToArray(arguments), 281 | callback = args.pop(); 282 | 283 | args.unshift(command); 284 | 285 | if (sendsData) 286 | { 287 | data = args.pop(); 288 | if (!Buffer.isBuffer(data)) 289 | data = new Buffer(data); 290 | args.push(data.length); 291 | } 292 | 293 | this.handlers.push([new ResponseHandler(expectedResponse), callback]); 294 | 295 | if (data) 296 | { 297 | buffer = Buffer.concat([new Buffer(args.join(' ')), CRLF, data, CRLF]); 298 | } 299 | else 300 | { 301 | buffer = Buffer.concat([new Buffer(args.join(' ')), CRLF]); 302 | } 303 | this.stream.write(buffer); 304 | }; 305 | } 306 | 307 | // beanstalkd commands 308 | 309 | FiveBeansClient.prototype.use = makeBeanstalkCommand('use', 'USING'); 310 | FiveBeansClient.prototype.put = makeBeanstalkCommand('put', 'INSERTED', true); 311 | 312 | FiveBeansClient.prototype.watch = makeBeanstalkCommand('watch', 'WATCHING'); 313 | FiveBeansClient.prototype.ignore = makeBeanstalkCommand('ignore', 'WATCHING'); 314 | FiveBeansClient.prototype.reserve = makeBeanstalkCommand('reserve', 'RESERVED'); 315 | FiveBeansClient.prototype.reserve_with_timeout = makeBeanstalkCommand('reserve-with-timeout', 'RESERVED'); 316 | FiveBeansClient.prototype.destroy = makeBeanstalkCommand('delete', 'DELETED'); 317 | FiveBeansClient.prototype.release = makeBeanstalkCommand('release', 'RELEASED'); 318 | FiveBeansClient.prototype.bury = makeBeanstalkCommand('bury', 'BURIED'); 319 | FiveBeansClient.prototype.touch = makeBeanstalkCommand('touch', 'TOUCHED'); 320 | FiveBeansClient.prototype.kick = makeBeanstalkCommand('kick', 'KICKED'); 321 | FiveBeansClient.prototype.kick_job = makeBeanstalkCommand('kick-job', 'KICKED'); 322 | 323 | FiveBeansClient.prototype.peek = makeBeanstalkCommand('peek', 'FOUND'); 324 | FiveBeansClient.prototype.peek_ready = makeBeanstalkCommand('peek-ready', 'FOUND'); 325 | FiveBeansClient.prototype.peek_delayed = makeBeanstalkCommand('peek-delayed', 'FOUND'); 326 | FiveBeansClient.prototype.peek_buried = makeBeanstalkCommand('peek-buried', 'FOUND'); 327 | 328 | FiveBeansClient.prototype.list_tube_used = makeBeanstalkCommand('list-tube-used', 'USING'); 329 | FiveBeansClient.prototype.pause_tube = makeBeanstalkCommand('pause-tube', 'PAUSED'); 330 | 331 | // the server returns yaml files in response to these commands 332 | FiveBeansClient.prototype.list_tubes = makeBeanstalkCommand('list-tubes', 'OK'); 333 | FiveBeansClient.prototype.list_tubes_watched = makeBeanstalkCommand('list-tubes-watched', 'OK'); 334 | FiveBeansClient.prototype.stats_job = makeBeanstalkCommand('stats-job', 'OK'); 335 | FiveBeansClient.prototype.stats_tube = makeBeanstalkCommand('stats-tube', 'OK'); 336 | FiveBeansClient.prototype.stats = makeBeanstalkCommand('stats', 'OK'); 337 | 338 | // closes the connection, no response 339 | FiveBeansClient.prototype.quit = makeBeanstalkCommand('quit', ''); 340 | 341 | // end beanstalkd commands 342 | 343 | module.exports = FiveBeansClient; 344 | FiveBeansClient.LOWEST_PRIORITY = LOWEST_PRIORITY; 345 | -------------------------------------------------------------------------------- /test/test-worker.js: -------------------------------------------------------------------------------- 1 | /*global describe:true, it:true, before:true, after:true, beforeEach:true, afterEach:true */ 2 | 3 | var 4 | demand = require('must'), 5 | events = require('events'), 6 | fivebeans = require('../index'), 7 | util = require('util') 8 | ; 9 | 10 | //------------------------------------------------------------- 11 | // some job handlers for testing 12 | 13 | var asyncHandler = require('./fixtures/async')(); 14 | 15 | function TestHandler() 16 | { 17 | events.EventEmitter.call(this); 18 | this.type = 'reverse'; 19 | } 20 | util.inherits(TestHandler, events.EventEmitter); 21 | 22 | TestHandler.prototype.work = function(payload, callback) 23 | { 24 | this.emit('result', this.reverseWords(payload.words)); 25 | callback(payload.trigger || 'success', 0); 26 | }; 27 | 28 | TestHandler.prototype.reverseWords = function(input) 29 | { 30 | var words = input.split(' '); 31 | words.reverse(); 32 | return words.join(' '); 33 | }; 34 | 35 | //------------------------------------------------------------- 36 | 37 | var host = '127.0.0.1'; 38 | var port = 11300; 39 | var tube = 'testtube'; 40 | 41 | var testopts = { 42 | id: 'testworker', 43 | host: host, 44 | port: port, 45 | ignoreDefault: true, 46 | handlers: 47 | { 48 | reverse: new TestHandler(), 49 | longasync: asyncHandler, 50 | }, 51 | timeout: 1 52 | }; 53 | 54 | //------------------------------------------------------------- 55 | 56 | describe('FiveBeansWorker', function() 57 | { 58 | this.timeout(5000); 59 | var producer; 60 | 61 | before(function(done) 62 | { 63 | producer = new fivebeans.client(host, port); 64 | producer.once('connect', function() 65 | { 66 | producer.use(tube, function(err, resp) 67 | { 68 | demand(err).not.exist(); 69 | done(); 70 | }); 71 | }); 72 | producer.connect(); 73 | }); 74 | 75 | describe('constructor', function() 76 | { 77 | it('creates a worker with the passed-in options', function() 78 | { 79 | var opts = { 80 | id: 'testworker', 81 | host: 'example.com', 82 | port: 3000 83 | }; 84 | var w = new fivebeans.worker(opts); 85 | 86 | w.id.must.equal(opts.id); 87 | w.host.must.equal(opts.host); 88 | w.port.must.equal(opts.port); 89 | }); 90 | 91 | it('inherits from EventEmitter', function() 92 | { 93 | var w = new fivebeans.worker({ id: 'testworker' }); 94 | w.must.have.property('on'); 95 | w.on.must.be.a.function(); 96 | }); 97 | 98 | it('respects the timeout option', function() 99 | { 100 | var opts = { 101 | id: 'testworker', 102 | host: 'example.com', 103 | port: 3000, 104 | timeout: 20 105 | }; 106 | var w = new fivebeans.worker(opts); 107 | w.timeout.must.equal(20); 108 | }); 109 | }); 110 | 111 | describe('starting & stopping', function() 112 | { 113 | var w; 114 | 115 | it('emits the error event on failure', function(done) 116 | { 117 | w = new fivebeans.worker({id: 'fail', port: 5000}); 118 | w.on('error', function(err) 119 | { 120 | err.must.exist(); 121 | err.must.have.property('errno'); 122 | err.errno.must.equal('ECONNREFUSED'); 123 | done(); 124 | }); 125 | w.start(); 126 | }); 127 | 128 | it('emits the started event on success', function(done) 129 | { 130 | w = new fivebeans.worker(testopts); 131 | w.once('started', function() 132 | { 133 | done(); 134 | }).on('error', function(err) 135 | { 136 | throw(err); 137 | }); 138 | w.start(); 139 | }); 140 | 141 | it('stops and cleans up when stopped', function(done) 142 | { 143 | w.on('stopped', function() 144 | { 145 | w.stopped.must.equal(true); 146 | done(); 147 | }); 148 | 149 | w.stop(); 150 | }); 151 | 152 | it('watches tubes on start', function(done) 153 | { 154 | var worker = new fivebeans.worker(testopts); 155 | // worker.on('info', function(obj) { console.log(obj); }) 156 | // worker.on('warning', function(obj) { console.error(util.inspect(obj)); }) 157 | 158 | function handleStart() 159 | { 160 | worker.client.list_tubes_watched(function(err, response) 161 | { 162 | demand(err).not.exist(); 163 | response.must.be.an.array(); 164 | response.length.must.equal(2); 165 | response.indexOf(tube).must.be.above(-1); 166 | 167 | worker.removeListener('started', handleStart); 168 | worker.stop(); 169 | }); 170 | } 171 | 172 | worker.on('started', handleStart); 173 | worker.on('stopped', done); 174 | worker.start([tube, 'unused']); 175 | }); 176 | }); 177 | 178 | describe('job processing', function() 179 | { 180 | var worker; 181 | 182 | before(function(done) 183 | { 184 | worker = new fivebeans.worker(testopts); 185 | worker.on('started', done); 186 | worker.start([tube, 'unused']); 187 | }); 188 | 189 | it('deletes jobs with bad formats', function(done) 190 | { 191 | var job = { format: 'bad'}; 192 | producer.put(0, 0, 60, JSON.stringify(job), function(err, jobid) 193 | { 194 | demand(err).not.exist(); 195 | jobid.must.exist(); 196 | 197 | function detectReady() 198 | { 199 | producer.peek_ready(function(err, jobid, payload) 200 | { 201 | err.must.exist(); 202 | err.must.equal('NOT_FOUND'); 203 | done(); 204 | }); 205 | } 206 | 207 | setTimeout(detectReady, 500); 208 | }); 209 | }); 210 | 211 | it('buries jobs with bad json', function(done) 212 | { 213 | function handleBuried(jobid) 214 | { 215 | producer.peek_buried(function(err, buriedID, payload) 216 | { 217 | demand(err).not.exist(); 218 | buriedID.must.equal(jobid); 219 | producer.destroy(buriedID, function(err) 220 | { 221 | demand(err).not.exist(); 222 | done(); 223 | }); 224 | }); 225 | } 226 | 227 | worker.once('job.buried', handleBuried); 228 | 229 | producer.put(0, 0, 60, '{ I am invalid JSON', function(err, jobid) 230 | { 231 | demand(err).not.exist(); 232 | jobid.must.exist(); 233 | }); 234 | }); 235 | 236 | it('buries jobs for which it has no handler', function(done) 237 | { 238 | function handleBuried(jobid) 239 | { 240 | producer.peek_buried(function(err, buriedID, payload) 241 | { 242 | demand(err).not.exist(); 243 | buriedID.must.equal(jobid); 244 | producer.destroy(buriedID, function(err) 245 | { 246 | demand(err).not.exist(); 247 | done(); 248 | }); 249 | }); 250 | } 251 | 252 | worker.once('job.buried', handleBuried); 253 | var job = { type: 'unknown', payload: 'extremely important!'}; 254 | producer.put(0, 0, 60, JSON.stringify(job), function(err, jobid) 255 | { 256 | demand(err).not.exist(); 257 | jobid.must.exist(); 258 | }); 259 | }); 260 | 261 | it('passes good jobs to handlers', function(done) 262 | { 263 | function verifyResult(item) 264 | { 265 | item.must.exist(); 266 | item.must.be.a.string(); 267 | item.must.equal('yo success'); 268 | done(); 269 | } 270 | 271 | testopts.handlers.reverse.once('result', verifyResult); 272 | var job = { type: 'reverse', payload: {words: 'success yo', trigger: 'success' }}; 273 | producer.put(0, 0, 60, JSON.stringify(job), function(err, jobid) 274 | { 275 | demand(err).not.exist(); 276 | jobid.must.exist(); 277 | }); 278 | }); 279 | 280 | it('handles jobs that contain arrays (for ruby compatibility)', function(done) 281 | { 282 | worker.once('job.deleted', function(result) { done(); }); 283 | var job = ['stalker', { type: 'reverse', payload: {words: 'not important', trigger: 'success'}}]; 284 | producer.put(0, 0, 60, JSON.stringify(job), function(err, jobid) 285 | { 286 | demand(err).not.exist(); 287 | jobid.must.exist(); 288 | }); 289 | }); 290 | 291 | it('buries jobs when the handler responds with "bury"', function(done) 292 | { 293 | function detectBuried(jobid) 294 | { 295 | producer.peek_buried(function(err, buriedID, payload) 296 | { 297 | demand(err).not.exist(); 298 | buriedID.must.equal(jobid); 299 | producer.destroy(buriedID, function(err) 300 | { 301 | demand(err).not.exist(); 302 | done(); 303 | }); 304 | }); 305 | } 306 | 307 | worker.once('job.buried', detectBuried); 308 | 309 | var job = { type: 'reverse', payload: { words: 'bury', trigger: 'bury' }}; 310 | producer.put(0, 0, 60, JSON.stringify(job), function(err, jobid) 311 | { 312 | demand(err).not.exist(); 313 | jobid.must.exist(); 314 | }); 315 | }); 316 | 317 | it('successfully handles jobs with non-ascii characters', function(done) 318 | { 319 | testopts.handlers.reverse.once('result', function(result) 320 | { 321 | result.must.equal('brûlée crèmes'); 322 | done(); 323 | }); 324 | var job = { type: 'reverse', payload: { words: 'crèmes brûlée', trigger: 'success' }}; 325 | producer.put(0, 0, 60, JSON.stringify(job), function(err, jobid) 326 | { 327 | demand(err).not.exist(); 328 | jobid.must.exist(); 329 | }); 330 | }); 331 | 332 | it('can call touch() on jobs in progress', function(done) 333 | { 334 | this.timeout(15000); 335 | 336 | var jobid, timeleft; 337 | 338 | function getInfo() 339 | { 340 | worker.client.stats_job(jobid, function(err, info) 341 | { 342 | demand(err).not.exist(); 343 | timeleft = info['time-left']; 344 | timeleft.must.be.below(27); // 30 seconds minus the 2 second wait 345 | 346 | worker.client.touch(jobid, function(err) 347 | { 348 | demand(err).not.exist(); 349 | 350 | worker.client.stats_job(jobid, function(err, info2) 351 | { 352 | // now test that the wait has been reset 353 | demand(err).not.exist(); 354 | info2['time-left'].must.be.above(timeleft); 355 | }); 356 | }); 357 | }); 358 | } 359 | 360 | function handleReserved(id) 361 | { 362 | jobid = id; 363 | worker.once('job.handled', function() { done(); }); 364 | 365 | setTimeout(getInfo, 3000); 366 | } 367 | 368 | worker.once('job.reserved', handleReserved); 369 | worker.on('warning', console.log); 370 | 371 | var job = { type: 'longasync', payload: { words: 'ignored', trigger: 'ignored' }}; 372 | producer.put(0, 0, 30, JSON.stringify(job), function(err, jobid) 373 | { 374 | demand(err).not.exist(); 375 | jobid.must.exist(); 376 | }); 377 | }); 378 | 379 | it('releases jobs when the handler responds with "release"', function(done) 380 | { 381 | function detectReleased(jobid) 382 | { 383 | worker.stop(); 384 | 385 | producer.peek_ready(function(err, releasedID, payload) 386 | { 387 | demand(err).not.exist(); 388 | releasedID.must.equal(jobid); 389 | producer.destroy(releasedID, function(err) 390 | { 391 | demand(err).not.exist(); 392 | done(); 393 | }); 394 | }); 395 | } 396 | 397 | worker.once('job.released', detectReleased); 398 | 399 | var job = { type: 'reverse', payload: { words: 'release', trigger: 'release' }}; 400 | producer.put(0, 0, 60, JSON.stringify(job), function(err, jobid) 401 | { 402 | demand(err).not.exist(); 403 | jobid.must.exist(); 404 | }); 405 | }); 406 | }); 407 | 408 | describe('log events', function() 409 | { 410 | it('have tests'); 411 | }); 412 | }); 413 | -------------------------------------------------------------------------------- /test/test-client.js: -------------------------------------------------------------------------------- 1 | /*global describe:true, it:true, before:true, after:true */ 2 | 3 | var 4 | demand = require('must'), 5 | fivebeans = require('../index'), 6 | fs = require('fs'), 7 | semver = require('semver') 8 | ; 9 | 10 | var host = '127.0.0.1'; 11 | var port = 11300; 12 | var tube = 'testtube'; 13 | 14 | function readTestImage() 15 | { 16 | return fs.readFileSync('./test/test.png'); 17 | } 18 | 19 | describe('FiveBeansClient', function() 20 | { 21 | var producer, consumer, testjobid; 22 | var version; 23 | 24 | before(function() 25 | { 26 | producer = new fivebeans.client(host); 27 | consumer = new fivebeans.client(host, port); 28 | }); 29 | 30 | describe('#FiveBeansClient()', function() 31 | { 32 | it('creates a client with the passed-in options', function() 33 | { 34 | producer.host.must.equal(host); 35 | producer.port.must.equal(port); 36 | }); 37 | }); 38 | 39 | describe('#connect()', function() 40 | { 41 | it('creates and saves a connection', function(done) 42 | { 43 | producer.on('connect', function() 44 | { 45 | producer.stream.must.exist(); 46 | done(); 47 | 48 | }).on('error', function(err) 49 | { 50 | throw(err); 51 | }); 52 | producer.connect(); 53 | }); 54 | }); 55 | 56 | describe('job producer:', function() 57 | { 58 | it('#use() connects to a specific tube', function(done) 59 | { 60 | producer.use(tube, function(err, response) 61 | { 62 | demand(err).not.exist(); 63 | response.must.equal(tube); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('#list_tube_used() returns the tube used by a producer', function(done) 69 | { 70 | producer.list_tube_used(function(err, response) 71 | { 72 | demand(err).not.exist(); 73 | response.must.equal(tube); 74 | done(); 75 | }); 76 | }); 77 | 78 | it('#put() submits a job', function(done) 79 | { 80 | var data = { type: 'test', payload: 'the explosive energy of the warhead of a missile or of the bomb load of an aircraft' }; 81 | producer.put(0, 0, 60, JSON.stringify(data), function(err, jobid) 82 | { 83 | demand(err).not.exist(); 84 | jobid.must.exist(); 85 | done(); 86 | }); 87 | }); 88 | 89 | after(function(done) 90 | { 91 | producer.stats(function(err, response) 92 | { 93 | demand(err).not.exist(); 94 | if (response.version) 95 | version = response.version + '.0'; 96 | done(); 97 | }); 98 | 99 | }); 100 | }); 101 | 102 | describe('job consumer:', function() 103 | { 104 | it('#watch() watches a tube', function(done) 105 | { 106 | consumer.on('connect', function() 107 | { 108 | consumer.watch(tube, function(err, response) 109 | { 110 | demand(err).not.exist(); 111 | response.must.equal('2'); 112 | done(); 113 | }); 114 | }).on('error', function(err) 115 | { 116 | throw(err); 117 | }); 118 | consumer.connect(); 119 | }); 120 | 121 | it('#ignore() ignores a tube', function(done) 122 | { 123 | consumer.ignore('default', function(err, response) 124 | { 125 | demand(err).not.exist(); 126 | response.must.equal('1'); 127 | done(); 128 | }); 129 | }); 130 | 131 | it('#list_tubes_watched() returns the tubes the consumer watches', function(done) 132 | { 133 | consumer.list_tubes_watched(function(err, response) 134 | { 135 | demand(err).not.exist(); 136 | response.length.must.equal(1); 137 | response.indexOf(tube).must.equal(0); 138 | done(); 139 | }); 140 | }); 141 | 142 | it('#peek_ready() peeks ahead at jobs', function(done) 143 | { 144 | this.timeout(4000); 145 | producer.peek_ready(function(err, jobid, payload) 146 | { 147 | demand(err).not.exist(); 148 | jobid.must.exist(); 149 | testjobid = jobid; 150 | var parsed = JSON.parse(payload); 151 | parsed.must.have.property('type'); 152 | parsed.type.must.equal('test'); 153 | done(); 154 | }); 155 | }); 156 | 157 | it('#stats_job() returns job stats', function(done) 158 | { 159 | consumer.stats_job(testjobid, function(err, response) 160 | { 161 | demand(err).not.exist(); 162 | response.must.be.an.object(); 163 | response.must.have.property('id'); 164 | response.id.must.equal(parseInt(testjobid, 10)); 165 | response.tube.must.equal(tube); 166 | done(); 167 | }); 168 | }); 169 | 170 | it('consumer can run stats_job() while a job is reserved', function(done) 171 | { 172 | consumer.reserve(function(err, jobid, payload) 173 | { 174 | demand(err).not.exist(); 175 | consumer.stats_job(jobid, function(err, res) 176 | { 177 | demand(err).not.exist(); 178 | res.must.be.an.object(); 179 | res.must.have.property('id'); 180 | res.id.must.equal(parseInt(jobid, 10)); 181 | res.state.must.equal('reserved'); 182 | consumer.release(jobid, 1, 1, function(err) 183 | { 184 | demand(err).not.exist(); 185 | done(); 186 | }); 187 | }); 188 | }); 189 | }); 190 | 191 | it('#reserve() returns a job', function(done) 192 | { 193 | consumer.reserve(function(err, jobid, payload) 194 | { 195 | demand(err).not.exist(); 196 | jobid.must.equal(testjobid); 197 | var parsed = JSON.parse(payload); 198 | parsed.must.have.property('type'); 199 | parsed.type.must.equal('test'); 200 | done(); 201 | }); 202 | }); 203 | 204 | it('#touch() informs the server the client is still working', function(done) 205 | { 206 | consumer.touch(testjobid, function(err) 207 | { 208 | demand(err).not.exist(); 209 | done(); 210 | }); 211 | }); 212 | 213 | it('#release() releases a job', function(done) 214 | { 215 | consumer.release(testjobid, 1, 1, function(err) 216 | { 217 | demand(err).not.exist(); 218 | done(); 219 | }); 220 | }); 221 | 222 | it('jobs can contain binary data', function(done) 223 | { 224 | var payload = readTestImage(); 225 | var ptr = 0; 226 | 227 | producer.put(0, 0, 60, payload, function(err, jobid) 228 | { 229 | demand(err).not.exist(); 230 | jobid.must.exist(); 231 | 232 | consumer.reserve(function(err, returnID, returnPayload) 233 | { 234 | demand(err).not.exist(); 235 | returnID.must.equal(jobid); 236 | 237 | // we should get back exactly the same bytes we put in 238 | returnPayload.length.must.equal(payload.length); 239 | while (ptr < returnPayload.length) 240 | { 241 | returnPayload[ptr].must.equal(payload[ptr]); 242 | ptr++; 243 | } 244 | consumer.destroy(returnID, function(err) 245 | { 246 | demand(err).not.exist(); 247 | done(); 248 | }); 249 | }); 250 | }); 251 | }); 252 | 253 | it('jobs can contain utf8 data', function(done) 254 | { 255 | var payload = 'Many people like crème brûlée.'; 256 | var returnString; 257 | producer.put(0, 0, 60, payload, function(err, jobid) 258 | { 259 | demand(err).not.exist(); 260 | jobid.must.exist(); 261 | 262 | consumer.reserve(function(err, returnID, returnPayload) 263 | { 264 | demand(err).not.exist(); 265 | returnID.must.equal(jobid); 266 | // we should get back exactly the same bytes we put in 267 | returnString = returnPayload.toString(); 268 | returnString.must.equal(payload); 269 | consumer.destroy(returnID, function(err) 270 | { 271 | demand(err).not.exist(); 272 | done(); 273 | }); 274 | }); 275 | }); 276 | }); 277 | 278 | it('#peek_delayed() returns data for a delayed job', function(done) 279 | { 280 | producer.peek_delayed(function(err, jobid, payload) 281 | { 282 | demand(err).not.exist(); 283 | jobid.must.equal(testjobid); 284 | done(); 285 | }); 286 | }); 287 | 288 | it('#bury() buries a job (> 1sec expected)', function(done) 289 | { 290 | // this takes a second because of the minumum delay enforced by release() above 291 | this.timeout(3000); 292 | consumer.reserve(function(err, jobid, payload) 293 | { 294 | demand(err).not.exist(); 295 | 296 | consumer.bury(jobid, fivebeans.LOWEST_PRIORITY, function(err) 297 | { 298 | demand(err).not.exist(); 299 | done(); 300 | }); 301 | }); 302 | }); 303 | 304 | it('#peek_buried() returns data for a buried job', function(done) 305 | { 306 | producer.peek_buried(function(err, jobid, payload) 307 | { 308 | demand(err).not.exist(); 309 | jobid.must.equal(testjobid); 310 | done(); 311 | }); 312 | }); 313 | 314 | it('#kick() un-buries jobs in the producer\'s used queue', function(done) 315 | { 316 | producer.kick(10, function(err, count) 317 | { 318 | demand(err).not.exist(); 319 | count.must.equal('1'); 320 | done(); 321 | }); 322 | }); 323 | 324 | it('#kick_job() kicks a specific job id', function(done) 325 | { 326 | // Skip the test if the version of beanstalkd doesn't have this command. 327 | // Beanstalkd does not have semver-compliant version numbers, however. 328 | if (version.match(/\d+\.\d+\.\d+\.\d+/)) 329 | { 330 | version = version.replace(/\.\d+$/, ''); 331 | } 332 | if (!semver.satisfies(version, '>= 1.8.0')) 333 | return done(); 334 | 335 | consumer.reserve(function(err, jobid, payload) 336 | { 337 | demand(err).not.exist(); 338 | consumer.bury(testjobid, fivebeans.LOWEST_PRIORITY, function(err) 339 | { 340 | demand(err).not.exist(); 341 | 342 | producer.kick_job(testjobid, function(err) 343 | { 344 | demand(err).not.exist(); 345 | done(); 346 | }); 347 | }); 348 | }); 349 | }); 350 | 351 | it('#pause_tube() suspends new job reservations (> 1sec expected)', function(done) 352 | { 353 | consumer.pause_tube(tube, 3, function(err) 354 | { 355 | demand(err).not.exist(); 356 | consumer.reserve_with_timeout(1, function(err, jobid, payload) 357 | { 358 | err.must.equal('TIMED_OUT'); 359 | done(); 360 | }); 361 | }); 362 | }); 363 | 364 | it('#destroy() deletes a job (nearly 2 sec expected)', function(done) 365 | { 366 | // this takes a couple of seconds because of the minumum delay enforced by pause_tube() above 367 | this.timeout(5000); 368 | consumer.reserve(function(err, jobid, payload) 369 | { 370 | demand(err).not.exist(); 371 | consumer.destroy(jobid, function(err) 372 | { 373 | demand(err).not.exist(); 374 | done(); 375 | }); 376 | }); 377 | }); 378 | 379 | it('#reserve_with_timeout() times out when no jobs are waiting (> 1sec expected)', function(done) 380 | { 381 | this.timeout(3000); 382 | consumer.reserve_with_timeout(1, function(err, jobid, payload) 383 | { 384 | err.must.equal('TIMED_OUT'); 385 | done(); 386 | }); 387 | }); 388 | }); 389 | 390 | describe('server statistics', function() 391 | { 392 | it('#stats() returns a hash of server stats', function(done) 393 | { 394 | consumer.stats(function(err, response) 395 | { 396 | demand(err).not.exist(); 397 | response.must.be.an.object(); 398 | response.must.have.property('pid'); 399 | response.must.have.property('version'); 400 | done(); 401 | }); 402 | }); 403 | 404 | it('#list_tubes() returns a list of tubes', function(done) 405 | { 406 | consumer.list_tubes(function(err, response) 407 | { 408 | demand(err).not.exist(); 409 | response.length.must.be.above(0); 410 | response.indexOf(tube).must.be.above(-1); 411 | done(); 412 | }); 413 | }); 414 | 415 | it('#stats_tube() returns a hash of tube stats', function(done) 416 | { 417 | consumer.stats_tube(tube, function(err, response) 418 | { 419 | demand(err).not.exist(); 420 | response.must.be.an.object(); 421 | done(); 422 | }); 423 | }); 424 | 425 | it('#stats_tube() returns not found for non-existent tubes', function(done) 426 | { 427 | consumer.stats_tube('i-dont-exist', function(err, response) 428 | { 429 | err.must.be.a.string(); 430 | err.must.equal('NOT_FOUND'); 431 | done(); 432 | }); 433 | }); 434 | }); 435 | 436 | describe('concurrent commands', function() 437 | { 438 | it('can be handled', function(done) 439 | { 440 | var concurrency = 10; 441 | var replied = 0; 442 | var handleResponse = function(err, response) 443 | { 444 | demand(err).not.exist(); 445 | if (++replied >= concurrency) 446 | done(); 447 | }; 448 | for (var i = 0; i < 10; ++i) 449 | consumer.stats_tube(tube, handleResponse); 450 | }); 451 | }); 452 | 453 | }); 454 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A straightforward and (nearly) complete [beanstalkd](http://kr.github.com/beanstalkd/) client for node.js, along with a more opinionated beanstalkd jobs worker & runner. 2 | 3 | [![on npm](http://img.shields.io/npm/v/fivebeans.svg?style=flat)](https://www.npmjs.org/package/fivebeans) [![Tests](http://img.shields.io/travis/ceejbot/fivebeans.svg?style=flat)](http://travis-ci.org/ceejbot/fivebeans) [![Coverage Status](https://img.shields.io/coveralls/ceejbot/fivebeans.svg?style=flat)](https://coveralls.io/github/ceejbot/fivebeans?branch=master) [![Dependencies](http://img.shields.io/david/ceejbot/fivebeans.svg?style=flat)](https://david-dm.org/ceejbot/fivebeans) 4 | 5 | ## FiveBeansClient 6 | 7 | Heavily inspired by [node-beanstalk-client](https://github.com/benlund/node-beanstalk-client), which is a perfectly usable client but somewhat dusty. I wanted more complete support of the beanstalkd protocol in a project written in plain javascript. 8 | 9 | All client method names are the same case & spelling as the beanstalk text command, with hyphens replaced by underscore. The single exception is `delete`, which is renamed to `destroy()`. 10 | 11 | For complete details on the beanstalkd commands, see [its protocol documentation](https://github.com/kr/beanstalkd/blob/master/doc/protocol.md). 12 | 13 | ### Creating a client 14 | 15 | The client constructor takes two arguments: 16 | 17 | __host__: The address of the beanstalkd server. Defaults to `127.0.0.1`. 18 | __port__: Port to connect to. Defaults to `11300`. 19 | 20 | The client emits three events that you should listen for: `connect`, `error`, and `close`. 21 | 22 | The client is not usable until you call its `connect()` method. Here's an example of setting up a client: 23 | 24 | ```javascript 25 | var fivebeans = require('fivebeans'); 26 | 27 | var client = new fivebeans.client('10.0.1.1', 11300); 28 | client 29 | .on('connect', function() 30 | { 31 | // client can now be used 32 | }) 33 | .on('error', function(err) 34 | { 35 | // connection failure 36 | }) 37 | .on('close', function() 38 | { 39 | // underlying connection has closed 40 | }) 41 | .connect(); 42 | ``` 43 | 44 | ### Producing jobs 45 | 46 | #### use 47 | 48 | `client.use(tube, function(err, tubename) {});` 49 | 50 | Use the specified tube. Reponds with the name of the tube being used. 51 | 52 | #### list_tube_used 53 | 54 | `client.list_tube_used(function(err, tubename) {});` 55 | 56 | Responds with the name of the tube currently being used by the client. 57 | 58 | #### put 59 | 60 | `client.put(priority, delay, ttr, payload, function(err, jobid) {});` 61 | 62 | Submit a job with the specified priority (smaller integers are higher priority), delay in seconds, and allowed time-to-run in seconds. The payload contains the job data the server will return to clients reserving jobs; it can be either a Buffer object or a string. No processing is done on the data. Responds with the id of the newly-created job. 63 | 64 | #### peek_ready 65 | 66 | `client.peek_ready(function(err, jobid, payload) {});` 67 | 68 | Peek at the data for the job at the top of the ready queue of the tube currently in use. Responds with the job id and payload of the next job, or 'NOT_FOUND' if there are no qualifying jobs in the tube. The payload is a Buffer object. 69 | 70 | #### peek_delayed 71 | 72 | `client.peek_delayed(function(err, jobid, payload) {});` 73 | 74 | Peek at the data for the delayed job with the shortest delay in the tube currently in use. Responds with the job id and payload of the next job, or 'NOT_FOUND' in *err* if there are no qualifying jobs in the tube. The payload is a Buffer object. 75 | 76 | #### peek_buried 77 | 78 | `client.peek_buried(function(err, jobid, payload) {});` 79 | 80 | Peek at the data for the next buried job in the tube currently in use. Responds with the job id and payload of the next job, or 'NOT_FOUND' in *err* if there are no qualifying jobs in the tube. The payload is a Buffer object. 81 | 82 | ### Consuming jobs 83 | 84 | #### watch 85 | 86 | `client.watch(tube, function(err, numwatched) {});` 87 | 88 | Watch the named tube. Responds with the number of tubes currently watched by the client. 89 | 90 | #### ignore 91 | 92 | `client.ignore(tube, function(err, numwatched) {});` 93 | 94 | Ignore the named tube. Responds with the number of tubes currently watched by the client. 95 | 96 | #### list_tubes_watched 97 | 98 | `client.list_tubes_watched(function(err, tubelist) {});` 99 | 100 | Responds with an array containing the names of the tubes currently watched by the client. 101 | 102 | #### reserve 103 | 104 | `client.reserve(function(err, jobid, payload) {});` 105 | 106 | Reserve a job. Responds with the id and the job data. The payload is a Buffer object. 107 | 108 | #### reserve_with_timeout 109 | 110 | `client.reserve_with_timeout(seconds, function(err, jobid, payload) {});` 111 | 112 | Reserve a job, waiting the specified number of seconds before timing out. *err* contains the string "TIMED_OUT" if the specified time elapsed before a job became available. Payload is a buffer. 113 | 114 | #### touch 115 | 116 | `client.touch(jobid, function(err) {});` 117 | 118 | Inform the server that the client is still processing a job, thus requesting more time to work on it. 119 | 120 | #### destroy 121 | 122 | `client.destroy(jobid, function(err) {});` 123 | 124 | Delete the specified job. Responds with null if successful, a string error otherwise. This is the only method not named identically to its beanstalkd counterpart, because delete is a reserved word in Javascript. 125 | 126 | #### release 127 | 128 | `client.release(jobid, priority, delay, function(err) {});` 129 | 130 | Release the specified job and assign it the given priority and delay (in seconds). Responds with null if successful, a string error otherwise. 131 | 132 | #### bury 133 | 134 | `client.bury(jobid, priority, function(err) {});` 135 | 136 | Bury the specified job and assign it the given priority. Responds with null if successful, a string error otherwise. 137 | 138 | #### kick 139 | 140 | `client.kick(maxToKick, function(err, numkicked) {});` 141 | 142 | Kick at most *maxToKick* delayed and buried jobs back into the active queue. Responds with the number of jobs kicked. 143 | 144 | #### kick_job 145 | 146 | `client.kick_job(jobID, function(err) {});` 147 | 148 | Kick the specified job id. Responds with `NOT_FOUND` if the job was not found. Supported in beanstalkd versions >= 1.6. 149 | 150 | ### Server statistics 151 | 152 | #### peek 153 | 154 | `client.peek(id, function(err, jobid, payload) {});` 155 | 156 | Peek at the data for the specified job. Payload is a Buffer object. 157 | 158 | #### pause_tube 159 | 160 | `client.pause_tube(tubename, delay, function(err) {});` 161 | 162 | Pause the named tube for the given number of seconds. No new jobs may be reserved from the tube while it is paused. 163 | 164 | #### list_tubes 165 | 166 | `client.list_tubes(function(err, tubenames) {});` 167 | 168 | List all the existing tubes. Responds with an array of tube names. 169 | 170 | #### stats_job 171 | 172 | `client.stats_job(jobid, function(err, response) {});` 173 | 174 | Request statistics for the specified job. Responds with a hash containing information about the job. See the beanstalkd documentation for a complete list of stats. 175 | 176 | #### stats_tube 177 | 178 | `client.stats_tube(tubename, function(err, response) {});` 179 | 180 | Request statistics for the specified tube. Responds with a hash containing information about the tube. See the beanstalkd documentation for a complete list of stats. 181 | 182 | #### stats 183 | 184 | `client.stats(function(err, response) {});` 185 | 186 | Request statistics for the beanstalkd server. Responds with a hash containing information about the server. See the beanstalkd documentation for a complete list of stats. 187 | 188 | ## FiveBeansWorker 189 | 190 | Inspired by [node-beanstalk-worker](https://github.com/benlund/node-beanstalk-worker) but updated & rewritten to work with jobs queued by [Stalker](https://github.com/kr/stalker). 191 | 192 | The worker pulls jobs off the queue & passes them to matching handlers. It deletes successful jobs & buries unsuccessful ones. It continues processing past all recoverable errors, though it emits events on error. 193 | 194 | ### API 195 | 196 | #### constructor 197 | 198 | `new FiveBeansWorker(options)` 199 | 200 | Returns a new worker object. *options* is a hash containing the following keys: 201 | 202 | __id__: how this worker should identify itself in log events 203 | __host__: beanstalkd host 204 | __port__: beanstalkd port 205 | __handlers__: hash with handler objects, with handler types as keys 206 | __ignoreDefault__: true if this worker should ignore the default tube 207 | __timeout__: timeout parameter used with on reserve_with_timeout, defaults to 10 (in seconds) 208 | 209 | #### start 210 | 211 | `start(tubelist)` 212 | 213 | Connect the worker to the beanstalkd server & make it watch the specified tubes. Emits the 'started' event when it is complete. 214 | 215 | #### stop 216 | 217 | `stop()` 218 | 219 | Finish processing the current job then close the client. Emits the 'stopped' event when complete. 220 | 221 | #### watch 222 | 223 | `watch(tubelist, callback)` 224 | 225 | Begin watching the tubes named in the list. 226 | 227 | #### ignore 228 | 229 | `ignore(tubelist, callback)` 230 | 231 | Ignore the tubes named in the list. 232 | 233 | 234 | ### Events 235 | 236 | The worker is intended to continue processing jobs through most errors. Its response to exceptions encountered when processing jobs is to bury the job and emit an event that can be logged or handled somewhere else. 237 | 238 | `error`: Emitted on error in the underlying client. Payload is the error object. Execution is halted. You must listen for this event. 239 | 240 | `close`: Emitted on close in the underlying client. No payload. 241 | 242 | `started`: Worker has started processing. No payload. 243 | 244 | `stopped`: Worker has stopped processing. No payload. 245 | 246 | `info`: The worker has taken some action that you might want to log. The payload is an object with information about the action, with two fields: 247 | 248 | ```javascript 249 | { 250 | clientid: 'id-of-worker', 251 | message: 'a logging-style description of the action' 252 | } 253 | ``` 254 | 255 | This event is the tattered remnants of what used to be built-in logging, and it might go away. 256 | 257 | `warning`: The worker has encountered an error condition that will not stop processing, but that you might wish to act upon or log. The payload is an object with information about the error. Fields guaranteed to be present are: 258 | 259 | ```javascript 260 | { 261 | clientid: 'id-of-worker', 262 | message: 'the context of the error', 263 | error: errorObject 264 | } 265 | ``` 266 | 267 | Some errors might have additional fields providing context, such as a job id. 268 | 269 | `job.reserved`: The worker has reserved a job. The payload is the job id. 270 | 271 | `job.handled`: The worker has completed processing a job. The payload is an object with information about the job. 272 | 273 | ```javascript 274 | { 275 | id: job id, 276 | type: job type, 277 | elapsed: elapsed time in ms, 278 | action: [ 'success' | 'release' | 'bury' | custom error message ] 279 | } 280 | ``` 281 | 282 | `job.deleted`: The worker has deleted a job. The payload is the job id. 283 | 284 | `job.buried`: The worker has buried a job. The payload is the job id. 285 | 286 | ### Jobs 287 | 288 | Each job must be a JSON-serialized object with two fields: 289 | 290 | __type__: type string matching a handler 291 | __payload__: job data, in whatever format the job defines 292 | 293 | The worker looks up a handler using the given type string and calls work() on the job payload. 294 | 295 | The job *may* also be a JSON array containing two items: 296 | 297 | `[ tubename, jobdata ]` 298 | 299 | Where the second item is an object as specified above. This is for compatibility with the Stalker library, which wraps the job data this way. 300 | 301 | ### Handlers 302 | 303 | Handler modules must export a single function that returns an object. The object must have a field called 'type' with a brief descriptive string. It must also expose a function called work() with this signature: 304 | 305 | `work(jobdata, callback(action, delay))` 306 | 307 | __jobdata__: job payload 308 | __action__: 'success' | 'release' | 'bury' | custom error message 309 | __delay__: seconds to delay if the job is released; otherwise unused 310 | 311 | If the *action* is "success", the job is deleted. If it is "release", the job is released with the specified delay. If it is "bury", the job is buried. All other actions are treated as errors & the job is buried in response. 312 | 313 | Here's a simple handler example. 314 | 315 | ```javascript 316 | module.exports = function() 317 | { 318 | function EmitKeysHandler() 319 | { 320 | this.type = 'emitkeys'; 321 | } 322 | 323 | EmitKeysHandler.prototype.work = function(payload, callback) 324 | { 325 | var keys = Object.keys(payload); 326 | for (var i = 0; i < keys.length; i++) 327 | console.log(keys[i]); 328 | callback('success'); 329 | } 330 | 331 | var handler = new EmitKeysHandler(); 332 | return handler; 333 | }; 334 | ``` 335 | 336 | The [examples](examples) directory has another sample handler. 337 | 338 | ### Example 339 | 340 | This example starts a worker capable of handling the `emitkeys` example from above. 341 | 342 | ```javascript 343 | var Beanworker = require('fivebeans').worker; 344 | var options = 345 | { 346 | id: 'worker_4', 347 | host: '127.0.0.1', 348 | port: 11300, 349 | handlers: 350 | { 351 | emitkeys: require('./emitkeyshandler')() 352 | }, 353 | ignoreDefault: true 354 | } 355 | var worker = new Beanworker(options); 356 | worker.start(['high', 'medium', 'low']); 357 | ``` 358 | 359 | ## FiveBeansRunner 360 | 361 | A wrapper that runs a single beanstalkd worker as a daemon. Responds to the USR2 signal by reloading the configuration and restarting the worker. Handles SIGINT, SIGHUP, and SIGQUIT by completing processing on the current job then stopping. 362 | 363 | Example use: 364 | 365 | ```javascript 366 | var fivebeans = require('fivebeans'); 367 | var runner = new fivebeans.runner('worker_id_1', '/path/to/config.yml'); 368 | runner.go(); 369 | ``` 370 | 371 | ### bin/beanworker 372 | 373 | The above code plus [yargs](https://github.com/chevex/yargs) wrapped in a node shell script for your convenience. 374 | 375 | `bin/beanworker --id=[ID] --config=[config.yml]` 376 | 377 | Creates a runner for a worker with the specified ID & configured with the specified yaml file. 378 | 379 | Here's the complete source: 380 | 381 | ```javascript 382 | #!/usr/bin/env node 383 | 384 | var argv = require('yargs') 385 | .usage('Usage: beanworker --id=[ID] --config=[config.yml]') 386 | .default('id', 'defaultID') 387 | .demand(['config']) 388 | .argv; 389 | 390 | var FiveBeans = require('fivebeans'); 391 | 392 | var runner = new FiveBeans.runner(argv.id, argv.config); 393 | runner.go(); 394 | ``` 395 | 396 | ### Configuration file 397 | 398 | Here's an example yaml configuration: 399 | 400 | ```yaml 401 | beanstalkd: 402 | host: "127.0.0.1" 403 | port: 11300 404 | watch: 405 | - 'circle' 406 | - 'picadilly' 407 | - 'northern' 408 | - 'central' 409 | handlers: 410 | - "./handlers/holborn.js" 411 | - "./handlers/greenpark.js" 412 | - "./handlers/knightsbridge.js" 413 | ignoreDefault: true 414 | ``` 415 | 416 | __beanstalkd__: where to connect 417 | __watch__: a list of tubes to watch. 418 | __handlers__: a list of handler files to require 419 | __ignoreDefault__: true if this worker should ignore the default tube 420 | 421 | If the handler paths don't start with `/` the current working directory will be prepended to them before they are required. 422 | 423 | Why yaml not json? Because when I originally wrote this, it was in support of a ruby service, and yaml is the native config format over in that land. I continue using it because it's more readable than json and easier for humans to type. 424 | 425 | ## Contributors 426 | 427 | @AVVS 428 | @crackcomm 429 | @zr40 430 | Jon Keating 431 | Jevgenij Tsoi 432 | 433 | Many thanks! 434 | 435 | ## TODO 436 | 437 | * Handle DEADLINE_SOON from the server. 438 | --------------------------------------------------------------------------------