├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── index.js ├── lib ├── broker-create.js ├── broker-events.js ├── broker-subscribe.js └── publish.js ├── package.json └── test ├── .eslintrc ├── broker-create-failure.test.js ├── broker-publish-failure.test.js ├── broker-subscribe-failure.test.js ├── init.test.js ├── mock ├── broker-create.js └── rascal.js ├── publish.test.js ├── shutdown.test.js ├── subscribe.test.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "rules": { 4 | "strict": [2, "global"], 5 | "no-use-before-define": [2, "nofunc"] 6 | }, 7 | "ecmaFeatures": { 8 | "modules": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2" 4 | after_script: 5 | - npm run coveralls 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 Trainline.com Ltd 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 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trainline/rabbitmq-warren-node/5ec041009fb258290512afea611978ba4e92770b/NOTICE.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # warren 2 | 3 | [![build](https://travis-ci.org/trainline/rabbitmq-warren-node.svg?branch=master)](https://travis-ci.org/trainline/rabbitmq-warren-node) 4 | [![coverage](https://coveralls.io/repos/trainline/rabbitmq-warren-node/badge.svg?branch=master&service=github)](https://coveralls.io/github/trainline/rabbitmq-warren-node?branch=master) 5 | [![dependencies](https://img.shields.io/david/trainline/rabbitmq-warren-node.svg)](https://david-dm.org/trainline/rabbitmq-warren-node) 6 | ![node](https://img.shields.io/node/v/warren.svg) 7 | [![npm](https://img.shields.io/npm/v/warren.svg)](https://www.npmjs.com/package/warren) 8 | [![licence](https://img.shields.io/npm/l/warren.svg)](LICENSE.txt) 9 | 10 | ## A warren is a shared-nothing RabbitMQ cluster 11 | 12 | The original warren pattern uses a load balancer stuck in front of the Rabbit instances (active-passive). This module implements a variation of that pattern but does away with the load balancer and is **active-active**. 13 | 14 | ## Single publish - multiple listen 15 | 16 | A publisher can publish to any instance and a consumer listens to all instances. This approach improves availability of a broker to do work against, it's effectively active-active but does not replicate messages as found with the Shovel/Federation/Mirrored queue features. 17 | 18 | ## Caveats 19 | 20 | If you use non-persistent messages with this pattern, message loss is still possible - in that case you're better off with publishing to multiple brokers at the same time (unsupported). When using persistent messages, catastrophic broker failure will still cause message loss. Use where appropriate. 21 | 22 | 23 | ## tl;dr 24 | 25 | This module: 26 | * takes care of connecting to multiple brokers (and retries connecting on failures) 27 | * subscribes to messages on all brokers (and recovers from errors, subscribes on newly connected brokers) 28 | * publishes messages to a single broker (and tries other brokers if failed) 29 | 30 | ## Installation 31 | 32 | As usual, with npm: 33 | 34 | ```bash 35 | $ npm install --save warren 36 | ``` 37 | 38 | ## Usage 39 | 40 | warren uses [rascal configuration](https://github.com/guidesmiths/rascal#configuration) to define exchanges, queues, etc. It expects a `brokerConfig` with a single default vhost named `/`. Different host connections are specified under `hosts`. 41 | 42 | ```js 43 | const createWarren = require('warren'); 44 | 45 | const options = { 46 | hosts: [ 47 | // multiple rascal vhost connection configs 48 | ], 49 | brokerConfig: { 50 | // rascal config (same for all hosts) 51 | } 52 | } 53 | 54 | createWarren(options, (err, warren) => { 55 | warren.subscribe('messages', (message, content) => { 56 | // message received 57 | }); 58 | warren.publish('messages', message, { timeout: 100 }, err => { 59 | // message published 60 | }); 61 | }).on('error', error => { 62 | // handle errors 63 | }); 64 | ``` 65 | 66 | 67 | ## API 68 | 69 | ### createWarren(brokerConfigs, [options,] callback) 70 | 71 | Creates a warren by creating rascal brokers from the passed in configurations. You can specify some global options (timeouts, retries) or e.g. how many brokers to wait to connect to initially. 72 | 73 | **Arguments** 74 | * **options**: configuration Object with properties below 75 | * **hosts**: array of rascal connection objects 76 | * **brokerConfig**: rascal config (exchange, queue definitions, etc) to be used for all broker hosts 77 | * **minBrokersAvailable**: the minimum number of brokers to connect to before calling back (default: 1) 78 | * **timeout**: timeout in milliseconds for broker creation and publish operations (default: 5000) 79 | * **tries**: number of publish retries (default: number of broker configs) 80 | * **callback(err, warren)**: called once warren is ready or an error occurs 81 | 82 | **Example** 83 | 84 | ```js 85 | createWarren(options, (err, warren) => { 86 | // ... 87 | }); 88 | ``` 89 | 90 | ### warren.publish(publication, message, options, callback(err)) 91 | 92 | Publishes a message by trying available brokers until it succeeds (or number of tries exhausted or it times out). 93 | 94 | **Arguments** 95 | 96 | * **publication**: publication name to use (from your rascal config) 97 | * **message**: message body 98 | * **options**: configuration Object overriding global warren and rascal publish configuration 99 | * **timeout**: overall timeout in milliseconds for publish completion (default: 5000) 100 | * **tries**: number of publish retries (default: number of broker configs) 101 | * *****: rascal specific overrides 102 | * **callback(err)**: called once publish succeeded or an error occurs 103 | 104 | **Example** 105 | 106 | ```js 107 | warren.publish(publication, message, options, err => { 108 | // ... 109 | }); 110 | ``` 111 | 112 | ### warren.subscribe(subscription, onMessage) 113 | 114 | **Arguments** 115 | 116 | * **subscription**: subscription name to use (from your rascal config) 117 | * **message**: message body 118 | * **onMessage(message, content, ackOrNack)**: called when a message is received, follows [rascal conventions](https://github.com/guidesmiths/rascal#subscriptions) 119 | * **message**: raw message object 120 | * **content**: parsed message content 121 | * **ackOrNack**: acknowledgement callback for not auto-acknowledged subscriptions 122 | 123 | **Example** 124 | 125 | ```js 126 | warren.subscribe(subscription, (message, content, ackOrNack) => { 127 | // ... 128 | }); 129 | ``` 130 | 131 | ## Acknowledgements 132 | 133 | * Thanks to [Pedro Teixeira](https://github.com/pgte), [Hassy Veldstra](https://github.com/hassy) and [João Jerónimo](https://github.com/joaojeronimo) for building the first versions of warren 134 | * Thanks to [Frederik Brysse](https://github.com/frederik256) who designed the high level architecture 135 | * This project is built on the amazing [rascal](https://github.com/guidesmiths/rascal) and [amqplib](https://github.com/squaremo/amqp.node) modules. Thanks to [Stephen Creswell](https://github.com/cressie176) and [Michael Bridgen](https://github.com/squaremo)! 136 | 137 | 138 | ## License 139 | 140 | Copyright 2015 Trainline.com Ltd 141 | 142 | Licensed under the Apache License, Version 2.0 (the "License"); 143 | you may not use this file except in compliance with the License. 144 | You may obtain a copy of the License at 145 | 146 | http://www.apache.org/licenses/LICENSE-2.0 147 | 148 | Unless required by applicable law or agreed to in writing, software 149 | distributed under the License is distributed on an "AS IS" BASIS, 150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 151 | See the License for the specific language governing permissions and 152 | limitations under the License. 153 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const util = require('util'); 6 | const async = require('async'); 7 | const debug = require('debug')('warren:init'); 8 | const timers = require('timers'); 9 | const defaults = require('lodash.defaults'); 10 | const once = require('lodash.once'); 11 | const cloneDeep = require('lodash.clonedeep'); 12 | const map = require('lodash.map'); 13 | const EventEmitter = require('events'); 14 | const rascal = require('rascal'); 15 | 16 | const createBroker = require('./lib/broker-create'); 17 | const subscribeBroker = require('./lib/broker-subscribe'); 18 | const listenToBrokerEvents = require('./lib/broker-events'); 19 | const publish = require('./lib/publish'); 20 | 21 | const defaultOptions = { 22 | timeout: 5000, 23 | minBrokersAvailable: 1, 24 | }; 25 | 26 | const hostsToBrokerConfig = (config, host) => { 27 | const amqpConfig = cloneDeep(config); 28 | amqpConfig.vhosts['/'].connection = host; 29 | return rascal.withDefaultConfig(amqpConfig); 30 | }; 31 | 32 | module.exports = function createWarren(opts, _callback) { 33 | const callback = once(_callback); 34 | const options = defaults({}, opts, defaultOptions); 35 | const brokerConfigs = map(options.hosts, hostsToBrokerConfig.bind(null, options.brokerConfig)); 36 | 37 | if (!brokerConfigs || brokerConfigs.length < options.minBrokersAvailable) { 38 | return callback(new Error('not enough hosts')); 39 | } 40 | if (!options.tries) { 41 | options.tries = brokerConfigs.length; 42 | } 43 | debug('global options:', options); 44 | 45 | const subscriptions = {}; 46 | const availableBrokers = []; 47 | const brokers = []; 48 | 49 | const warren = new EventEmitter(); 50 | warren.publish = publish.bind(null, availableBrokers, options); 51 | warren.subscribe = subscribe; 52 | warren.brokers = availableBrokers; 53 | warren.shutdown = (cb) => { 54 | async.each(brokers, (broker, done) => broker.shutdown(done), cb); 55 | }; 56 | 57 | debug('creating %d brokers', brokerConfigs.length); 58 | warren.on('created', onBrokerCreated); 59 | let timeout; 60 | if (options.minBrokersAvailable > 0) { 61 | timeout = timers.setTimeout(callback, options.timeout, timeoutError(options)); 62 | timeout.unref(); 63 | } else { 64 | callback(null, warren); 65 | } 66 | brokerConfigs.forEach(config => timers.setImmediate(createBroker, warren, config)); 67 | return warren; 68 | 69 | // Public methods 70 | 71 | function subscribe(channel, onMessage) { 72 | subscriptions[channel] = onMessage; 73 | debug('subscribing to channel %s', channel); 74 | availableBrokers.forEach(broker => subscribeBroker(warren, broker, channel, onMessage)); 75 | } 76 | 77 | // Private methods 78 | 79 | function timeoutError() { 80 | return new Error(util.format('Timed out while connecting to %d brokers in %dms', 81 | options.minBrokersAvailable, 82 | options.timeout)); 83 | } 84 | 85 | function onBrokerCreated(broker) { 86 | brokers.push(broker); 87 | listenToBrokerEvents(broker, availableBrokers, onError); 88 | 89 | Object.keys(subscriptions).forEach(channel => { 90 | const onMessage = subscriptions[channel]; 91 | subscribeBroker(warren, broker, channel, onMessage); 92 | }); 93 | 94 | broker.emit('connect'); 95 | 96 | const minBrokersAvailable = options.minBrokersAvailable; 97 | if (minBrokersAvailable > 0 && availableBrokers.length === minBrokersAvailable) { 98 | timers.clearTimeout(timeout); 99 | callback(null, warren); 100 | } 101 | } 102 | 103 | function onError(err) { 104 | if (err) { 105 | warren.emit('error', err); 106 | } 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /lib/broker-create.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const debug = require('debug')('warren:createBroker'); 6 | const rascal = require('rascal'); 7 | const Backoff = require('backoff'); 8 | 9 | module.exports = function createBroker(warren, brokerConfig, cb, noRecover) { 10 | let callback = cb; 11 | if (!callback) { 12 | callback = onError; 13 | } 14 | rascal.createBroker(brokerConfig, (err, broker) => { 15 | if (!err) { 16 | debug('successfully created broker'); 17 | warren.emit('created', broker); 18 | } else { 19 | debug('error creating broker', err); 20 | recover(); 21 | onError(err); 22 | } 23 | callback(err); 24 | }); 25 | 26 | function recover() { 27 | if (noRecover) return; 28 | debug('recovering broker creation'); 29 | const backoff = Backoff.exponential(); 30 | backoff.on('ready', () => { 31 | debug('backoff ready, trying to recreate broker'); 32 | createBroker(warren, brokerConfig, onceBrokerCreated, true); 33 | }); 34 | 35 | backoff.backoff(); 36 | 37 | function onceBrokerCreated(err) { 38 | if (err) { 39 | debug('broker recreation failed, backing off'); 40 | backoff.backoff(); 41 | } else { 42 | debug('broker recreation succeeded'); 43 | } 44 | } 45 | } 46 | 47 | function onError(err) { 48 | if (err) { 49 | warren.emit('error', err); 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /lib/broker-events.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const debug = require('debug')('warren:broker-events'); 6 | 7 | module.exports = function listenToBrokerEvents(broker, availableBrokers, onError) { 8 | broker.on('error', onError); 9 | 10 | ['blocked', 'disconnect'].forEach(event => { 11 | broker.on(event, () => { 12 | debug('broker %s', event); 13 | const brokerIndex = availableBrokers.indexOf(broker); 14 | if (brokerIndex >= 0) { 15 | availableBrokers.splice(brokerIndex, 1); 16 | } 17 | }); 18 | }); 19 | 20 | broker.on('blocked', () => broker.bounce(onError)); 21 | 22 | ['unblocked', 'connect'].forEach(event => { 23 | broker.on(event, () => { 24 | debug('broker %s', event); 25 | const brokerIndex = availableBrokers.indexOf(broker); 26 | if (brokerIndex === -1) { 27 | availableBrokers.push(broker); 28 | } 29 | }); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/broker-subscribe.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const debug = require('debug')('warren:subscribe'); 6 | const Backoff = require('backoff'); 7 | 8 | module.exports = function subscribeBroker(warren, broker, channel, onMessage, cb, noRecover) { 9 | let callback = cb; 10 | if (!callback) { 11 | callback = onError; 12 | } 13 | broker.subscribe(channel, (err, subscription) => { 14 | if (!err) { 15 | debug('successfully subscribed to channel on one broker'); 16 | subscription.on('message', onMessage); 17 | subscription.on('error', onError); 18 | warren.emit('subscribed', channel, broker); 19 | } else { 20 | debug('error subscribing to channel on one broker', channel); 21 | warren.emit('error', err); 22 | recover(); 23 | } 24 | callback(err); 25 | }); 26 | 27 | function recover() { 28 | if (noRecover) return; 29 | debug('recovering subscription to %s', channel); 30 | const backoff = Backoff.exponential(); 31 | backoff.on('ready', () => { 32 | debug('backoff ready, trying to resubscribe to channel', channel); 33 | subscribeBroker(warren, broker, channel, onMessage, onceBrokerResubscribed, true); 34 | }); 35 | 36 | backoff.backoff(); 37 | 38 | function onceBrokerResubscribed(err) { 39 | if (err) { 40 | debug('resubscribe failed, backing off'); 41 | backoff.backoff(); 42 | } else { 43 | debug('resubscribe succeeded'); 44 | } 45 | } 46 | } 47 | 48 | function onError(err) { 49 | if (err) { 50 | warren.emit('error', err); 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /lib/publish.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const timers = require('timers'); 6 | 7 | const defaults = require('lodash.defaults'); 8 | const shuffle = require('lodash.shuffle'); 9 | const once = require('lodash.once'); 10 | const debug = require('debug')('warren:publish'); 11 | 12 | function publish(exchange, message, options, cb) { 13 | let timedout = false; 14 | debug('publish', exchange, message, options); 15 | 16 | function onTimeout() { 17 | debug('timed out'); 18 | timedout = true; 19 | cb(new Error('publish timed out after ' + options.timeout + ' ms')); 20 | } 21 | 22 | options.tries --; 23 | if (options.tries < 0) { 24 | debug('max tries exceeded, giving up'); 25 | return cb(options.lastError); 26 | } 27 | 28 | const broker = options.brokers[options.brokerIndex]; 29 | options.brokerIndex = ++options.brokerIndex % options.brokers.length; 30 | if (! broker) { 31 | return cb(new Error('no broker')); 32 | } 33 | 34 | const timeout = timers.setTimeout(onTimeout, options.expiresAt - Date.now()); 35 | timeout.unref(); 36 | 37 | broker.publish(exchange, message, options, onPublication); 38 | 39 | function onPublication(err, publication) { 40 | debug('onPublication'); 41 | if (err) { 42 | onError(err); 43 | } else { 44 | publication 45 | .once('error', onError) 46 | .once('return', onReturn) 47 | .once('success', onSuccess); 48 | } 49 | } 50 | 51 | function retry() { 52 | debug('retry'); 53 | if (timedout) { 54 | cb(options.lastError); 55 | } else { 56 | timers.clearTimeout(timeout); 57 | publish(exchange, message, options, cb); 58 | } 59 | } 60 | 61 | function onSuccess() { 62 | debug('onSuccess'); 63 | timers.clearTimeout(timeout); 64 | cb(); 65 | } 66 | 67 | function onError(err) { 68 | debug('onError', err); 69 | options.lastError = err; 70 | retry(); 71 | } 72 | 73 | function onReturn() { 74 | debug('onReturn'); 75 | onError(new Error('message was returned')); 76 | } 77 | } 78 | 79 | module.exports = (brokers, globalOptions, exchange, message, opts, _cb) => { 80 | debug('publish', exchange, message, opts); 81 | const cb = once(_cb); 82 | const options = defaults({ 83 | brokers: shuffle(brokers), 84 | brokerIndex: 0, 85 | }, opts, globalOptions); 86 | options.expiresAt = Date.now() + options.timeout; 87 | publish(exchange, message, options, cb); 88 | }; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "warren", 3 | "version": "1.1.0", 4 | "description": "A fault-tolerant client for a RabbitMQ warren (multiple shared-nothing instances)", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "test": "istanbul test -- _mocha", 9 | "coverage": "npm test --coverage && istanbul check-coverage --statement 100 --branch 100 --function 100", 10 | "precommit": "npm run lint && npm run coverage", 11 | "coveralls": "npm run coverage && istanbul-coveralls" 12 | }, 13 | "contributors": [ 14 | { 15 | "name": "Csaba Palfi", 16 | "url": "https://github.com/csabapalfi" 17 | }, 18 | { 19 | "name": "Pedro Teixeira", 20 | "url": "https://github.com/pgte" 21 | }, 22 | { 23 | "name": "Frederik Brysse", 24 | "url": "https://github.com/frederik256" 25 | }, 26 | { 27 | "name": "Hassy Veldstra", 28 | "url": "https://github.com/hassy" 29 | }, 30 | { 31 | "name": "João Jerónimo", 32 | "url": "https://github.com/joaojeronimo" 33 | } 34 | ], 35 | "keywords": [ 36 | "rabbitmq", 37 | "warren", 38 | "rascal", 39 | "amqplib" 40 | ], 41 | "author": "trainline", 42 | "license": "Apache-2.0", 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/trainline/rabbitmq-warren-node" 46 | }, 47 | "dependencies": { 48 | "async": "1.5.2", 49 | "backoff": "2.4.1", 50 | "debug": "2.2.0", 51 | "lodash.clonedeep": "4.0.3", 52 | "lodash.defaults": "4.0.1", 53 | "lodash.map": "4.2.1", 54 | "lodash.once": "4.0.0", 55 | "lodash.shuffle": "4.0.0", 56 | "rascal": "0.10.2" 57 | }, 58 | "devDependencies": { 59 | "code": "2.1.0", 60 | "eslint": "1.9.0", 61 | "eslint-config-airbnb": "1.0.0", 62 | "husky": "0.11.3", 63 | "istanbul": "0.4.2", 64 | "istanbul-coveralls": "1.0.3", 65 | "lodash.clone": "4.3.1", 66 | "lodash.foreach": "4.1.0", 67 | "lodash.get": "4.1.2", 68 | "lodash.isequal": "4.1.1", 69 | "lodash.merge": "4.3.2", 70 | "lodash.omit": "4.1.0", 71 | "lodash.times": "4.0.2", 72 | "mocha": "2.4.5", 73 | "proxyquire": "1.7.4" 74 | }, 75 | "engines": { 76 | "node": ">=4.2.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/broker-create-failure.test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const Code = require('code'); 6 | const expect = Code.expect; 7 | const EventEmitter = require('events'); 8 | const proxyquire = require('proxyquire'); 9 | 10 | const createBroker = proxyquire('../lib/broker-create', { rascal: require('./mock/rascal') }); 11 | const warren = new EventEmitter(); 12 | const createBrokerOptions = require('./utils').createBrokerOptions; 13 | 14 | warren.on('error', () => {}); 15 | 16 | describe('warren broker creation (failure tests)', () => { 17 | it('calls back with error', done => { 18 | createBroker(warren, createBrokerOptions({ failOnCreate: true }), err => { 19 | expect(err).to.equal('some error'); 20 | done(); 21 | }); 22 | }); 23 | 24 | it('calls back with error when norecover', done => { 25 | createBroker(warren, createBrokerOptions({ failOnCreate: true }), err => { 26 | expect(err).to.equal('some error'); 27 | done(); 28 | }, true); 29 | }); 30 | 31 | it('calls back with broker if recovered', done => { 32 | warren.on('created', broker => { 33 | expect(broker).to.exist(); 34 | done(); 35 | }); 36 | createBroker(warren, createBrokerOptions({ failOnCreateOnce: true }), err => { 37 | expect(err).to.equal('some error'); 38 | }); 39 | }); 40 | 41 | it('waits a bit', done => { 42 | setTimeout(done, 1000); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/broker-publish-failure.test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const Code = require('code'); 6 | const expect = Code.expect; 7 | const proxyquire = require('proxyquire'); 8 | const clone = require('lodash.clone'); 9 | 10 | const createBroker = require('./mock/broker-create'); 11 | const createWarren = proxyquire('../', { 'broker-create': createBroker }); 12 | const warrenOptions = require('./utils').warrenOptions; 13 | 14 | const options = {}; 15 | 16 | describe('warren publish (failure tests)', () => { 17 | it('handles disconnected brokers', done => { 18 | const warrenOpts = warrenOptions({count: 2}); 19 | createWarren(warrenOpts, (err, warren) => { 20 | const brokers = clone(warren.brokers); 21 | brokers.forEach(broker => { 22 | broker.emit('disconnect'); 23 | broker.emit('disconnect'); 24 | }); 25 | warren.publish('channel', 'message', options, error => { 26 | expect(error).to.be.object(); 27 | expect(error.message).to.equal('no broker'); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | 33 | it('handles blocked brokers', done => { 34 | const warrenOpts = warrenOptions({count: 2}); 35 | createWarren(warrenOpts, (err, warren) => { 36 | const brokers = clone(warren.brokers); 37 | brokers.forEach(broker => { 38 | broker.emit('blocked'); 39 | }); 40 | warren.publish('channel', 'message', options, error => { 41 | expect(error).to.be.object(); 42 | expect(error.message).to.equal('no broker'); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | it('handles broker reconnects', done => { 49 | const warrenOpts = warrenOptions({count: 2}); 50 | createWarren(warrenOpts, (error, warren) => { 51 | const brokers = clone(warren.brokers); 52 | brokers.forEach(broker => { 53 | broker.emit('disconnect'); 54 | }); 55 | brokers.forEach(broker => { 56 | broker.emit('connect'); 57 | broker.emit('connect'); 58 | }); 59 | warren.publish('channel', 'message', options, err => { 60 | expect(err).to.be.undefined(); 61 | expect(brokers.filter(broker => { 62 | return broker.sentOnlyOne('channel', 'message'); 63 | }).length).to.equal(1); 64 | done(); 65 | }); 66 | }); 67 | }); 68 | 69 | it('handles balancer unblock', done => { 70 | const warrenOpts = warrenOptions({count: 2}); 71 | createWarren(warrenOpts, (err, warren) => { 72 | const brokers = clone(warren.brokers); 73 | brokers.forEach(broker => { 74 | broker.emit('disconnect'); 75 | }); 76 | brokers.forEach(broker => { 77 | broker.emit('unblocked'); 78 | }); 79 | warren.publish('channel', 'message', options, error => { 80 | expect(error).to.be.undefined(); 81 | expect(brokers.filter(broker => { 82 | return broker.sentOnlyOne('channel', 'message'); 83 | }).length).to.equal(1); 84 | done(); 85 | }); 86 | }); 87 | }); 88 | 89 | it('emits error if broker emits error', done => { 90 | const warrenOpts = warrenOptions({count: 2}); 91 | createWarren(warrenOpts, (err, warren) => { 92 | warren.once('error', error => { 93 | expect(error).to.be.object(); 94 | expect(error.message).to.equal('oops'); 95 | done(); 96 | }); 97 | warren.brokers[0].emit('error', new Error('oops')); 98 | }); 99 | }); 100 | 101 | it('waits a bit', done => { 102 | setTimeout(done, 1000); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/broker-subscribe-failure.test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const Code = require('code'); 6 | const expect = Code.expect; 7 | const proxyquire = require('proxyquire'); 8 | 9 | const createBroker = require('./mock/broker-create'); 10 | const createWarren = proxyquire('../', { 'broker-create': createBroker }); 11 | const warrenOptions = require('./utils').warrenOptions; 12 | 13 | describe('warren subscribe (failure tests)', () => { 14 | it('recovers from failed subscription', done => { 15 | const warrenOpts = warrenOptions({failOnSubscribe: true, count: 1}); 16 | createWarren(warrenOpts, (err, warren) => { 17 | warren.on('error', () => {}); 18 | 19 | const messages = ['message 1', 'message 2', 'message 3']; 20 | 21 | expect(warren.brokers[0].failureCount).to.equal(0); 22 | warren.subscribe('channel', message => { 23 | expect(message).to.equal(messages.shift()); 24 | if (! messages.length) { 25 | done(); 26 | } 27 | }); 28 | expect(warren.brokers[0].failureCount).to.equal(1); 29 | warren.brokers[0].changeGlobalOption('failOnSubscribe', false); 30 | 31 | warren.once('subscribed', () => { 32 | messages.forEach(message => warren.brokers[0].message('channel', message)); 33 | }); 34 | }); 35 | }); 36 | 37 | it('waits a bit', done => { 38 | setTimeout(done, 1000); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/init.test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const Code = require('code'); 6 | const expect = Code.expect; 7 | const proxyquire = require('proxyquire'); 8 | 9 | const createBroker = require('./mock/broker-create'); 10 | const createWarren = proxyquire('../', { 'broker-create': createBroker }); 11 | const warrenOptions = require('./utils').warrenOptions; 12 | 13 | describe('warren init', () => { 14 | it('errors if you have less hosts than minBrokersAvailable', done => { 15 | createWarren(warrenOptions({hosts: [{count: 2}], minBrokersAvailable: 4}), err => { 16 | expect(err.message).to.exist(); 17 | expect(err.message).to.equal('not enough hosts'); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('doesn\'t wait for broker creation if asked not to', done => { 23 | createWarren(warrenOptions({ hosts: [{count: 4}], minBrokersAvailable: 0 }), (err, warren) => { 24 | expect(err).to.be.null(); 25 | expect(warren).to.exist(); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('errors if can\`t connect to required number of brokers before timeout', done => { 31 | createWarren(warrenOptions({ hosts: [{ failOnCreate: true }], minBrokersAvailable: 1, timeout: 100, tries: 1 }), err => { 32 | expect(err).to.exist(); 33 | expect(err.message).to.equal('Timed out while connecting to 1 brokers in 100ms'); 34 | done(); 35 | }).on('error', () => {}); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/mock/broker-create.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const proxyquire = require('proxyquire'); 6 | 7 | module.exports = proxyquire('../../lib/broker-create', { rascal: require('./rascal') }); 8 | -------------------------------------------------------------------------------- /test/mock/rascal.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const timers = require('timers'); 6 | const isEqual = require('lodash.isequal'); 7 | const EventEmitter = require('events'); 8 | const defaults = require('lodash.defaults'); 9 | 10 | exports.createBroker = createBroker; 11 | 12 | let failOnCreateOnce = true; 13 | 14 | function createBroker(opts, callback) { 15 | const globalOpts = opts.vhosts['/'].connection; 16 | const broker = new EventEmitter(); 17 | const globalOptions = defaults({}, globalOpts); 18 | const channels = {}; 19 | const sentMessages = []; 20 | broker.publish = publish; 21 | broker.subscribe = subscribe; 22 | broker.bounce = bounce; 23 | broker.shutdown = shutdown; 24 | 25 | broker.message = brokerMessage; 26 | broker.sentCount = sentCount; 27 | broker.sentOnlyOne = sentOnlyOne; 28 | broker.changeGlobalOption = changeGlobalOption; 29 | broker.failureCount = 0; 30 | broker.down = false; 31 | 32 | if (globalOptions.failOnCreate) { 33 | return callback('some error'); 34 | } 35 | 36 | if (globalOptions.failOnCreateOnce && failOnCreateOnce) { 37 | failOnCreateOnce = false; 38 | return callback('some error'); 39 | } 40 | failOnCreateOnce = true; 41 | callback(null, broker); 42 | 43 | 44 | function publish(channel, message, options, cb) { 45 | if (globalOptions.failOnPublish) { 46 | return fail('some error', cb); 47 | } 48 | timers.setTimeout(() => { 49 | const publication = new EventEmitter(); 50 | timers.setTimeout(() => { 51 | if (globalOptions.failOnPublishing) { 52 | broker.failureCount ++; 53 | publication.emit('error', new Error('publication failed')); 54 | } else if (globalOptions.returnMessage) { 55 | broker.failureCount ++; 56 | publication.emit('return', message); 57 | } else { 58 | sentMessages.push([channel, message]); 59 | publication.emit('success'); 60 | } 61 | }, globalOptions.lag || 0); 62 | cb(null, publication); 63 | }, globalOptions.publicationLag || 0); 64 | } 65 | 66 | function subscribe(channel, cb) { 67 | if (! channels[channel]) { 68 | channels[channel] = []; 69 | } 70 | if (globalOptions.failOnSubscribe) { 71 | broker.failureCount ++; 72 | timers.setImmediate(cb, new Error('subscribe failed')); 73 | } else { 74 | const subscription = new EventEmitter(); 75 | channels[channel].push(subscription); 76 | timers.setImmediate(cb, null, subscription); 77 | 78 | if (globalOptions.failSubscriptionAfter) { 79 | timers.setTimeout(() => { 80 | const idx = channels[channel].indexOf(subscription); 81 | if (idx >= 0) { 82 | channels[channel].splice(idx, 1); 83 | } 84 | subscription.emit('error', new Error('subscription error')); 85 | }, globalOptions.failSubscriptionAfter); 86 | } 87 | } 88 | } 89 | 90 | function bounce(cb) { 91 | timers.setImmediate(() => { 92 | broker.emit('disconnect'); 93 | timers.setImmediate(() => { 94 | broker.emit('connect'); 95 | timers.setImmediate(cb); 96 | }); 97 | }); 98 | } 99 | 100 | function brokerMessage(channel, message) { 101 | const subscriptions = channels[channel]; 102 | if (subscriptions) { 103 | subscriptions.forEach((subscription) => { 104 | timers.setImmediate(() => { 105 | subscription.emit('message', message); 106 | }); 107 | }); 108 | } 109 | } 110 | 111 | function sentCount(channel, message) { 112 | return sentMessages.filter(match).length; 113 | 114 | function match(sentMessage) { 115 | return isEqual(sentMessage, [channel, message]); 116 | } 117 | } 118 | 119 | function sentOnlyOne(channel, message) { 120 | return sentCount(channel, message) === 1; 121 | } 122 | 123 | function changeGlobalOption(key, val) { 124 | globalOptions[key] = val; 125 | } 126 | 127 | function fail(err, cb) { 128 | broker.failureCount ++; 129 | cb(new Error(err)); 130 | } 131 | 132 | function shutdown(cb) { 133 | broker.down = true; 134 | cb(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/publish.test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const Code = require('code'); 6 | const expect = Code.expect; 7 | const defaults = require('lodash.defaults'); 8 | const proxyquire = require('proxyquire'); 9 | 10 | const createBroker = require('./mock/broker-create'); 11 | const createWarren = proxyquire('../', { 'broker-create': createBroker }); 12 | const warrenOptions = require('./utils').warrenOptions; 13 | 14 | const publishOptions = {}; 15 | 16 | describe('warren publish', () => { 17 | it('works if no broker errors', done => { 18 | createWarren(warrenOptions(), (error, warren) => { 19 | warren.publish('channel', 'message', publishOptions, err => { 20 | expect(err).to.be.undefined(); 21 | expect(warren.brokers.filter(broker => { 22 | return broker.sentOnlyOne('channel', 'message'); 23 | }).length).to.equal(1); 24 | done(); 25 | }); 26 | }); 27 | }); 28 | 29 | it('fails if every broker publish fails', done => { 30 | createWarren(warrenOptions({failOnPublish: true}), (error, warren) => { 31 | warren.publish('channel', 'message', publishOptions, err => { 32 | expect(err).to.be.object(); 33 | expect(err.message).to.equal('some error'); 34 | expect(warren.brokers.every(broker => { 35 | return broker.failureCount === 1; 36 | })).to.equal(true); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | 42 | it('succeeds if one broker publish succeeds', done => { 43 | const options = warrenOptions({hosts: [{ failOnPublish: true, count: 3 }, { count: 1 }]}); 44 | createWarren(options, (error, warren) => { 45 | warren.publish('channel', 'message', publishOptions, err => { 46 | expect(err).to.be.undefined(); 47 | expect(warren.brokers.filter(broker => { 48 | return broker.sentOnlyOne('channel', 'message'); 49 | }).length).to.equal(1); 50 | done(); 51 | }); 52 | }); 53 | }); 54 | 55 | it('fails if every broker publishing fails', done => { 56 | createWarren(warrenOptions({failOnPublishing: true}), (error, warren) => { 57 | warren.publish('channel', 'message', publishOptions, err => { 58 | expect(err).to.be.object(); 59 | expect(err.message).to.equal('publication failed'); 60 | expect(warren.brokers.every(broker => { 61 | return broker.failureCount === 1; 62 | })).to.equal(true); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | 68 | it('fails if every broker returns message', done => { 69 | createWarren(warrenOptions({returnMessage: true}), (error, warren) => { 70 | warren.publish('channel', 'message', publishOptions, err => { 71 | expect(err).to.be.object(); 72 | expect(err.message).to.equal('message was returned'); 73 | expect(warren.brokers.every(broker => { 74 | return broker.failureCount === 1; 75 | })).to.equal(true); 76 | 77 | done(); 78 | }); 79 | }); 80 | }); 81 | 82 | it('fails if one broker and exceeds timeout', done => { 83 | const opts = defaults({ timeout: 100, tries: 2 }, publishOptions); 84 | createWarren(warrenOptions({lag: 200, count: 1}), (error, warren) => { 85 | warren.publish('channel', 'message', opts, err => { 86 | expect(err).to.be.object(); 87 | expect(err.message).to.equal('publish timed out after 100 ms'); 88 | done(); 89 | }); 90 | }); 91 | }); 92 | 93 | it('fails if exceeds timeout', done => { 94 | const opts = defaults({ timeout: 100 }, publishOptions); 95 | createWarren(warrenOptions({lag: 200}), (error, warren) => { 96 | warren.publish('channel', 'message', opts, err => { 97 | expect(err).to.be.object(); 98 | expect(err.message).to.equal('publish timed out after 100 ms'); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | 104 | it('fails if exceeds overall timeout', done => { 105 | const opts = defaults({ timeout: 300, tries: 100 }, publishOptions); 106 | createWarren(warrenOptions({lag: 100, failOnPublishing: true}), (error, warren) => { 107 | warren.publish('channel', 'message', opts, err => { 108 | expect(err).to.be.object(); 109 | expect(err.message).to.equal('publish timed out after 300 ms'); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | 115 | it('fails if exceeds overall timeout 2', done => { 116 | const opts = defaults({ timeout: 300, tries: 100 }, publishOptions); 117 | createWarren(warrenOptions({publicationLag: 100, failOnPublishing: true}), (error, warren) => { 118 | warren.publish('channel', 'message', opts, err => { 119 | expect(err).to.be.object(); 120 | expect(err.message).to.equal('publish timed out after 300 ms'); 121 | done(); 122 | }); 123 | }); 124 | }); 125 | 126 | it('succeeds if does not exceed timeout', done => { 127 | const opts = defaults({ timeout: 300 }, publishOptions); 128 | createWarren(warrenOptions({lag: 100, count: 2}), (error, warren) => { 129 | warren.publish('channel', 'message', opts, err => { 130 | expect(err).to.be.undefined(); 131 | expect(warren.brokers.filter(broker => { 132 | return broker.sentOnlyOne('channel', 'message'); 133 | }).length).to.equal(1); 134 | done(); 135 | }); 136 | }); 137 | }); 138 | 139 | it('fails but doesnt retry if tries is 1', done => { 140 | const opts = defaults({ tries: 1 }, publishOptions); 141 | createWarren(warrenOptions({failOnPublish: true}), (error, warren) => { 142 | warren.publish('channel', 'message', opts, err => { 143 | expect(err).to.be.object(); 144 | expect(err.message).to.equal('some error'); 145 | expect(warren.brokers.filter(broker => { 146 | return broker.failureCount >= 1; 147 | }).length).to.equal(1); 148 | done(); 149 | }); 150 | }); 151 | }); 152 | 153 | it('waits a bit', done => { 154 | setTimeout(done, 1000); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /test/shutdown.test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const Code = require('code'); 6 | const expect = Code.expect; 7 | const proxyquire = require('proxyquire'); 8 | 9 | const createBroker = require('./mock/broker-create'); 10 | const createWarren = proxyquire('../', { 'broker-create': createBroker }); 11 | const warrenOptions = require('./utils').warrenOptions; 12 | 13 | describe('shutdown', () => { 14 | it('calls shutdown on all brokers', (done) => { 15 | createWarren(warrenOptions(), (error, warren) => { 16 | warren.shutdown(err => { 17 | expect(err).to.be.null(); 18 | expect(warren.brokers.map(broker => broker.down)).to.deep.equal( 19 | [true, true, true, true] 20 | ); 21 | done(); 22 | }); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/subscribe.test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const Code = require('code'); 6 | const expect = Code.expect; 7 | const proxyquire = require('proxyquire'); 8 | 9 | const createBroker = require('./mock/broker-create'); 10 | const createWarren = proxyquire('../', { 'broker-create': createBroker }); 11 | const warrenOptions = require('./utils').warrenOptions; 12 | 13 | describe('warren subscribe', () => { 14 | it('works if no broker errors', done => { 15 | const warrenOpts = warrenOptions(); 16 | createWarren(warrenOpts, (error, warren) => { 17 | const messages = ['message 1', 'message 2', 'message 3']; 18 | warren.subscribe('channel', message => { 19 | expect(message).to.equal(messages.shift()); 20 | if (! messages.length) { 21 | done(); 22 | } 23 | }); 24 | 25 | messages.forEach((message, index) => { 26 | warren.brokers[index % warren.brokers.length].message('channel', message); 27 | }); 28 | }); 29 | }); 30 | 31 | it('it doesnt works if all brokers error', done => { 32 | const warrenOpts = warrenOptions({failOnSubscribe: true}); 33 | createWarren(warrenOpts, (error, warren) => { 34 | let pendingErrors = warren.brokers.length; 35 | warren.on('error', err => { 36 | expect(err).to.be.object(); 37 | expect(err.message).to.equal('subscribe failed'); 38 | pendingErrors--; 39 | if (! pendingErrors) { 40 | done(); 41 | } 42 | }); 43 | warren.subscribe('channel', () => { 44 | throw new Error('should not reach here'); 45 | }); 46 | warren.brokers[0].message('channel', 'message'); 47 | }); 48 | }); 49 | 50 | it('works even if not waiting for all brokers to be created', done => { 51 | const warrenOpts = warrenOptions({minBrokersAvailable: 1}); 52 | createWarren(warrenOpts, (error, warren) => { 53 | const messages = ['message 1', 'message 2', 'message 3']; 54 | warren.subscribe('channel', message => { 55 | expect(message).to.equal(messages.shift()); 56 | if (! messages.length) { 57 | done(); 58 | } 59 | }); 60 | messages.forEach((message, index) => { 61 | warren.brokers[index % warren.brokers.length].message('channel', message); 62 | }); 63 | }); 64 | }); 65 | 66 | it('waits a bit', done => { 67 | setTimeout(done, 1000); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Trainline.com Limited. All rights reserved. See LICENSE.txt in the project root for license . 2 | 3 | 'use strict'; 4 | 5 | const get = require('lodash.get'); 6 | const times = require('lodash.times'); 7 | const forEach = require('lodash.foreach'); 8 | const merge = require('lodash.merge'); 9 | const omit = require('lodash.omit'); 10 | 11 | const defaultBrokerCount = 4; 12 | 13 | exports.createBrokerConfigs = function createBrokerConfigs(options) { 14 | const count = options && options.count || defaultBrokerCount; 15 | const configs = []; 16 | for (let i = 0; i < count; i ++) { 17 | configs.push(options); 18 | } 19 | return configs; 20 | }; 21 | 22 | exports.warrenOptions = (options) => { 23 | const hosts = []; 24 | if (options && !options.hosts) options.hosts = [options]; 25 | forEach(get(options, 'hosts') || [{}], host => { 26 | times(host.count || defaultBrokerCount, () => hosts.push(omit(host, 'count'))); 27 | }); 28 | return merge({ 29 | hosts: hosts, 30 | brokerConfig: { vhosts: { '/': {} } }, 31 | minBrokersAvailable: get(options, 'minBrokersAvailable') || hosts.length, 32 | }, options); 33 | }; 34 | 35 | exports.createBrokerOptions = options => ({ vhosts: { '/': { connection: options } } }); 36 | --------------------------------------------------------------------------------