├── .codeclimate.yml ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── node-js-ci.yml │ └── node-js-publish.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── _config.yml ├── examples ├── advanced │ ├── cluster.js │ ├── config.js │ ├── handlers │ │ ├── deleteUser.js │ │ └── saveUser.js │ └── index.js ├── busy-publisher │ ├── config.json │ ├── index.js │ ├── package-lock.json │ └── package.json ├── default-exchange │ ├── config.json │ └── index.js ├── mocha │ ├── config.json │ └── test.js ├── promises │ ├── config.json │ └── index.js ├── simple │ ├── config.json │ └── index.js └── streams │ ├── package-lock.json │ ├── package.json │ ├── publisher-config.json │ ├── publisher.js │ ├── subscriber-config.json │ └── subscriber.js ├── index.js ├── lib ├── amqp │ ├── Broker.js │ ├── BrokerAsPromised.js │ ├── Publication.js │ ├── PublicationSession.js │ ├── SubscriberError.js │ ├── SubscriberSession.js │ ├── SubscriberSessionAsPromised.js │ ├── Subscription.js │ ├── Vhost.js │ ├── XDeath.js │ └── tasks │ │ ├── applyBindings.js │ │ ├── assertExchanges.js │ │ ├── assertQueues.js │ │ ├── assertVhost.js │ │ ├── bounceVhost.js │ │ ├── checkExchanges.js │ │ ├── checkQueues.js │ │ ├── checkVhost.js │ │ ├── closeChannels.js │ │ ├── closeConnection.js │ │ ├── createChannels.js │ │ ├── createConnection.js │ │ ├── deleteExchanges.js │ │ ├── deleteQueues.js │ │ ├── deleteVhost.js │ │ ├── forewarnVhost.js │ │ ├── index.js │ │ ├── initCounters.js │ │ ├── initPublications.js │ │ ├── initShovels.js │ │ ├── initSubscriptions.js │ │ ├── initVhosts.js │ │ ├── nukeVhost.js │ │ ├── purgeQueues.js │ │ ├── purgeVhost.js │ │ └── shutdownVhost.js ├── backoff │ ├── exponential.js │ ├── index.js │ └── linear.js ├── config │ ├── baseline.js │ ├── configure.js │ ├── defaults.js │ ├── fqn.js │ ├── schema.json │ ├── tests.js │ └── validate.js ├── counters │ ├── inMemory.js │ ├── inMemoryCluster.js │ ├── index.js │ └── stub.js ├── management │ └── Client.js └── utils │ └── setTimeoutUnref.js ├── package-lock.json ├── package.json └── test ├── .eslintrc.json ├── backoff ├── exponential.tests.js └── linear.tests.js ├── broker.tests.js ├── brokerAsPromised.tests.js ├── caches └── inMemory.tests.js ├── config.tests.js ├── defaults.tests.js ├── publications.tests.js ├── publicationsAsPromised.tests.js ├── shovel.tests.js ├── subscriptions.tests.js ├── subscriptionsAsPromised.tests.js ├── utils └── amqputils.js ├── validation.tests.js └── vhost.tests.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: false 4 | duplication: 5 | enabled: false 6 | checks: 7 | method-complexity: 8 | enabled: false 9 | method-lines: 10 | enabled: false 11 | ratings: 12 | paths: 13 | - index.js 14 | - lib/** 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | examples 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-airbnb-base"], 3 | "env": { 4 | "node": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": "2018" 8 | }, 9 | "rules": { 10 | "arrow-body-style": 0, 11 | "consistent-return": 0, 12 | "func-names": 0, 13 | "max-len": 0, 14 | "no-param-reassign": 0, 15 | "no-plusplus": 0, 16 | "no-restricted-properties": 0, 17 | "no-shadow": ["error", { "allow": ["cb", "err"] }], 18 | "no-underscore-dangle": 0, 19 | "no-unused-expressions": 0, 20 | "no-unused-vars": ["error", { "varsIgnorePattern": "^debug$", "argsIgnorePattern": "^_\\w+" }], 21 | "no-use-before-define": 0, 22 | "prefer-destructuring": 0, 23 | "prefer-exponentiation-operator": 0, 24 | "prefer-rest-params": 0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/node-js-ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | services: 9 | rabbitmq: 10 | image: rabbitmq:3-management-alpine 11 | ports: 12 | - 5672:5672 13 | - 15672:15672 14 | strategy: 15 | matrix: 16 | node-version: [14.x, 16.x, 18.x, 20.x] 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm run lint 24 | - run: npm test 25 | 26 | code-climate: 27 | needs: build 28 | runs-on: ubuntu-latest 29 | services: 30 | rabbitmq: 31 | image: rabbitmq:3.13.2-management-alpine 32 | ports: 33 | - 5672:5672 34 | - 15672:15672 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: actions/setup-node@v3 38 | with: 39 | node-version: '14.x' 40 | - run: npm ci 41 | - run: npm install -g nyc 42 | - run: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 43 | - run: chmod +x ./cc-test-reporter 44 | - run: ./cc-test-reporter before-build 45 | - run: npm run coverage 46 | - run: ./cc-test-reporter format-coverage -t lcov coverage/lcov.info 47 | - run: ./cc-test-reporter upload-coverage 48 | env: 49 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 50 | -------------------------------------------------------------------------------- /.github/workflows/node-js-publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | services: 11 | rabbitmq: 12 | image: rabbitmq:3-management-alpine 13 | ports: 14 | - 5672:5672 15 | - 15672:15672 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: '14.x' 21 | - run: npm ci 22 | - run: npm run lint 23 | - run: npm test 24 | 25 | publish-npm: 26 | needs: build 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: '14.x' 33 | registry-url: https://registry.npmjs.org/ 34 | - run: npm ci 35 | - run: npm publish 36 | env: 37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | .codeclimate 5 | .idea 6 | .nyc_output 7 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint-staged && npm run test 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .codeclimate.yml 3 | .idea 4 | .eslintignore 5 | .eslintrc 6 | .github 7 | .nyc_output 8 | _config.yml 9 | cc-test-reporter 10 | coverage 11 | node_modules 12 | test 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2022 GuideSmiths Ltd. 4 | Copyright (c) 2022-present One Beyond 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /examples/advanced/cluster.js: -------------------------------------------------------------------------------- 1 | var Rascal = require('../..'); 2 | var cluster = require('cluster'); 3 | 4 | if (cluster.isMaster) { 5 | Rascal.counters.inMemoryCluster.master(); 6 | cluster.fork(); 7 | cluster.on('exit', function () { 8 | cluster.fork(); 9 | }); 10 | } else { 11 | require('./index'); 12 | } 13 | -------------------------------------------------------------------------------- /examples/advanced/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rascal: { 3 | vhosts: { 4 | // Define the name of the vhost 5 | 'customer-vhost': { 6 | // Creates the vhost if it doesn't exist (requires the RabbitMQ management plugin to be installed) 7 | assert: true, 8 | 9 | // Define the vhost connection parameters. Specify multiple entries for clusters. 10 | // Rascal will randomise the list, and cycle through the entries until it finds one that works 11 | connections: [ 12 | { 13 | url: 'amqp://does-not-exist-1b9935d9-5066-4b13-84dc-a8e2bb618154:5672/customer-vhost', 14 | }, 15 | { 16 | user: 'guest', 17 | password: 'guest', 18 | port: 5672, 19 | options: { 20 | heartbeat: 1, 21 | }, 22 | socketOptions: { 23 | timeout: 1000, 24 | }, 25 | }, 26 | ], 27 | 28 | // Define exchanges within the registration vhost 29 | exchanges: [ 30 | 'service', // Shared exchange for all services within this vhost 31 | 'delay', // To delay failed messages before a retry 32 | 'retry', // To retry failed messages up to maximum number of times 33 | 'dead_letters', // When retrying fails, messages end up here 34 | ], 35 | 36 | // Define queues within the registration vhost 37 | // A good naming convention for queues is consumer:entity:action 38 | queues: { 39 | // Create a queue for saving users 40 | 'registration_service:user:save': { 41 | options: { 42 | arguments: { 43 | // Route nacked messages to a service specific dead letter queue 44 | 'x-dead-letter-exchange': 'dead_letters', 45 | 'x-dead-letter-routing-key': 'registration_service.dead_letter', 46 | }, 47 | }, 48 | }, 49 | 50 | // Create a queue for deleting users 51 | 'registration_service:user:delete': { 52 | options: { 53 | arguments: { 54 | // Route nacked messages to a service specific dead letter queue 55 | 'x-dead-letter-exchange': 'dead_letters', 56 | 'x-dead-letter-routing-key': 'registration_service.dead_letter', 57 | }, 58 | }, 59 | }, 60 | 61 | // Create a delay queue to hold failed messages for a short interval before retrying 62 | 'delay:1m': { 63 | options: { 64 | arguments: { 65 | // Configure messages to expire after 1 minute, then route them to the retry exchange 66 | 'x-message-ttl': 60000, 67 | 'x-dead-letter-exchange': 'retry', 68 | }, 69 | }, 70 | }, 71 | 72 | // Queue for holding dead letters until they can be resolved 73 | 'dead_letters:registration_service': {}, 74 | }, 75 | 76 | // Bind the queues to the exchanges. 77 | // A good naming convention for routing keys is producer.entity.event 78 | bindings: { 79 | // Route create/update user messages to the save queue 80 | 'service[registration_webapp.user.created.#,registration_webapp.user.updated.#] -> registration_service:user:save': {}, 81 | 82 | // Route delete user messages to the delete queue 83 | 'service[registration_webapp.user.deleted.#] -> registration_service:user:delete': {}, 84 | 85 | // Route delayed messages to the 1 minute delay queue 86 | 'delay[delay.1m] -> delay:1m': {}, 87 | 88 | // Route retried messages back to their original queue using the CC routing keys set by Rascal 89 | 'retry[registration_service:user:save.#] -> registration_service:user:save': {}, 90 | 'retry[registration_service:user:delete.#] -> registration_service:user:delete': {}, 91 | 92 | // Route dead letters the service specific dead letter queue 93 | 'dead_letters[registration_service.dead_letter] -> dead_letters:registration_service': {}, 94 | }, 95 | 96 | // Setup subscriptions 97 | subscriptions: { 98 | save_user: { 99 | queue: 'registration_service:user:save', 100 | handler: 'saveUser.js', 101 | redeliveries: { 102 | limit: 5, 103 | counter: 'shared', 104 | }, 105 | }, 106 | 107 | delete_user: { 108 | queue: 'registration_service:user:delete', 109 | handler: 'deleteUser.js', 110 | redeliveries: { 111 | limit: 5, 112 | counter: 'shared', 113 | }, 114 | }, 115 | }, 116 | 117 | // Setup publications 118 | publications: { 119 | // Always publish a notification of success (it's useful for testing if nothing else) 120 | save_user_succeeded: { 121 | exchange: 'service', 122 | }, 123 | delete_user_succeeded: { 124 | exchange: 'service', 125 | encryption: 'well-known-v1', 126 | }, 127 | 128 | // Forward messages to the 1 minute delay queue when retrying 129 | retry_in_1m: { 130 | exchange: 'delay', 131 | options: { 132 | CC: ['delay.1m'], 133 | }, 134 | }, 135 | 136 | // Publication for generating user create, update and delete messages 137 | // This would probably be the job of another application (e.g. a web application) 138 | user_event: { 139 | exchange: 'service', 140 | // Specifying an encryption profile in the publication will cause the message content to be encrypted 141 | // The profile name and iv are added as headers, and used to automatically decrypt messages, 142 | // providing the consumer configuration has a matching profile. 143 | encryption: 'well-known-v1', 144 | }, 145 | }, 146 | 147 | // Configure confirm channel pools. See https://www.npmjs.com/package/generic-pool 148 | // The demo application doesn't publish using regular channels. A regular pool will be created by default, but 149 | // never allocated channels because autostart defaults to false. 150 | publicationChannelPools: { 151 | confirmPool: { 152 | max: 10, 153 | min: 5, 154 | autostart: true, 155 | }, 156 | }, 157 | }, 158 | }, 159 | // Define recovery strategies for different error scenarios 160 | recovery: { 161 | // Deferred retry is a good strategy for temporary (connection timeout) or unknown errors 162 | deferred_retry: [ 163 | { 164 | strategy: 'forward', 165 | attempts: 10, 166 | publication: 'retry_in_1m', 167 | xDeathFix: true, // See https://github.com/rabbitmq/rabbitmq-server/issues/161 168 | }, 169 | { 170 | strategy: 'nack', 171 | }, 172 | ], 173 | 174 | // Republishing with immediate nack returns the message to the original queue but decorates 175 | // it with error headers. The next time Rascal encounters the message it immediately nacks it 176 | // causing it to be routed to the services dead letter queue 177 | dead_letter: [ 178 | { 179 | strategy: 'republish', 180 | immediateNack: true, 181 | }, 182 | ], 183 | }, 184 | // Define counter(s) for counting redeliveries 185 | redeliveries: { 186 | counters: { 187 | shared: { 188 | size: 10, 189 | type: 'inMemoryCluster', 190 | }, 191 | }, 192 | }, 193 | // Define encryption profiles 194 | encryption: { 195 | 'well-known-v1': { 196 | key: 'f81db52a3b2c717fe65d9a3b7dd04d2a08793e1a28e3083db3ea08db56e7c315', 197 | ivLength: 16, 198 | algorithm: 'aes-256-cbc', 199 | }, 200 | }, 201 | }, 202 | }; 203 | -------------------------------------------------------------------------------- /examples/advanced/handlers/deleteUser.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var format = require('util').format; 3 | 4 | module.exports = function (broker) { 5 | return function (user, cb) { 6 | // Pretend we'd really asynchronously deleted something 7 | setImmediate(function () { 8 | console.log(chalk.cyan('Deleting user:'), user.username); 9 | 10 | if (user.crash) throw new Error('Crashing on user: ' + user.username); 11 | 12 | var routingKey = format('registration_service.user.deleted.%s', user.username); 13 | broker.publish('delete_user_succeeded', routingKey, function (err, publication) { 14 | if (err) return cb(err); 15 | publication.on('success', () => cb).on('error', console.error); 16 | }); 17 | }); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /examples/advanced/handlers/saveUser.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var format = require('util').format; 3 | 4 | module.exports = function (broker) { 5 | return function (user, cb) { 6 | // Pretend we'd really asynchronously saved something 7 | setImmediate(function () { 8 | console.log(chalk.magenta('Saving user:'), user.username); 9 | 10 | if (user.crash) throw new Error('Crashing on user: ' + user.username); 11 | 12 | // Simulate errors and success 13 | var err; 14 | switch (Math.floor(Math.random() * 10)) { 15 | case 5: { 16 | err = new Error('Connection Timeout'); 17 | err.recoverable = true; 18 | return cb(err); 19 | } 20 | case 7: { 21 | err = new Error('Duplicate Key Violation'); 22 | err.recoverable = false; 23 | return cb(err); 24 | } 25 | default: { 26 | var routingKey = format('registration_service.user.saved.%s', user.username); 27 | broker.publish('save_user_succeeded', routingKey, function (err, publication) { 28 | if (err) return cb(err); 29 | publication.on('success', () => cb).on('error', console.error); 30 | }); 31 | } 32 | } 33 | }); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /examples/advanced/index.js: -------------------------------------------------------------------------------- 1 | var Rascal = require('../..'); 2 | var config = require('./config'); 3 | var _ = require('lodash'); 4 | var Chance = require('Chance'); 5 | var chance = new Chance(); 6 | var format = require('util').format; 7 | 8 | Rascal.Broker.create(Rascal.withDefaultConfig(config.rascal), function (err, broker) { 9 | if (err) bail(err); 10 | 11 | broker.on('error', function (err) { 12 | console.error(err.message); 13 | }); 14 | 15 | _.each(broker.config.subscriptions, function (subscriptionConfig, subscriptionName) { 16 | if (!subscriptionConfig.handler) return; 17 | 18 | var handler = require('./handlers/' + subscriptionConfig.handler)(broker); 19 | 20 | broker.subscribe(subscriptionName, function (err, subscription) { 21 | if (err) return bail(err); 22 | subscription 23 | .on('message', function (message, content, ackOrNack) { 24 | handler(content, function (err) { 25 | if (!err) return ackOrNack(); 26 | console.log(err); 27 | ackOrNack(err, err.recoverable ? broker.config.recovery.deferred_retry : broker.config.recovery.dead_letter); 28 | }); 29 | }) 30 | .on('invalid_content', function (err, message, ackOrNack) { 31 | console.error('Invalid Content', err.message); 32 | ackOrNack(err, broker.config.recovery.dead_letter); 33 | }) 34 | .on('redeliveries_exceeded', function (err, message, ackOrNack) { 35 | console.error('Redeliveries Exceeded', err.message); 36 | ackOrNack(err, broker.config.recovery.dead_letter); 37 | }) 38 | .on('cancel', function (err) { 39 | console.warn(err.message); 40 | }) 41 | .on('error', function (err) { 42 | console.error(err.message); 43 | }); 44 | }); 45 | }); 46 | 47 | // Simulate a web app handling user registrations 48 | setInterval(function () { 49 | var user = { 50 | username: chance.first() + '_' + chance.last(), 51 | crash: randomInt(10) === 10, 52 | }; 53 | var events = { 1: 'created', 2: 'updated', 3: 'deleted' }; 54 | var event = events[randomInt(3)]; 55 | var routingKey = format('registration_webapp.user.%s.%s', event, user.username); 56 | 57 | broker.publish('user_event', user, routingKey, function (err, publication) { 58 | if (err) return console.log(err.message); 59 | publication 60 | .on('success', function () { 61 | // confirmed 62 | }) 63 | .on('error', function (err) { 64 | console.error(err.message); 65 | }); 66 | }); 67 | }, 1000); 68 | 69 | process 70 | .on('SIGINT', function () { 71 | broker.shutdown(function () { 72 | process.exit(); 73 | }); 74 | }) 75 | .on('SIGTERM', () => { 76 | broker.shutdown(function () { 77 | process.exit(); 78 | }); 79 | }) 80 | .on('unhandledRejection', (reason, p) => { 81 | console.error(reason, 'Unhandled Rejection at Promise', p); 82 | broker.shutdown(function () { 83 | process.exit(-1); 84 | }); 85 | }) 86 | .on('uncaughtException', (err) => { 87 | console.error(err, 'Uncaught Exception thrown'); 88 | broker.shutdown(function () { 89 | process.exit(-2); 90 | }); 91 | }); 92 | }); 93 | 94 | function randomInt(max) { 95 | return Math.floor(Math.random() * max) + 1; 96 | } 97 | 98 | function bail(err) { 99 | console.error(err); 100 | process.exit(1); 101 | } 102 | -------------------------------------------------------------------------------- /examples/busy-publisher/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../lib/config/schema.json", 3 | "vhosts": { 4 | "/": { 5 | "publicationChannelPools": { 6 | "regularPool": { 7 | "max": 10, 8 | "min": 10, 9 | "evictionRunIntervalMillis": 1000, 10 | "idleTimeoutMillis": 5000, 11 | "autostart": true 12 | } 13 | }, 14 | "connection": { 15 | "socketOptions": { 16 | "timeout": 1000 17 | } 18 | }, 19 | "exchanges": ["demo_ex"], 20 | "queues": ["demo_q"], 21 | "bindings": ["demo_ex[a.b.c] -> demo_q"], 22 | "publications": { 23 | "demo_pub": { 24 | "exchange": "demo_ex", 25 | "routingKey": "a.b.c", 26 | "confirm": false, 27 | "options": { 28 | "persistent": false 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/busy-publisher/index.js: -------------------------------------------------------------------------------- 1 | const Rascal = require('../..'); 2 | const config = require('./config'); 3 | const random = require('random-readable'); 4 | 5 | Rascal.Broker.create(Rascal.withDefaultConfig(config), (err, broker) => { 6 | if (err) throw err; 7 | 8 | broker.on('error', console.error); 9 | 10 | const stream = random 11 | .createRandomStream() 12 | .on('error', console.error) 13 | .on('data', (data) => { 14 | broker.publish('demo_pub', data, (err, publication) => { 15 | if (err) throw err; 16 | publication.on('error', console.error); 17 | }); 18 | }) 19 | .on('end', () => { 20 | console.log('end'); 21 | }); 22 | 23 | broker.on('busy', (details) => { 24 | console.log(Date.now(), `Pausing vhost: ${details.vhost} (mode: ${details.mode}, queue: ${details.queue}, size: ${details.size}, borrowed: ${details.borrowed}, available: ${details.available})`); 25 | stream.pause(); 26 | }); 27 | 28 | broker.on('ready', (details) => { 29 | console.log(Date.now(), `Resuming vhost: ${details.vhost} (mode: ${details.mode}, queue: ${details.queue}, size: ${details.size}, borrowed: ${details.borrowed}, available: ${details.available})`); 30 | stream.resume(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /examples/busy-publisher/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "busy-publisher", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "random-readable": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/random-readable/-/random-readable-1.0.1.tgz", 10 | "integrity": "sha512-Y++VltLA4yRsvFDAPbODh9hMw7cfkng+c/S+44ob6xGt0itLr8s6VhANl7kY7igEv3igPgzdc+T8EhBjQWjd9g==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/busy-publisher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "busy-publisher", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "random-readable": "^1.0.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/default-exchange/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../lib/config/schema.json", 3 | "vhosts": { 4 | "/": { 5 | "connection": { 6 | "socketOptions": { 7 | "timeout": 1000 8 | } 9 | }, 10 | "exchanges": [""], 11 | "queues": ["demo_q"], 12 | "publications": { 13 | "demo_pub": { 14 | "exchange": "" 15 | } 16 | }, 17 | "subscriptions": { 18 | "demo_sub": { 19 | "queue": "demo_q" 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/default-exchange/index.js: -------------------------------------------------------------------------------- 1 | var Rascal = require('../..'); 2 | var config = require('./config'); 3 | 4 | Rascal.Broker.create(Rascal.withDefaultConfig(config), function (err, broker) { 5 | if (err) throw err; 6 | 7 | broker 8 | .subscribe('demo_sub', function (err, subscription) { 9 | if (err) throw err; 10 | subscription 11 | .on('message', function (message, content, ackOrNack) { 12 | console.log(content); 13 | ackOrNack(); 14 | }) 15 | .on('error', console.error); 16 | }) 17 | .on('error', console.error); 18 | 19 | setInterval(function () { 20 | broker.publish('demo_pub', new Date().toISOString() + ': hello world', 'demo_q', function (err, publication) { 21 | if (err) throw err; 22 | publication.on('error', console.error); 23 | }); 24 | }, 1000); 25 | }); 26 | -------------------------------------------------------------------------------- /examples/mocha/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../lib/config/schema.json", 3 | "vhosts": { 4 | "/": { 5 | "exchanges": ["demo_ex"], 6 | "queues": ["demo_q"], 7 | "bindings": ["demo_ex[a.b.c] -> demo_q"], 8 | "publications": { 9 | "demo_pub": { 10 | "exchange": "demo_ex", 11 | "routingKey": "a.b.c" 12 | } 13 | }, 14 | "subscriptions": { 15 | "demo_sub": { 16 | "queue": "demo_q" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/mocha/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Rascal = require('../..'); 3 | var config = require('./config.js'); 4 | 5 | describe('Example rascal test', function () { 6 | var broker; 7 | 8 | before(function (done) { 9 | config.vhosts['/'].publications.test_pub = { exchange: 'demo_ex' }; 10 | Rascal.Broker.create(Rascal.withTestConfig(config), function (err, _broker) { 11 | if (err) return done(err); 12 | broker = _broker; 13 | done(); 14 | }); 15 | }); 16 | 17 | beforeEach(function (done) { 18 | broker.purge(done); 19 | }); 20 | 21 | after(function (done) { 22 | if (!broker) return done(); 23 | broker.nuke(done); 24 | }); 25 | 26 | it('should demonstrate tests', function (done) { 27 | broker.subscribe('demo_sub', function (err, subscription) { 28 | assert.ifError(err); 29 | subscription.on('message', function (message, content, ackOrNack) { 30 | subscription.cancel(); 31 | ackOrNack(); 32 | done(); 33 | }); 34 | }); 35 | 36 | broker.publish('test_pub', 'Hello Test', 'a.b.c', function (err, publication) { 37 | assert.ifError(err); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /examples/promises/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../lib/config/schema.json", 3 | "vhosts": { 4 | "/": { 5 | "connection": { 6 | "options": { 7 | "heartbeat": 5 8 | }, 9 | "socketOptions": { 10 | "timeout": 1000 11 | } 12 | }, 13 | "exchanges": ["demo_ex"], 14 | "queues": ["demo_q"], 15 | "bindings": ["demo_ex[a.b.c] -> demo_q"], 16 | "publications": { 17 | "demo_pub": { 18 | "exchange": "demo_ex", 19 | "routingKey": "a.b.c" 20 | } 21 | }, 22 | "subscriptions": { 23 | "demo_sub": { 24 | "queue": "demo_q" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/promises/index.js: -------------------------------------------------------------------------------- 1 | var Rascal = require('../..'); 2 | var config = require('./config'); 3 | 4 | (async function () { 5 | try { 6 | const broker = await Rascal.BrokerAsPromised.create(Rascal.withDefaultConfig(config)); 7 | broker.on('error', console.error); 8 | 9 | try { 10 | const subscription = await broker.subscribe('demo_sub'); 11 | subscription 12 | .on('message', function (message, content, ackOrNack) { 13 | console.log(content); 14 | ackOrNack(); 15 | }) 16 | .on('error', console.error); 17 | } catch (err) { 18 | console.error(err); 19 | } 20 | 21 | setInterval(async function () { 22 | try { 23 | const publication = await broker.publish('demo_pub', new Date().toISOString() + ': hello world'); 24 | publication.on('error', console.error); 25 | } catch (err) { 26 | console.error(err); 27 | } 28 | }, 1000); 29 | } catch (err) { 30 | console.error(err); 31 | } 32 | })(); 33 | -------------------------------------------------------------------------------- /examples/simple/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../lib/config/schema.json", 3 | "vhosts": { 4 | "/": { 5 | "publicationChannelPools": { 6 | "confirmPool": { 7 | "autostart": true 8 | } 9 | }, 10 | "connection": { 11 | "options": { 12 | "heartbeat": 10 13 | }, 14 | "socketOptions": { 15 | "timeout": 1000 16 | } 17 | }, 18 | "exchanges": ["demo_ex"], 19 | "queues": ["demo_q"], 20 | "bindings": ["demo_ex[a.b.c] -> demo_q"], 21 | "publications": { 22 | "demo_pub": { 23 | "exchange": "demo_ex", 24 | "routingKey": "a.b.c", 25 | "options": { 26 | "persistent": false 27 | } 28 | } 29 | }, 30 | "subscriptions": { 31 | "demo_sub": { 32 | "queue": "demo_q" 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | var Rascal = require('../..'); 2 | var config = require('./config'); 3 | 4 | Rascal.Broker.create(Rascal.withDefaultConfig(config), function (err, broker) { 5 | if (err) throw err; 6 | 7 | broker.subscribe('demo_sub', function (err, subscription) { 8 | if (err) throw err; 9 | subscription.on('message', function (message, content, ackOrNack) { 10 | console.log(content); 11 | ackOrNack(); 12 | }); 13 | subscription.on('error', console.error); 14 | subscription.on('cancel', console.warn); 15 | }); 16 | broker.on('error', console.error); 17 | 18 | setInterval(function () { 19 | broker.publish('demo_pub', new Date().toISOString() + ': hello world', function (err, publication) { 20 | if (err) throw err; 21 | publication.on('error', console.error); 22 | }); 23 | }, 1000); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/streams/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streams", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "random-readable": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/random-readable/-/random-readable-1.0.1.tgz", 10 | "integrity": "sha512-Y++VltLA4yRsvFDAPbODh9hMw7cfkng+c/S+44ob6xGt0itLr8s6VhANl7kY7igEv3igPgzdc+T8EhBjQWjd9g==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/streams/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streams", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "random-readable": "^1.0.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/streams/publisher-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../lib/config/schema.json", 3 | "vhosts": { 4 | "/": { 5 | "publicationChannelPools": { 6 | "regularPool": { 7 | "max": 10, 8 | "min": 10, 9 | "evictionRunIntervalMillis": 1000, 10 | "idleTimeoutMillis": 5000, 11 | "autostart": true 12 | } 13 | }, 14 | "connection": { 15 | "socketOptions": { 16 | "timeout": 1000 17 | } 18 | }, 19 | "queues": { 20 | "demo_stream": { 21 | "options": { 22 | "arguments": { 23 | "x-queue-type": "stream", 24 | "x-max-length-bytes": 10485760 25 | } 26 | } 27 | } 28 | }, 29 | "publications": { 30 | "demo_pub": { 31 | "queue": "demo_stream", 32 | "confirm": false 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/streams/publisher.js: -------------------------------------------------------------------------------- 1 | const Rascal = require('../..'); 2 | const config = require('./publisher-config'); 3 | const random = require('random-readable'); 4 | const max = parseInt(process.argv[2], 10) || Infinity; 5 | 6 | Rascal.Broker.create(Rascal.withDefaultConfig(config), (err, broker) => { 7 | if (err) throw err; 8 | 9 | broker.on('error', console.error); 10 | 11 | let count = 0; 12 | 13 | const stream = random 14 | .createRandomStream() 15 | .on('error', console.error) 16 | .on('data', (data) => { 17 | broker.publish('demo_pub', data, (err, publication) => { 18 | if (err) throw err; 19 | else if (count >= max) stream.destroy(); 20 | else count++; 21 | publication.on('error', console.error); 22 | }); 23 | }) 24 | .on('close', () => { 25 | console.log(`Published ${count} messages`) 26 | broker.shutdown(); 27 | }); ; 28 | 29 | broker.on('busy', (details) => { 30 | console.log(Date.now(), `Pausing vhost: ${details.vhost} (mode: ${details.mode}, queue: ${details.queue}, size: ${details.size}, borrowed: ${details.borrowed}, available: ${details.available})`); 31 | stream.pause(); 32 | }); 33 | 34 | broker.on('ready', (details) => { 35 | console.log(Date.now(), `Resuming vhost: ${details.vhost} (mode: ${details.mode}, queue: ${details.queue}, size: ${details.size}, borrowed: ${details.borrowed}, available: ${details.available})`); 36 | stream.resume(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /examples/streams/subscriber-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../lib/config/schema.json", 3 | "vhosts": { 4 | "/": { 5 | "connection": { 6 | "socketOptions": { 7 | "timeout": 1000 8 | } 9 | }, 10 | "queues": { 11 | "demo_stream": { 12 | "options": { 13 | "arguments": { 14 | "x-queue-type": "stream", 15 | "x-max-length-bytes": 10485760 16 | } 17 | } 18 | } 19 | }, 20 | "subscriptions": { 21 | "demo_sub": { 22 | "queue": "demo_stream", 23 | "prefetch": 250 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/streams/subscriber.js: -------------------------------------------------------------------------------- 1 | const Rascal = require('../..'); 2 | const config = require('./subscriber-config'); 3 | const offset = parseInt(process.argv[2], 10) || 'first'; 4 | 5 | Rascal.Broker.create(Rascal.withDefaultConfig(config), (err, broker) => { 6 | if (err) throw err; 7 | 8 | broker.on('error', console.error); 9 | 10 | const overrides = { 11 | options: { 12 | arguments: { 13 | 'x-stream-offset': offset 14 | } 15 | } 16 | }; 17 | 18 | broker.subscribe('demo_sub', overrides, (err, subscription) => { 19 | if (err) throw err; 20 | subscription.on('message', (message, content, ackOrNack) => { 21 | console.log(`Received message: ${message.properties.headers['x-stream-offset']}`) 22 | ackOrNack(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const defaultConfig = require('./lib/config/defaults'); 3 | const testConfig = require('./lib/config/tests'); 4 | const Broker = require('./lib/amqp/Broker'); 5 | const BrokerAsPromised = require('./lib/amqp/BrokerAsPromised'); 6 | const counters = require('./lib/counters'); 7 | 8 | module.exports = (function () { 9 | return { 10 | Broker, 11 | BrokerAsPromised, 12 | createBroker: Broker.create, 13 | createBrokerAsPromised: BrokerAsPromised.create, 14 | defaultConfig, 15 | testConfig, 16 | withDefaultConfig(config) { 17 | return _.defaultsDeep({}, config, defaultConfig); 18 | }, 19 | withTestConfig(config) { 20 | return _.defaultsDeep({}, config, testConfig); 21 | }, 22 | counters, 23 | }; 24 | }()); 25 | -------------------------------------------------------------------------------- /lib/amqp/Broker.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:Broker'); 2 | const format = require('util').format; 3 | const inherits = require('util').inherits; 4 | const EventEmitter = require('events').EventEmitter; 5 | const _ = require('lodash'); 6 | const async = require('async'); 7 | const tasks = require('./tasks'); 8 | const configure = require('../config/configure'); 9 | const validate = require('../config/validate'); 10 | const fqn = require('../config/fqn'); 11 | 12 | const preflight = async.compose(validate, configure); 13 | const stub = require('../counters/stub'); 14 | const inMemory = require('../counters/inMemory'); 15 | const inMemoryCluster = require('../counters/inMemoryCluster').worker; 16 | 17 | const maxInterval = 2147483647; 18 | 19 | module.exports = { 20 | create: function create(config, components, next) { 21 | if (arguments.length === 2) return create(config, {}, arguments[1]); 22 | 23 | const counters = _.defaults({}, components.counters, { 24 | stub, 25 | inMemory, 26 | inMemoryCluster, 27 | }); 28 | 29 | preflight(_.cloneDeep(config), (err, augmentedConfig) => { 30 | if (err) return next(err); 31 | new Broker(augmentedConfig, _.assign({}, components, { counters }))._init(next); 32 | }); 33 | }, 34 | }; 35 | 36 | inherits(Broker, EventEmitter); 37 | 38 | function Broker(config, components) { 39 | const self = this; 40 | let vhosts = {}; 41 | let publications = {}; 42 | let subscriptions = {}; 43 | let sessions = []; 44 | const init = async.compose(tasks.initShovels, tasks.initSubscriptions, tasks.initPublications, tasks.initCounters, tasks.initVhosts); 45 | const nukeVhost = async.compose(tasks.deleteVhost, tasks.shutdownVhost, tasks.nukeVhost); 46 | const purgeVhost = tasks.purgeVhost; 47 | const forewarnVhost = tasks.forewarnVhost; 48 | const shutdownVhost = tasks.shutdownVhost; 49 | const bounceVhost = tasks.bounceVhost; 50 | 51 | this.config = _.cloneDeep(config); 52 | this.promises = false; 53 | 54 | this._init = function (next) { 55 | debug('Initialising broker'); 56 | vhosts = {}; 57 | publications = {}; 58 | subscriptions = {}; 59 | sessions = []; 60 | init(config, { broker: self, components }, (err) => { 61 | self.keepActive = setInterval(_.noop, maxInterval); 62 | setImmediate(() => { 63 | next(err, self); 64 | }); 65 | }); 66 | }; 67 | 68 | this.connect = function (name, next) { 69 | if (!vhosts[name]) return next(new Error(format('Unknown vhost: %s', name))); 70 | vhosts[name].connect(next); 71 | }; 72 | 73 | this.purge = function (next) { 74 | debug('Purging all queues in all vhosts'); 75 | async.eachSeries( 76 | _.values(vhosts), 77 | (vhost, callback) => { 78 | purgeVhost(config, { vhost }, callback); 79 | }, 80 | (err) => { 81 | if (err) return next(err); 82 | debug('Finished purging all queues in all vhosts'); 83 | next(); 84 | }, 85 | ); 86 | }; 87 | 88 | this.shutdown = function (next) { 89 | debug('Shutting down broker'); 90 | async.eachSeries( 91 | _.values(vhosts), 92 | (vhost, callback) => { 93 | forewarnVhost(config, { vhost }, callback); 94 | }, 95 | (err) => { 96 | if (err) return next(err); 97 | self.unsubscribeAll((err) => { 98 | if (err) self.emit('error', err); 99 | async.eachSeries( 100 | _.values(vhosts), 101 | (vhost, callback) => { 102 | shutdownVhost(config, { vhost }, callback); 103 | }, 104 | (err) => { 105 | if (err) return next(err); 106 | clearInterval(self.keepActive); 107 | debug('Finished shutting down broker'); 108 | next(); 109 | }, 110 | ); 111 | }); 112 | }, 113 | ); 114 | }; 115 | 116 | this.bounce = function (next) { 117 | debug('Bouncing broker'); 118 | self.unsubscribeAll((err) => { 119 | if (err) self.emit('error', err); 120 | async.eachSeries( 121 | _.values(vhosts), 122 | (vhost, callback) => { 123 | bounceVhost(config, { vhost }, callback); 124 | }, 125 | (err) => { 126 | if (err) return next(err); 127 | debug('Finished bouncing broker'); 128 | next(); 129 | }, 130 | ); 131 | }); 132 | }; 133 | 134 | this.nuke = function (next) { 135 | debug('Nuking broker'); 136 | self.unsubscribeAll((err) => { 137 | if (err) self.emit('error', err); 138 | async.eachSeries( 139 | _.values(vhosts), 140 | (vhost, callback) => { 141 | nukeVhost(config, { vhost, components }, callback); 142 | }, 143 | (err) => { 144 | if (err) return next(err); 145 | vhosts = {}; 146 | publications = {}; 147 | subscriptions = {}; 148 | clearInterval(self.keepActive); 149 | debug('Finished nuking broker'); 150 | next(); 151 | }, 152 | ); 153 | }); 154 | }; 155 | 156 | this.publish = function (name, message, overrides, next) { 157 | if (arguments.length === 3) return self.publish(name, message, {}, arguments[2]); 158 | if (_.isString(overrides)) return self.publish(name, message, { routingKey: overrides }, next); 159 | if (!publications[name]) return next(new Error(format('Unknown publication: %s', name))); 160 | publications[name].publish(message, overrides, next); 161 | }; 162 | 163 | this.forward = function (name, message, overrides, next) { 164 | if (arguments.length === 3) return self.forward(name, message, {}, arguments[2]); 165 | if (_.isString(overrides)) return self.forward(name, message, { routingKey: overrides }, next); 166 | if (!config.publications[name]) return next(new Error(format('Unknown publication: %s', name))); 167 | publications[name].forward(message, overrides, next); 168 | }; 169 | 170 | this.subscribe = function (name, overrides, next) { 171 | if (arguments.length === 2) return self.subscribe(name, {}, arguments[1]); 172 | if (!subscriptions[name]) return next(new Error(format('Unknown subscription: %s', name))); 173 | subscriptions[name].subscribe(overrides, (err, session) => { 174 | if (err) return next(err); 175 | sessions.push(session); 176 | next(null, session); 177 | }); 178 | }; 179 | 180 | this.subscribeAll = function (filter, next) { 181 | if (arguments.length === 1) { 182 | return self.subscribeAll(() => { 183 | return true; 184 | }, arguments[0]); 185 | } 186 | const filteredSubscriptions = _.chain(config.subscriptions).values().filter(filter).value(); 187 | async.mapSeries( 188 | filteredSubscriptions, 189 | (subscriptionConfig, cb) => { 190 | self.subscribe(subscriptionConfig.name, (err, subscription) => { 191 | if (err) return cb(err); 192 | cb(null, subscription); 193 | }); 194 | }, 195 | next, 196 | ); 197 | }; 198 | 199 | this.unsubscribeAll = function (next) { 200 | async.each( 201 | sessions.slice(), 202 | (session, cb) => { 203 | sessions.shift(); 204 | session.cancel(cb); 205 | }, 206 | next, 207 | ); 208 | }; 209 | 210 | this.getConnections = function () { 211 | return Object.keys(vhosts).map((name) => { 212 | return vhosts[name].getConnectionDetails(); 213 | }); 214 | }; 215 | 216 | /* eslint-disable-next-line no-multi-assign */ 217 | this.getFullyQualifiedName = this.qualify = function (vhost, name) { 218 | return fqn.qualify(name, config.vhosts[vhost].namespace); 219 | }; 220 | 221 | this._addVhost = function (vhost) { 222 | vhosts[vhost.name] = vhost; 223 | }; 224 | 225 | this._addPublication = function (publication) { 226 | publications[publication.name] = publication; 227 | }; 228 | 229 | this._addSubscription = function (subscription) { 230 | subscriptions[subscription.name] = subscription; 231 | }; 232 | } 233 | -------------------------------------------------------------------------------- /lib/amqp/BrokerAsPromised.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits; 2 | const EventEmitter = require('events').EventEmitter; 3 | const forwardEvents = require('forward-emitter'); 4 | const _ = require('lodash'); 5 | const Broker = require('./Broker'); 6 | const SubscriberSessionAsPromised = require('./SubscriberSessionAsPromised'); 7 | 8 | module.exports = { 9 | create() { 10 | const args = Array.prototype.slice.call(arguments); 11 | return new Promise((resolve, reject) => { 12 | Broker.create( 13 | ...args.concat((err, broker) => { 14 | if (err && !broker) return reject(err); 15 | broker.promises = true; 16 | const brokerAsPromised = new BrokerAsPromised(broker); 17 | if (!err) return resolve(brokerAsPromised); 18 | err.broker = Symbol('broker-as-promised'); 19 | Object.defineProperty(err, err.broker, { 20 | enumerable: false, 21 | value: brokerAsPromised, 22 | }); 23 | return reject(err); 24 | }), 25 | ); 26 | }); 27 | }, 28 | }; 29 | 30 | inherits(BrokerAsPromised, EventEmitter); 31 | 32 | function BrokerAsPromised(broker) { 33 | const methods = ['connect', 'nuke', 'purge', 'shutdown', 'bounce', 'publish', 'forward', 'unsubscribeAll']; 34 | const self = this; 35 | 36 | forwardEvents(broker, this); 37 | 38 | _.each(methods, (method) => { 39 | self[method] = function () { 40 | const args = Array.prototype.slice.call(arguments); 41 | return new Promise((resolve, reject) => { 42 | broker[method]( 43 | ...args.concat((err, result) => { 44 | if (err) return reject(err); 45 | resolve(result); 46 | }), 47 | ); 48 | }); 49 | }; 50 | }); 51 | 52 | this.config = broker.config; 53 | this.getConnections = broker.getConnections; 54 | 55 | this.subscribe = function () { 56 | const args = Array.prototype.slice.call(arguments); 57 | return new Promise((resolve, reject) => { 58 | broker.subscribe( 59 | ...args.concat((err, session) => { 60 | if (err) return reject(err); 61 | resolve(new SubscriberSessionAsPromised(session)); 62 | }), 63 | ); 64 | }); 65 | }; 66 | 67 | this.subscribeAll = function () { 68 | const args = Array.prototype.slice.call(arguments); 69 | return new Promise((resolve, reject) => { 70 | broker.subscribeAll( 71 | ...args.concat((err, sessions) => { 72 | if (err) return reject(err); 73 | resolve( 74 | sessions.map((session) => { 75 | return new SubscriberSessionAsPromised(session); 76 | }), 77 | ); 78 | }), 79 | ); 80 | }); 81 | }; 82 | 83 | /* eslint-disable-next-line no-multi-assign */ 84 | this.getFullyQualifiedName = this.qualify = function (vhost, name) { 85 | return broker.qualify(vhost, name); 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /lib/amqp/Publication.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:Publication'); 2 | const format = require('util').format; 3 | const _ = require('lodash'); 4 | const uuid = require('uuid').v4; 5 | const crypto = require('crypto'); 6 | const PublicationSession = require('./PublicationSession'); 7 | const setTimeoutUnref = require('../utils/setTimeoutUnref'); 8 | 9 | module.exports = { 10 | create(vhost, config, next) { 11 | const borrowConfirmChannel = vhost.borrowConfirmChannel.bind(vhost); 12 | const returnConfirmChannel = vhost.returnConfirmChannel.bind(vhost); 13 | const destroyConfirmChannel = vhost.destroyConfirmChannel.bind(vhost); 14 | const borrowChannel = vhost.borrowChannel.bind(vhost); 15 | const returnChannel = vhost.returnChannel.bind(vhost); 16 | const destroyChannel = vhost.destroyChannel.bind(vhost); 17 | 18 | if (Object.prototype.hasOwnProperty.call(config, 'exchange') && config.confirm) return new Publication(vhost, borrowConfirmChannel, returnConfirmChannel, destroyConfirmChannel, publishToConfirmExchange, config).init(next); 19 | if (Object.prototype.hasOwnProperty.call(config, 'exchange')) return new Publication(vhost, borrowChannel, returnChannel, destroyChannel, publishToExchange, config).init(next); 20 | if (config.queue && config.confirm) return new Publication(vhost, borrowConfirmChannel, returnConfirmChannel, destroyConfirmChannel, sendToConfirmQueue, config).init(next); 21 | if (config.queue) return new Publication(vhost, borrowChannel, returnChannel, destroyChannel, sendToQueue, config).init(next); 22 | }, 23 | }; 24 | 25 | function Publication(vhost, borrowChannelFn, returnChannelFn, destroyChannelFn, publishFn, config) { 26 | const self = this; 27 | 28 | this.name = config.name; 29 | 30 | this.init = function (next) { 31 | debug('Initialising publication: %s', config.name); 32 | next(null, self); 33 | }; 34 | 35 | this.publish = function (payload, overrides, next) { 36 | const publishConfig = _.defaultsDeep({}, overrides, config); 37 | const content = getContent(payload); 38 | publishConfig.options.contentType = publishConfig.options.contentType || content.type; 39 | publishConfig.options.messageId = publishConfig.options.messageId || uuid(); 40 | publishConfig.options.replyTo = publishConfig.options.replyTo || publishConfig.replyTo; 41 | 42 | publishConfig.encryption ? _publishEncrypted(content.buffer, publishConfig, next) : _publish(content.buffer, publishConfig, next); 43 | }; 44 | 45 | this.forward = function (message, overrides, next) { 46 | const originalQueue = message.properties.headers.rascal.originalQueue; 47 | const publishConfig = _.defaultsDeep({}, overrides, config, { 48 | routingKey: message.fields.routingKey, 49 | }); 50 | 51 | publishConfig.options = _.defaultsDeep(publishConfig.options, message.properties); 52 | 53 | _.set(publishConfig, 'options.headers.rascal.restoreRoutingHeaders', !!publishConfig.restoreRoutingHeaders); 54 | _.set(publishConfig, 'options.headers.rascal.originalExchange', message.fields.exchange); 55 | _.set(publishConfig, 'options.headers.rascal.originalRoutingKey', message.fields.routingKey); 56 | _.set(publishConfig, 'options.CC', _.chain([]).concat(publishConfig.options.CC, format('%s.%s', originalQueue, publishConfig.routingKey)).uniq().compact() 57 | .value()); 58 | 59 | _publish(message.content, publishConfig, next); 60 | }; 61 | 62 | function _publishEncrypted(buffer, publishConfig, next) { 63 | const encryptionConfig = publishConfig.encryption; 64 | encrypt(encryptionConfig.algorithm, encryptionConfig.key, encryptionConfig.ivLength, buffer, (err, iv, encrypted) => { 65 | if (err) return next(err); 66 | debug('Message was encrypted using encryption profile: %s', encryptionConfig.name); 67 | _.set(publishConfig, 'options.headers.rascal.encryption.name', encryptionConfig.name); 68 | _.set(publishConfig, 'options.headers.rascal.encryption.iv', iv); 69 | _.set(publishConfig, 'options.headers.rascal.encryption.originalContentType', publishConfig.options.contentType); 70 | _.set(publishConfig, 'options.contentType', 'application/octet-stream'); 71 | 72 | _publish(encrypted, publishConfig, next); 73 | }); 74 | } 75 | 76 | function encrypt(algorithm, keyHex, ivLength, unencrypted, next) { 77 | crypto.randomBytes(ivLength, (err, iv) => { 78 | if (err) return next(err); 79 | let encrypted; 80 | try { 81 | const key = Buffer.from(keyHex, 'hex'); 82 | const cipher = crypto.createCipheriv(algorithm, key, iv); 83 | encrypted = Buffer.concat([cipher.update(unencrypted), cipher.final()]); 84 | } catch (err) { 85 | return next(err); 86 | } 87 | next(null, iv.toString('hex'), encrypted); 88 | }); 89 | } 90 | 91 | function _publish(buffer, publishConfig, next) { 92 | const messageId = publishConfig.options.messageId; 93 | const session = new PublicationSession(vhost, messageId); 94 | borrowChannelFn((err, channel) => { 95 | session._removePausedListener(); 96 | if (err) return session.emit('error', err, messageId); 97 | if (session.isAborted()) return abortPublish(channel, messageId); 98 | 99 | const disconnectionHandler = makeDisconnectionHandler(channel, messageId, session, config); 100 | const returnHandler = session.emit.bind(session, 'return'); 101 | addListeners(channel, disconnectionHandler, returnHandler); 102 | 103 | try { 104 | session._startPublish(); 105 | 106 | publishFn(channel, buffer, publishConfig, (err, ok) => { 107 | session._endPublish(); 108 | if (err) { 109 | destroyChannel(channel, disconnectionHandler, returnHandler); 110 | return session.emit('error', err, messageId); 111 | } 112 | 113 | ok ? returnChannel(channel, disconnectionHandler, returnHandler) : deferReturnChannel(channel, disconnectionHandler, returnHandler); 114 | 115 | session.emit('success', messageId); 116 | }); 117 | } catch (err) { 118 | returnChannel(channel, disconnectionHandler, returnHandler); 119 | return session.emit('error', err, messageId); 120 | } 121 | }); 122 | 123 | next(null, session); 124 | } 125 | 126 | function abortPublish(channel, messageId) { 127 | debug('Publication of message: %s was aborted', messageId); 128 | returnChannelFn(channel); 129 | } 130 | 131 | function returnChannel(channel, disconnectionHandler, returnHandler) { 132 | removeListeners(channel, disconnectionHandler, returnHandler); 133 | returnChannelFn(channel); 134 | } 135 | 136 | function deferReturnChannel(channel, disconnectionHandler, returnHandler) { 137 | channel.once('drain', () => { 138 | returnChannel(channel, disconnectionHandler, returnHandler); 139 | }); 140 | } 141 | 142 | function destroyChannel(channel, disconnectionHandler, returnHandler) { 143 | removeListeners(channel, disconnectionHandler, returnHandler); 144 | destroyChannelFn(channel); 145 | } 146 | 147 | function getContent(payload) { 148 | if (Buffer.isBuffer(payload)) return bufferMessage(payload); 149 | if (_.isString(payload)) return textMessage(payload); 150 | return jsonMessage(payload); 151 | } 152 | 153 | function bufferMessage(payload) { 154 | return { buffer: payload, type: undefined }; 155 | } 156 | 157 | function textMessage(payload) { 158 | return { buffer: Buffer.from(payload), type: 'text/plain' }; 159 | } 160 | 161 | function jsonMessage(payload) { 162 | return { 163 | buffer: Buffer.from(JSON.stringify(payload)), 164 | type: 'application/json', 165 | }; 166 | } 167 | } 168 | 169 | function makeDisconnectionHandler(channel, messageId, session, config) { 170 | return _.once((err) => { 171 | // Use setImmediate to avoid amqplib accept loop swallowing errors 172 | setImmediate(() => (err 173 | // Treat close events with errors as error events 174 | ? handleChannelError(channel, messageId, session, config, err) 175 | : handleChannelClose(channel, messageId, session, config))); 176 | }); 177 | } 178 | 179 | function addListeners(channel, disconnectionHandler, returnHandler) { 180 | channel.on('error', disconnectionHandler); 181 | channel.on('return', returnHandler); 182 | channel.connection.once('error', disconnectionHandler); 183 | channel.connection.once('close', disconnectionHandler); 184 | } 185 | 186 | function removeListeners(channel, disconnectionHandler, returnHandler) { 187 | channel.removeAllListeners('drain'); 188 | channel.removeListener('error', disconnectionHandler); 189 | channel.removeListener('return', returnHandler); 190 | channel.connection.removeListener('error', disconnectionHandler); 191 | channel.connection.removeListener('close', disconnectionHandler); 192 | } 193 | 194 | function publishToExchange(channel, content, config, next) { 195 | debug('Publishing %d bytes to exchange: %s with routingKeys: %s', content.length, config.exchange, _.compact([].concat(config.routingKey, config.options.CC, config.options.BCC)).join(', ')); 196 | 197 | const fn = () => { 198 | return channel.publish(config.destination, config.routingKey, content, config.options); 199 | }; 200 | 201 | publishNoConfirm(fn, channel, next); 202 | } 203 | 204 | function publishToConfirmExchange(channel, content, config, next) { 205 | debug('Publishing %d bytes to confirm exchange: %s with routingKeys: %s', content.length, config.exchange, _.compact([].concat(config.routingKey, config.options.CC, config.options.BCC)).join(', ')); 206 | 207 | const fn = (cb) => { 208 | return channel.publish(config.destination, config.routingKey, content, config.options, cb); 209 | }; 210 | 211 | publishAndConfirm(fn, channel, config, next); 212 | } 213 | 214 | function sendToQueue(channel, content, config, next) { 215 | debug('Publishing %d bytes to queue: %s', content.length, config.queue); 216 | 217 | const fn = () => { 218 | return channel.sendToQueue(config.destination, content, config.options); 219 | }; 220 | 221 | publishNoConfirm(fn, channel, next); 222 | } 223 | 224 | function sendToConfirmQueue(channel, content, config, next) { 225 | debug('Publishing %d bytes to queue: %s', content.length, config.queue); 226 | 227 | const fn = (cb) => { 228 | return channel.sendToQueue(config.destination, content, config.options, cb); 229 | }; 230 | 231 | publishAndConfirm(fn, channel, config, next); 232 | } 233 | 234 | function publishNoConfirm(fn, channel, next) { 235 | let drained = false; 236 | channel.once('drain', () => { 237 | drained = true; 238 | }); 239 | 240 | const ok = fn(); 241 | next(null, ok || drained); 242 | } 243 | 244 | function publishAndConfirm(fn, channel, config, next) { 245 | const once = _.once(next); 246 | const timeout = config.timeout ? setConfirmationTimeout(config.timeout, config.destination, once) : null; 247 | let drained = false; 248 | channel.once('drain', () => { 249 | drained = true; 250 | }); 251 | 252 | const ok = fn((err) => { 253 | clearTimeout(timeout); 254 | next(err, ok || drained); 255 | }); 256 | } 257 | 258 | function setConfirmationTimeout(timeout, destination, next) { 259 | return setTimeoutUnref(() => { 260 | next(new Error(format('Timedout after %dms waiting for broker to confirm publication to: %s', timeout, destination))); 261 | }, timeout); 262 | } 263 | 264 | function handleChannelError(borked, messageId, emitter, config, err) { 265 | debug('Channel error: %s during publication of message: %s to %s using channel: %s', err.message, messageId, config.name, borked._rascal_id); 266 | emitter.emit('error', err, messageId); 267 | } 268 | 269 | function handleChannelClose(borked, messageId, emitter, config) { 270 | debug('Channel closed during publication of message: %s to %s using channel: %s', messageId, config.name, borked._rascal_id); 271 | emitter.emit('close', messageId); 272 | } 273 | -------------------------------------------------------------------------------- /lib/amqp/PublicationSession.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:SubscriberSession'); 2 | const EventEmitter = require('events').EventEmitter; 3 | const inherits = require('util').inherits; 4 | 5 | module.exports = PublicationSession; 6 | 7 | inherits(PublicationSession, EventEmitter); 8 | 9 | function PublicationSession(vhost, messageId) { 10 | const self = this; 11 | let aborted = false; 12 | 13 | this.stats = {}; 14 | 15 | this.abort = function () { 16 | aborted = true; 17 | }; 18 | 19 | this.isAborted = function () { 20 | return aborted; 21 | }; 22 | 23 | this._removePausedListener = function () { 24 | vhost.removeListener('paused', emitPaused); 25 | }; 26 | 27 | this._startPublish = function () { 28 | this.started = Date.now(); 29 | }; 30 | 31 | this._endPublish = function () { 32 | this.stats.duration = Date.now() - this.started; 33 | }; 34 | 35 | function emitPaused() { 36 | self.emit('paused', messageId); 37 | } 38 | 39 | vhost.on('paused', emitPaused); 40 | 41 | self.on('newListener', (event) => { 42 | if (event !== 'paused') return; 43 | if (vhost.isPaused()) emitPaused(); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /lib/amqp/SubscriberError.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:SubscriberError'); 2 | const format = require('util').format; 3 | const _ = require('lodash'); 4 | const async = require('async'); 5 | const setTimeoutUnref = require('../utils/setTimeoutUnref'); 6 | const { EMPTY_X_DEATH } = require('./XDeath'); 7 | 8 | module.exports = function SubscriptionRecovery(broker, vhost) { 9 | this.handle = function (session, message, err, recoveryOptions, next) { 10 | debug('Handling subscriber error for message: %s with error: %s', message.properties.messageId, err.message); 11 | 12 | async.eachSeries( 13 | [].concat(recoveryOptions || []).concat({ strategy: 'fallback-nack' }), 14 | (recoveryConfig, cb) => { 15 | debug('Attempting to recover message: %s using strategy: %s', message.properties.messageId, recoveryConfig.strategy); 16 | 17 | const once = _.once(cb); 18 | 19 | setTimeoutUnref(() => { 20 | getStrategy(recoveryConfig).execute(session, message, err, _.omit(recoveryConfig, 'defer'), (err, handled) => { 21 | if (err) { 22 | debug('Message: %s failed to be recovered using stragegy: %s', message.properties.messageId, recoveryConfig.strategy); 23 | setImmediate(() => next(err)); 24 | return once(false); 25 | } 26 | if (handled) { 27 | debug('Message: %s was recovered using stragegy: %s', message.properties.messageId, recoveryConfig.strategy); 28 | setImmediate(next); 29 | return once(false); 30 | } 31 | once(); 32 | }); 33 | }, recoveryConfig.defer); 34 | }, 35 | next, 36 | ); 37 | }; 38 | 39 | const recoveryStrategies = _.keyBy( 40 | [ 41 | { 42 | name: 'ack', 43 | execute(session, message, err, strategyConfig, next) { 44 | const ackFn = strategyConfig.all ? session._nackAll.bind(session) : session._nack.bind(session); 45 | ackFn(message, (err) => { 46 | next(err, true); 47 | }); 48 | }, 49 | }, 50 | { 51 | name: 'nack', 52 | execute(session, message, err, strategyConfig, next) { 53 | const nackFn = strategyConfig.all ? session._nackAll.bind(session) : session._nack.bind(session); 54 | nackFn(message, { requeue: strategyConfig.requeue }, (err) => { 55 | next(err, true); 56 | }); 57 | }, 58 | }, 59 | { 60 | name: 'fallback-nack', 61 | execute(session, message, err, strategyConfig, next) { 62 | session._nack(message, { requeue: strategyConfig.requeue }, (err) => { 63 | next(err, true); 64 | }); 65 | }, 66 | }, 67 | { 68 | name: 'republish', 69 | execute(session, message, err, strategyConfig, next) { 70 | debug('Republishing message: %s', message.properties.messageId); 71 | 72 | const originalQueue = _.get(message, 'properties.headers.rascal.originalQueue'); 73 | const republished = _.get(message, ['properties', 'headers', 'rascal', 'recovery', originalQueue, 'republished'], 0); 74 | 75 | if (strategyConfig.attempts && strategyConfig.attempts <= republished) { 76 | debug('Skipping recovery - message: %s has already been republished %d times.', message.properties.messageId, republished); 77 | return next(null, false); 78 | } 79 | 80 | const publishOptions = _.cloneDeep(message.properties); 81 | _.set(publishOptions, ['headers', 'rascal', 'recovery', originalQueue, 'republished'], republished + 1); 82 | _.set(publishOptions, 'headers.rascal.originalExchange', message.fields.exchange); 83 | _.set(publishOptions, 'headers.rascal.originalRoutingKey', message.fields.routingKey); 84 | _.set(publishOptions, 'headers.rascal.error.message', _.truncate(err.message, { length: 1024 })); 85 | _.set(publishOptions, 'headers.rascal.error.code', err.code); 86 | _.set(publishOptions, 'headers.rascal.restoreRoutingHeaders', _.has(strategyConfig, 'restoreRoutingHeaders') ? strategyConfig.restoreRoutingHeaders : true); 87 | 88 | if (strategyConfig.immediateNack) { 89 | const xDeathRecords = message.properties.headers['x-death'] || []; 90 | const xDeath = xDeathRecords.find(({ queue, reason }) => queue === originalQueue && reason === 'rejected') || EMPTY_X_DEATH; 91 | _.set(publishOptions, ['headers', 'rascal', 'recovery', originalQueue], { immediateNack: true, xDeath }); 92 | } 93 | 94 | const ackMessage = () => { 95 | session._ack(message, (err) => { 96 | next(err, true); 97 | }); 98 | }; 99 | 100 | const nackMessage = (err) => { 101 | session._nack(message, (_nackErr) => { 102 | // nackError just means the channel was already closed meaning the original message would have been rolled back 103 | next(err); 104 | }); 105 | }; 106 | 107 | const ackOrNack = _.once((err) => { 108 | return err ? nackMessage(err) : ackMessage(); 109 | }); 110 | 111 | vhost.getConfirmChannel((err, publisherChannel) => { 112 | if (err) return ackOrNack(err); 113 | 114 | if (!publisherChannel) return ackOrNack(new Error('Unable to handle subscriber error by republishing. The VHost is shutting down')); 115 | 116 | publisherChannel.on('error', (err) => { 117 | ackOrNack(err); 118 | }); 119 | 120 | publisherChannel.on('return', () => { 121 | ackOrNack(new Error(format('Message: %s was republished to queue: %s, but was returned', message.properties.messageId, originalQueue))); 122 | }); 123 | 124 | publisherChannel.publish(undefined, originalQueue, message.content, publishOptions, (err) => { 125 | if (err) { 126 | // Channel will already be closed, reclosing will trigger an error 127 | publisherChannel.removeAllListeners(); 128 | 129 | debug('Message: %s failed to be republished to queue: %s %d times - %s', message.properties.messageId, originalQueue, republished + 1, err.message); 130 | ackOrNack(err); 131 | } else { 132 | publisherChannel.close(); 133 | publisherChannel.removeAllListeners(); 134 | 135 | debug('Message: %s was republished to queue: %s %d times', message.properties.messageId, originalQueue, republished + 1); 136 | ackOrNack(); 137 | } 138 | }); 139 | }); 140 | }, 141 | }, 142 | { 143 | name: 'forward', 144 | execute(session, message, err, strategyConfig, next) { 145 | debug('Forwarding message: %s to publication: %s', message.properties.messageId, strategyConfig.publication); 146 | 147 | const originalQueue = _.get(message, 'properties.headers.rascal.originalQueue'); 148 | const forwarded = _.get(message, ['properties', 'headers', 'rascal', 'recovery', originalQueue, 'forwarded'], 0); 149 | 150 | if (strategyConfig.attempts && strategyConfig.attempts <= forwarded) { 151 | debug('Skipping recovery - message: %s has already been forwarded %d times.', message.properties.messageId, forwarded); 152 | return next(null, false); 153 | } 154 | 155 | // See https://github.com/rabbitmq/rabbitmq-server/issues/161 156 | if (strategyConfig.xDeathFix) delete message.properties.headers['x-death']; 157 | 158 | const forwardOverrides = _.cloneDeep(strategyConfig.options) || {}; 159 | _.set(forwardOverrides, 'restoreRoutingHeaders', _.has(strategyConfig, 'restoreRoutingHeaders') ? strategyConfig.restoreRoutingHeaders : true); 160 | _.set(forwardOverrides, ['options', 'headers', 'rascal', 'recovery', originalQueue, 'forwarded'], forwarded + 1); 161 | _.set(forwardOverrides, 'options.headers.rascal.error.message', _.truncate(err.message, { length: 1024 })); 162 | _.set(forwardOverrides, 'options.headers.rascal.error.code', err.code); 163 | 164 | const ackMessage = () => { 165 | session._ack(message, (err) => { 166 | next(err, true); 167 | }); 168 | }; 169 | 170 | const nackMessage = (err) => { 171 | session._nack(message, (_nackErr) => { 172 | // nackError just means the channel was already closed meaning the original message would have been rolled back 173 | next(err); 174 | }); 175 | }; 176 | 177 | const ackOrNack = _.once((err) => { 178 | return err ? nackMessage(err) : ackMessage(); 179 | }); 180 | 181 | broker.forward(strategyConfig.publication, message, forwardOverrides, (err, publication) => { 182 | if (err) return nackMessage(err); 183 | 184 | publication.on('success', () => { 185 | debug('Message: %s was forwarded to publication: %s %d times', message.properties.messageId, strategyConfig.publication, forwarded + 1); 186 | ackOrNack(); 187 | }); 188 | 189 | publication.on('error', (err) => { 190 | debug('Message: %s failed to be forwarded to publication: %s %d times - %s', message.properties.messageId, strategyConfig.publication, forwarded + 1, err.message); 191 | ackOrNack(err); 192 | }); 193 | 194 | publication.on('return', () => { 195 | publication.removeAllListeners('success'); 196 | ackOrNack(new Error(format('Message: %s was forwarded to publication: %s, but was returned', message.properties.messageId, strategyConfig.publication))); 197 | }); 198 | }); 199 | }, 200 | }, 201 | { 202 | name: 'unknown', 203 | execute(session, message, err, strategyConfig, next) { 204 | session._nack(message, () => { 205 | next(new Error(format('Error recovering message: %s. No such strategy: %s.', message.properties.messageId, strategyConfig.strategy))); 206 | }); 207 | }, 208 | }, 209 | ], 210 | 'name', 211 | ); 212 | 213 | function getStrategy(recoveryConfig) { 214 | return recoveryStrategies[recoveryConfig.strategy] || recoveryStrategies.unknown; 215 | } 216 | }; 217 | -------------------------------------------------------------------------------- /lib/amqp/SubscriberSession.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:SubscriberSession'); 2 | const EventEmitter = require('events').EventEmitter; 3 | const inherits = require('util').inherits; 4 | const _ = require('lodash'); 5 | const async = require('async'); 6 | const setTimeoutUnref = require('../utils/setTimeoutUnref'); 7 | 8 | module.exports = SubscriberSession; 9 | 10 | inherits(SubscriberSession, EventEmitter); 11 | 12 | function SubscriberSession(sequentialChannelOperations, config) { 13 | let index = 0; 14 | const channels = {}; 15 | let cancelled = false; 16 | let timeout; 17 | const self = this; 18 | 19 | this.name = config.name; 20 | this.config = _.cloneDeep(config); 21 | 22 | this.isCancelled = function () { 23 | return cancelled; 24 | }; 25 | 26 | this._open = function (channel, consumerTag, next) { 27 | if (cancelled) return next(new Error('Subscriber has been cancelled')); 28 | debug('Opening subscriber session: %s on channel: %s', consumerTag, channel._rascal_id); 29 | channels[consumerTag] = { 30 | index: index++, channel, consumerTag, unacknowledgedMessages: 0, 31 | }; 32 | channel.once('close', unref.bind(null, consumerTag)); 33 | channel.once('error', unref.bind(null, consumerTag)); 34 | next(); 35 | }; 36 | 37 | this.cancel = function (next) { 38 | clearTimeout(timeout); 39 | sequentialChannelOperations.push((done) => { 40 | cancelled = true; 41 | self._unsafeClose(done); 42 | }, next); 43 | }; 44 | 45 | this.setChannelPrefetch = function (prefetch, next) { 46 | sequentialChannelOperations.push((done) => { 47 | config.channelPrefetch = prefetch; 48 | withCurrentChannel( 49 | (channel) => { 50 | debug('Setting channel prefetch to %d on channel: %s', prefetch, channel._rascal_id); 51 | channel.prefetch(prefetch, true, done); 52 | }, 53 | () => { 54 | debug('No current channel on which to set prefetch'); 55 | done(); 56 | }, 57 | ); 58 | }, next); 59 | }; 60 | 61 | this._close = function (next) { 62 | sequentialChannelOperations.push((done) => { 63 | self._unsafeClose(done); 64 | }, next); 65 | }; 66 | 67 | this._unsafeClose = function (next) { 68 | withCurrentChannel( 69 | (channel, consumerTag, entry) => { 70 | entry.doomed = true; 71 | debug('Cancelling subscriber session: %s on channel: %s', consumerTag, channel._rascal_id); 72 | channel.cancel(consumerTag, (err) => { 73 | if (err) return next(err); 74 | const waitOrTimeout = config.closeTimeout ? async.timeout(waitForUnacknowledgedMessages, config.closeTimeout) : waitForUnacknowledgedMessages; 75 | waitOrTimeout(entry, null, (err) => { 76 | channel.close(() => { 77 | debug('Channel: %s was closed', entry.channel._rascal_id); 78 | next(err); 79 | }); 80 | }); 81 | }); 82 | }, 83 | () => { 84 | debug('No current channel to close'); 85 | next(); 86 | }, 87 | ); 88 | }; 89 | 90 | this._schedule = function (fn, delay) { 91 | timeout = setTimeoutUnref(fn, delay); 92 | }; 93 | 94 | this._getRascalChannelId = function () { 95 | let rascalChannelId = null; 96 | withCurrentChannel((channel) => { 97 | rascalChannelId = channel._rascal_id; 98 | }); 99 | return rascalChannelId; 100 | }; 101 | 102 | this._incrementUnacknowledgeMessageCount = function (consumerTag) { 103 | if (config.options.noAck) return; 104 | withConsumerChannel(consumerTag, (channel, __, entry) => { 105 | debug('Channel: %s has %s unacknowledged messages', channel._rascal_id, ++entry.unacknowledgedMessages); 106 | }); 107 | }; 108 | 109 | this._decrementUnacknowledgeMessageCount = function (consumerTag) { 110 | if (config.options.noAck) return; 111 | withConsumerChannel(consumerTag, (channel, __, entry) => { 112 | debug('Channel: %s has %s unacknowledged messages', channel._rascal_id, --entry.unacknowledgedMessages); 113 | }); 114 | }; 115 | 116 | this._resetUnacknowledgedMessageCount = function (consumerTag) { 117 | if (config.options.noAck) return; 118 | withConsumerChannel(consumerTag, (channel, __, entry) => { 119 | entry.unacknowledgedMessages = 0; 120 | debug('Channel: %s has %s unacknowledged messages', channel._rascal_id, entry.unacknowledgedMessages); 121 | }); 122 | }; 123 | 124 | this._ack = function (message, next) { 125 | withConsumerChannel( 126 | message.fields.consumerTag, 127 | (channel) => { 128 | debug('Acknowledging message: %s on channel: %s', message.properties.messageId, channel._rascal_id); 129 | channel.ack(message); 130 | self._decrementUnacknowledgeMessageCount(message.fields.consumerTag); 131 | setImmediate(next); 132 | }, 133 | () => { 134 | setImmediate(() => { 135 | next(new Error('The channel has been closed. Unable to ack message')); 136 | }); 137 | }, 138 | ); 139 | }; 140 | 141 | this._ackAll = function (message, next) { 142 | withConsumerChannel( 143 | message.fields.consumerTag, 144 | (channel) => { 145 | debug('Acknowledging all messages on channel: %s', message.properties.messageId, channel._rascal_id); 146 | channel.ackAll(); 147 | self._resetUnacknowledgedMessageCount(message.fields.consumerTag); 148 | setImmediate(next); 149 | }, 150 | () => { 151 | setImmediate(() => { 152 | next(new Error('The channel has been closed. Unable to ack messages')); 153 | }); 154 | }, 155 | ); 156 | }; 157 | 158 | this._nack = function (message, options, next) { 159 | if (arguments.length === 2) return self._nack(arguments[0], {}, arguments[1]); 160 | withConsumerChannel( 161 | message.fields.consumerTag, 162 | (channel) => { 163 | debug('Not acknowledging message: %s with requeue: %s on channel: %s', message.properties.messageId, !!options.requeue, channel._rascal_id); 164 | channel.nack(message, false, !!options.requeue); 165 | self._decrementUnacknowledgeMessageCount(message.fields.consumerTag); 166 | setImmediate(next); 167 | }, 168 | () => { 169 | setImmediate(() => { 170 | next(new Error('The channel has been closed. Unable to nack message')); 171 | }); 172 | }, 173 | ); 174 | }; 175 | 176 | this._nackAll = function (message, options, next) { 177 | if (arguments.length === 2) return self._nackAll(arguments[0], {}, arguments[1]); 178 | withConsumerChannel( 179 | message.fields.consumerTag, 180 | (channel) => { 181 | debug('Not acknowledging all messages with requeue: %s on channel: %s', message.properties.messageId, !!options.requeue, channel._rascal_id); 182 | channel.nack(message, true, !!options.requeue); 183 | self._resetUnacknowledgedMessageCount(message.fields.consumerTag); 184 | setImmediate(next); 185 | }, 186 | () => { 187 | setImmediate(() => { 188 | next(new Error('The channel has been closed. Unable to nack messages')); 189 | }); 190 | }, 191 | ); 192 | }; 193 | 194 | function withCurrentChannel(fn, altFn) { 195 | const entry = _.chain(channels) 196 | .values() 197 | .filter((e) => !e.doomed) 198 | .sortBy('index') 199 | .last() 200 | .value(); 201 | if (entry) return fn(entry.channel, entry.consumerTag, entry); 202 | return altFn && altFn(); 203 | } 204 | 205 | function withConsumerChannel(consumerTag, fn, altFn) { 206 | const entry = channels[consumerTag]; 207 | if (entry) return fn(entry.channel, entry.consumerTag, entry); 208 | return altFn && altFn(); 209 | } 210 | 211 | function unref(consumerTag) { 212 | withConsumerChannel(consumerTag, (channel) => { 213 | debug('Removing channel: %s from session', channel._rascal_id); 214 | delete channels[consumerTag]; 215 | }); 216 | } 217 | 218 | function waitForUnacknowledgedMessages(entry, previousCount, next) { 219 | const currentCount = entry.unacknowledgedMessages; 220 | if (currentCount > 0) { 221 | if (currentCount !== previousCount) { 222 | debug('Waiting for %d unacknowledged messages from channel: %s', currentCount, entry.channel._rascal_id); 223 | } 224 | setTimeoutUnref(() => waitForUnacknowledgedMessages(entry, currentCount, next), 100); 225 | return; 226 | } 227 | next(); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /lib/amqp/SubscriberSessionAsPromised.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const inherits = require('util').inherits; 3 | const forwardEvents = require('forward-emitter'); 4 | 5 | module.exports = SubscriberSessionAsPromised; 6 | 7 | inherits(SubscriberSessionAsPromised, EventEmitter); 8 | 9 | function SubscriberSessionAsPromised(session) { 10 | forwardEvents(session, this); 11 | 12 | this.name = session.name; 13 | this.config = session.config; 14 | 15 | this.cancel = function () { 16 | return new Promise((resolve, reject) => { 17 | session.cancel((err) => { 18 | if (err) return reject(err); 19 | resolve(); 20 | }); 21 | }); 22 | }; 23 | 24 | this.setChannelPrefetch = function (prefetch) { 25 | return new Promise((resolve, reject) => { 26 | session.setChannelPrefetch(prefetch, (err) => { 27 | if (err) return reject(err); 28 | resolve(); 29 | }); 30 | }); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /lib/amqp/Subscription.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:Subscription'); 2 | const _ = require('lodash'); 3 | const format = require('util').format; 4 | const crypto = require('crypto'); 5 | const async = require('async'); 6 | const safeParse = require('safe-json-parse/callback'); 7 | const SubscriberSession = require('./SubscriberSession'); 8 | const SubscriberError = require('./SubscriberError'); 9 | const backoff = require('../backoff'); 10 | const setTimeoutUnref = require('../utils/setTimeoutUnref'); 11 | const { EMPTY_X_DEATH } = require('./XDeath'); 12 | 13 | module.exports = { 14 | create(broker, vhost, counter, config, next) { 15 | return new Subscription(broker, vhost, config, counter).init(next); 16 | }, 17 | }; 18 | 19 | function Subscription(broker, vhost, subscriptionConfig, counter) { 20 | const timer = backoff(subscriptionConfig.retry); 21 | const subscriberError = new SubscriberError(broker, vhost); 22 | const sequentialChannelOperations = async.queue((task, next) => { 23 | task(next); 24 | }, 1); 25 | const self = this; 26 | 27 | this.name = subscriptionConfig.name; 28 | 29 | this.init = function (next) { 30 | debug('Initialising subscription: %s', subscriptionConfig.name); 31 | return next(null, self); 32 | }; 33 | 34 | this.subscribe = function (overrides, next) { 35 | const config = _.defaultsDeep(overrides, subscriptionConfig); 36 | const session = new SubscriberSession(sequentialChannelOperations, config); 37 | subscribeLater(session, config); 38 | return next(null, session); 39 | }; 40 | 41 | function subscribeLater(session, config) { 42 | session.on('newListener', (event) => { 43 | if (event !== 'message') return; 44 | subscribeNow(session, config, (err) => { 45 | if (err) return session.emit('error', err); 46 | session.emit('subscribed'); 47 | }); 48 | }); 49 | } 50 | 51 | function subscribeNow(session, config, next) { 52 | sequentialChannelOperations.push((done) => { 53 | if (session.isCancelled()) { 54 | debug('Subscription to queue: %s has been cancelled', config.queue); 55 | return done(); 56 | } 57 | debug('Subscribing to queue: %s', config.queue); 58 | vhost.getChannel((err, channel) => { 59 | if (err) return done(err); 60 | if (!channel) return done(); 61 | 62 | _configureQos(config, channel, (err) => { 63 | if (err) return done(err); 64 | 65 | const removeDisconnectionHandlers = attachDisconnectionHandlers(channel, session, config); 66 | const onMessage = _onMessage.bind(null, session, config, removeDisconnectionHandlers); 67 | 68 | channel.consume(config.source, onMessage, config.options, (err, response) => { 69 | if (err) { 70 | debug('Error subscribing to %s using channel: %s. %s', config.source, channel._rascal_id, err.message); 71 | removeDisconnectionHandlers(); 72 | return done(err); 73 | } 74 | session._open(channel, response.consumerTag, (err) => { 75 | if (err) return done(err); 76 | timer.reset(); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | }); 82 | }, next); 83 | } 84 | 85 | function _configureQos(config, channel, next) { 86 | const qos = []; 87 | if (config.prefetch) qos.push((cb) => channel.prefetch(config.prefetch, false, cb)); 88 | if (config.channelPrefetch) qos.push((cb) => channel.prefetch(config.channelPrefetch, true, cb)); 89 | async.series(qos, next); 90 | } 91 | 92 | function _onMessage(session, config, removeDisconnectionHandlers, message) { 93 | if (!message) return handleConsumerCancel(session, config, removeDisconnectionHandlers); 94 | 95 | debug('Received message: %s from queue: %s', message.properties.messageId, config.queue); 96 | session._incrementUnacknowledgeMessageCount(message.fields.consumerTag); 97 | 98 | decorateWithRoutingHeaders(message); 99 | if (immediateNack(message)) { 100 | debug('Immediately nacking message: %s from queue: %s', message.properties.messageId, config.queue); 101 | ackOrNack(session, message, new Error('Immediate nack')); 102 | return; 103 | } 104 | 105 | decorateWithRedeliveries(message, (err) => { 106 | if (err) return handleRedeliveriesError(err, session, message); 107 | if (redeliveriesExceeded(message)) return handleRedeliveriesExceeded(session, message); 108 | 109 | getContent(message, config, (err, content) => { 110 | err ? handleContentError(session, message, err) : session.emit('message', message, content, getAckOrNack(session, message)); 111 | }); 112 | }); 113 | } 114 | 115 | function getContent(message, config, next) { 116 | if (message.properties.headers.rascal.encryption) { 117 | const encryptionConfig = config.encryption[message.properties.headers.rascal.encryption.name]; 118 | if (!encryptionConfig) return next(new Error(format('Unknown encryption profile: %s', message.properties.headers.rascal.encryption.name))); 119 | decrypt(encryptionConfig.algorithm, encryptionConfig.key, message.properties.headers.rascal.encryption.iv, message.content, (err, unencrypted) => { 120 | if (err) return next(err); 121 | debug('Message was decrypted using encryption profile: %s', message.properties.headers.rascal.encryption.name); 122 | const contentType = config.contentType || message.properties.headers.rascal.encryption.originalContentType; 123 | negotiateContent(contentType, unencrypted, next); 124 | }); 125 | } else { 126 | const contentType = config.contentType || message.properties.contentType; 127 | negotiateContent(contentType, message.content, next); 128 | } 129 | } 130 | 131 | function negotiateContent(contentType, content, next) { 132 | if (contentType === 'text/plain') return next(null, content.toString()); 133 | if (contentType === 'application/json') return safeParse(content.toString(), next); 134 | return next(null, content); 135 | } 136 | 137 | function decrypt(algorithm, keyHex, ivHex, encrypted, next) { 138 | let unencrypted; 139 | try { 140 | const key = Buffer.from(keyHex, 'hex'); 141 | const iv = Buffer.from(ivHex, 'hex'); 142 | const cipher = crypto.createDecipheriv(algorithm, key, iv); 143 | unencrypted = Buffer.concat([cipher.update(encrypted), cipher.final()]); 144 | } catch (err) { 145 | return next(err); 146 | } 147 | next(null, unencrypted); 148 | } 149 | 150 | function handleContentError(session, message, err) { 151 | debug('Error getting content for message %s: %s', message.properties.messageId, err.message); 152 | // Documentation wrongly specified 'invalid_content' instead of 'invalid_message' emitting both 153 | if (session.emit('invalid_content', err, message, getAckOrNack(session, message))) return; 154 | if (session.emit('invalid_message', err, message, getAckOrNack(session, message))) return; 155 | nackAndError(session, message, err); 156 | } 157 | 158 | function redeliveriesExceeded(message) { 159 | return message.properties.headers.rascal.redeliveries > subscriptionConfig.redeliveries.limit; 160 | } 161 | 162 | function handleRedeliveriesError(err, session, message) { 163 | debug('Error handling redeliveries of message %s: %s', message.properties.messageId, err.message); 164 | if (session.emit('redeliveries_error', err, message, getAckOrNack(session, message))) return; 165 | if (session.emit('redeliveries_exceeded', err, message, getAckOrNack(session, message))) return; 166 | nackAndError(session, message, err); 167 | } 168 | 169 | function handleRedeliveriesExceeded(session, message) { 170 | const err = new Error(format('Message %s has exceeded %d redeliveries', message.properties.messageId, subscriptionConfig.redeliveries.limit)); 171 | debug(err.message); 172 | if (session.emit('redeliveries_exceeded', err, message, getAckOrNack(session, message))) return; 173 | if (session.emit('redeliveries_error', err, message, getAckOrNack(session, message))) return; 174 | nackAndError(session, message, err); 175 | } 176 | 177 | function nackAndError(session, message, err) { 178 | ackOrNack(session, message, err, () => { 179 | // Using setTimeout rather than process.nextTick as the latter fires before any IO. 180 | // If the app shuts down before the IO has completed, the message will be rolled back 181 | setTimeoutUnref(session.emit.bind(session, 'error', err)); 182 | }); 183 | } 184 | 185 | function decorateWithRoutingHeaders(message) { 186 | message.properties.headers = message.properties.headers || {}; 187 | message.properties.headers.rascal = message.properties.headers.rascal || {}; 188 | message.properties.headers.rascal.originalQueue = subscriptionConfig.source; 189 | message.properties.headers.rascal.originalVhost = vhost.name; 190 | 191 | if (!message.properties.headers.rascal.restoreRoutingHeaders) return; 192 | if (message.properties.headers.rascal.originalRoutingKey) message.fields.routingKey = message.properties.headers.rascal.originalRoutingKey; 193 | if (message.properties.headers.rascal.originalExchange) message.fields.exchange = message.properties.headers.rascal.originalExchange; 194 | } 195 | 196 | function decorateWithRedeliveries(message, next) { 197 | const once = _.once(next); 198 | const timeout = setTimeoutUnref(() => { 199 | once(new Error(format('Redeliveries timed out after %dms', subscriptionConfig.redeliveries.timeout))); 200 | }, subscriptionConfig.redeliveries.timeout); 201 | countRedeliveries(message, (err, redeliveries) => { 202 | clearTimeout(timeout); 203 | if (err) return once(err); 204 | message.properties.headers.rascal.redeliveries = redeliveries; 205 | once(); 206 | }); 207 | } 208 | 209 | function countRedeliveries(message, next) { 210 | if (!message.fields.redelivered) return next(null, 0); 211 | if (!message.properties.messageId) return next(null, 0); 212 | counter.incrementAndGet(`${subscriptionConfig.name}/${message.properties.messageId}`, next); 213 | } 214 | 215 | function immediateNack(message) { 216 | const originalQueue = message.properties.headers.rascal.originalQueue; 217 | const xDeathRecords = message.properties.headers['x-death'] || []; 218 | const currentXDeath = xDeathRecords.find(({ queue, reason }) => queue === originalQueue && reason === 'rejected') || EMPTY_X_DEATH; 219 | const previousXDeath = _.get(message, ['properties', 'headers', 'rascal', 'recovery', originalQueue, 'xDeath'], EMPTY_X_DEATH); 220 | const hasImmediateNackHeader = _.has(message, ['properties', 'headers', 'rascal', 'recovery', originalQueue, 'immediateNack']); 221 | if (!hasImmediateNackHeader) return false; 222 | debug('Message %s has been marked for immediate nack. Previous xDeath is %o. Current xDeath is %o.', message.properties.messageId, previousXDeath, currentXDeath); 223 | // See https://github.com/rabbitmq/rabbitmq-server/issues/11331 224 | // RabbitMQ v3.13 stopped updating the xDeath record's count property. 225 | // RabbitMQ v3.12 does not update the xDeath record's time property. 226 | // Therefore having test them both 227 | if (currentXDeath.count > previousXDeath.count || currentXDeath.time.value > previousXDeath.time.value) { 228 | debug('Message %s has been replayed after being dead lettered. Removing immediate nack.', message.properties.messageId); 229 | _.unset(message, ['properties', 'headers', 'rascal', 'recovery', originalQueue, 'immediateNack']); 230 | _.unset(message, ['properties', 'headers', 'rascal', 'recovery', originalQueue, 'xDeath']); 231 | return false; 232 | } 233 | return true; 234 | } 235 | 236 | function getAckOrNack(session, message) { 237 | return broker.promises && subscriptionConfig.promisifyAckOrNack ? ackOrNackP.bind(null, session, message) : ackOrNack.bind(null, session, message); 238 | } 239 | 240 | function ackOrNack(session, message, err, options, next) { 241 | if (arguments.length === 2) return ackOrNack(session, message, undefined, undefined, emitOnError.bind(null, session)); 242 | if (arguments.length === 3 && _.isFunction(arguments[2])) return ackOrNack(session, message, undefined, undefined, arguments[2]); 243 | if (arguments.length === 3) return ackOrNack(session, message, err, undefined, emitOnError.bind(null, session)); 244 | if (arguments.length === 4 && _.isFunction(arguments[3])) return ackOrNack(session, message, err, undefined, arguments[3]); 245 | if (arguments.length === 4) return ackOrNack(session, message, err, options, emitOnError.bind(null, session)); 246 | 247 | if (message.__rascal_acknowledged) return next(new Error('ackOrNack should only be called once per message')); 248 | message.__rascal_acknowledged = true; 249 | 250 | if (err) return subscriberError.handle(session, message, err, options, next); 251 | if (options && options.all) return session._ackAll(message, next); 252 | session._ack(message, next); 253 | } 254 | 255 | function ackOrNackP(session, message, err, options) { 256 | if (arguments.length === 2) return ackOrNackP(session, message, undefined, undefined); 257 | if (arguments.length === 3) return ackOrNackP(session, message, err, undefined); 258 | 259 | return new Promise((resolve, reject) => { 260 | const next = function (err) { 261 | err ? reject(err) : resolve(); 262 | }; 263 | if (err) subscriberError.handle(session, message, err, options, next); 264 | else if (options && options.all) session._ackAll(message, next); 265 | else session._ack(message, next); 266 | }); 267 | } 268 | 269 | function emitOnError(session, err) { 270 | if (err) session.emit('error', err); 271 | } 272 | 273 | function attachDisconnectionHandlers(channel, session, config) { 274 | /* eslint-disable no-use-before-define */ 275 | const connection = channel.connection; 276 | const removeDisconnectionHandlers = _.once(() => { 277 | channel.removeListener('error', disconnectionHandler); 278 | channel.on('error', (err) => { 279 | debug('Suppressing error on cancelled session: %s to prevent connection errors. %s', channel._rascal_id, err.message); 280 | }); 281 | connection.removeListener('error', disconnectionHandler); 282 | connection.removeListener('close', disconnectionHandler); 283 | }); 284 | 285 | const disconnectionHandler = makeDisconnectionHandler(session, config, removeDisconnectionHandlers); 286 | channel.on('error', disconnectionHandler); 287 | connection.once('error', disconnectionHandler); 288 | connection.once('close', disconnectionHandler); 289 | return removeDisconnectionHandlers; 290 | } 291 | 292 | function makeDisconnectionHandler(session, config, removeDisconnectionHandlers) { 293 | return _.once((err) => { 294 | // Use setImmediate to avoid amqplib accept loop swallowing errors 295 | setImmediate(() => (err 296 | // Treat close events with errors as error events 297 | ? handleChannelError(session, config, removeDisconnectionHandlers, 0, err) 298 | : handleChannelClose(session, config, removeDisconnectionHandlers, 0))); 299 | }); 300 | } 301 | 302 | function handleChannelError(session, config, removeDisconnectionHandler, attempt, err) { 303 | debug('Handling channel error: %s from %s using channel: %s', err.message, config.name, session._getRascalChannelId()); 304 | if (removeDisconnectionHandler) removeDisconnectionHandler(); 305 | session.emit('error', err); 306 | retrySubscription(session, config, attempt + 1); 307 | } 308 | 309 | function handleChannelClose(session, config, removeDisconnectionHandler, attempt) { 310 | debug('Handling channel close from %s using channel: %s', config.name, session._getRascalChannelId()); 311 | removeDisconnectionHandler(); 312 | session.emit('close'); 313 | retrySubscription(session, config, attempt + 1); 314 | } 315 | 316 | function handleConsumerCancel(session, config, removeDisconnectionHandler) { 317 | debug('Received consumer cancel from %s using channel: %s', config.name, session._getRascalChannelId()); 318 | removeDisconnectionHandler(); 319 | const cancelErr = new Error(format('Subscription: %s was cancelled by the broker', config.name)); 320 | session.emit('cancelled', cancelErr) || session.emit('cancel', cancelErr) || session.emit('error', cancelErr); 321 | session._close((err) => { 322 | if (err) debug('Error cancelling subscription: %s', err.message); 323 | retrySubscription(session, config, 1); 324 | }); 325 | } 326 | 327 | function retrySubscription(session, config, attempt) { 328 | config.retry && subscribeNow(session, config, (err) => { 329 | if (!err) return; 330 | const delay = timer.next(); 331 | debug('Will attempt resubscription(%d) to %s in %dms', attempt, config.name, delay); 332 | session._schedule(handleChannelError.bind(null, session, config, null, attempt, err), delay); 333 | }); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /lib/amqp/XDeath.js: -------------------------------------------------------------------------------- 1 | const EMPTY_X_DEATH = { count: 0, time: { value: 0 } }; 2 | 3 | module.exports = { 4 | EMPTY_X_DEATH, 5 | }; 6 | -------------------------------------------------------------------------------- /lib/amqp/tasks/applyBindings.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:applyBindings'); 2 | const format = require('util').format; 3 | const _ = require('lodash'); 4 | const async = require('async'); 5 | 6 | module.exports = _.curry((config, ctx, next) => { 7 | const bind = { 8 | queue: bindQueue, 9 | exchange: bindExchange, 10 | }; 11 | 12 | async.eachOfLimit( 13 | _.values(config.bindings), 14 | config.concurrency, 15 | (binding, index, cb) => { 16 | const channel = ctx.channels[index % config.concurrency]; 17 | bind[binding.destinationType](config, channel, binding, cb); 18 | }, 19 | (err) => { 20 | next(err, config, ctx); 21 | }, 22 | ); 23 | }); 24 | 25 | function bindQueue(config, channel, binding, next) { 26 | const destination = config.queues[binding.destination]; 27 | if (!destination) return next(new Error(format('Unknown destination: %s', binding.destination))); 28 | 29 | const source = config.exchanges[binding.source]; 30 | if (!source) return next(new Error(format('Unknown source: %s', binding.source))); 31 | 32 | debug('Binding queue: %s to exchange: %s with binding key: %s', destination.fullyQualifiedName, source.fullyQualifiedName, binding.bindingKey); 33 | channel.bindQueue(destination.fullyQualifiedName, source.fullyQualifiedName, binding.bindingKey, binding.options, next); 34 | } 35 | 36 | function bindExchange(config, channel, binding, next) { 37 | const destination = config.exchanges[binding.destination]; 38 | if (!destination) return next(new Error(format('Unknown destination: %s', binding.destination))); 39 | 40 | const source = config.exchanges[binding.source]; 41 | if (!source) return next(new Error(format('Unknown source: %s', binding.source))); 42 | 43 | debug('Binding exchange: %s to exchange: %s with binding key: %s', destination.fullyQualifiedName, source.fullyQualifiedName, binding.bindingKey); 44 | channel.bindExchange(destination.fullyQualifiedName, source.fullyQualifiedName, binding.bindingKey, binding.options, next); 45 | } 46 | -------------------------------------------------------------------------------- /lib/amqp/tasks/assertExchanges.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:assertExchanges'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | async.eachOfLimit( 7 | _.keys(config.exchanges), 8 | config.concurrency, 9 | (name, index, cb) => { 10 | const channel = ctx.channels[index % config.concurrency]; 11 | assertExchange(channel, config.exchanges[name], cb); 12 | }, 13 | (err) => { 14 | next(err, config, ctx); 15 | }, 16 | ); 17 | }); 18 | 19 | function assertExchange(channel, config, next) { 20 | if (!config.assert) return next(); 21 | if (config.fullyQualifiedName === '') return next(); 22 | debug('Asserting exchange: %s', config.fullyQualifiedName); 23 | channel.assertExchange(config.fullyQualifiedName, config.type, config.options, (err) => { 24 | if (err) return next(err); 25 | next(); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /lib/amqp/tasks/assertQueues.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:assertQueues'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | async.eachOfLimit( 7 | _.keys(config.queues), 8 | config.concurrency, 9 | (name, index, cb) => { 10 | const channel = ctx.channels[index % config.concurrency]; 11 | assertQueue(channel, config.queues[name], cb); 12 | }, 13 | (err) => { 14 | next(err, config, ctx); 15 | }, 16 | ); 17 | }); 18 | 19 | function assertQueue(channel, config, next) { 20 | if (!config.assert) return next(); 21 | debug('Asserting queue: %s', config.fullyQualifiedName); 22 | channel.assertQueue(config.fullyQualifiedName, config.options, next); 23 | } 24 | -------------------------------------------------------------------------------- /lib/amqp/tasks/assertVhost.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:assertVhost'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | const Client = require('../../management/Client'); 5 | 6 | module.exports = _.curry((config, ctx, next) => { 7 | if (!config.assert) return next(null, config, ctx); 8 | 9 | const candidates = config.connections; 10 | const client = new Client(ctx.components.agent); 11 | 12 | async.retry( 13 | candidates.length, 14 | (cb) => { 15 | const connectionConfig = candidates[ctx.connectionIndex]; 16 | client.assertVhost(config.name, connectionConfig.management, (err) => { 17 | if (err) { 18 | ctx.connectionIndex = (ctx.connectionIndex + 1) % candidates.length; 19 | return cb(err); 20 | } 21 | ctx.connectionConfig = connectionConfig; 22 | cb(); 23 | }); 24 | }, 25 | (err) => { 26 | next(err, config, ctx); 27 | }, 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /lib/amqp/tasks/bounceVhost.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config, ctx, next) { 2 | ctx.vhost.bounce((err) => { 3 | if (err) return next(err); 4 | return next(null, config, ctx); 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/amqp/tasks/checkExchanges.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:checkExchanges'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | async.eachOfLimit( 7 | _.keys(config.exchanges), 8 | config.concurrency, 9 | (name, index, cb) => { 10 | const channel = ctx.channels[index % config.concurrency]; 11 | checkExchange(channel, config.exchanges[name], cb); 12 | }, 13 | (err) => { 14 | next(err, config, ctx); 15 | }, 16 | ); 17 | }); 18 | 19 | function checkExchange(channel, config, next) { 20 | if (!config.check) return next(); 21 | debug('Checking exchange: %s', config.fullyQualifiedName); 22 | channel.checkExchange(config.fullyQualifiedName, next); 23 | } 24 | -------------------------------------------------------------------------------- /lib/amqp/tasks/checkQueues.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:checkQueues'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | async.eachOfLimit( 7 | _.keys(config.queues), 8 | config.concurrency, 9 | (name, index, cb) => { 10 | const channel = ctx.channels[index % config.concurrency]; 11 | checkQueue(channel, config.queues[name], cb); 12 | }, 13 | (err) => { 14 | next(err, config, ctx); 15 | }, 16 | ); 17 | }); 18 | 19 | function checkQueue(channel, config, next) { 20 | if (!config.check) return next(); 21 | debug('Checking queue: %s', config.fullyQualifiedName); 22 | channel.checkQueue(config.fullyQualifiedName, next); 23 | } 24 | -------------------------------------------------------------------------------- /lib/amqp/tasks/checkVhost.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:checkVhost'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | const Client = require('../../management/Client'); 5 | 6 | module.exports = _.curry((config, ctx, next) => { 7 | if (!config.check) return next(null, config, ctx); 8 | 9 | const candidates = config.connections; 10 | const client = new Client(ctx.components.agent); 11 | 12 | async.retry( 13 | candidates.length, 14 | (cb) => { 15 | const connectionConfig = candidates[ctx.connectionIndex]; 16 | client.checkVhost(config.name, connectionConfig.management, (err) => { 17 | if (err) { 18 | ctx.connectionIndex = (ctx.connectionIndex + 1) % candidates.length; 19 | return cb(err); 20 | } 21 | ctx.connectionConfig = connectionConfig; 22 | cb(); 23 | }); 24 | }, 25 | (err) => { 26 | next(err, config, ctx); 27 | }, 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /lib/amqp/tasks/closeChannels.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:closeChannels'); 2 | const async = require('async'); 3 | const _ = require('lodash'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | debug('Closing %d channels', ctx.channels.length); 7 | 8 | async.each( 9 | ctx.channels, 10 | (channel, cb) => { 11 | channel.close(cb); 12 | }, 13 | (err) => { 14 | delete ctx.channels; 15 | return next(err, config, ctx); 16 | }, 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /lib/amqp/tasks/closeConnection.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:checkQueues'); 2 | const _ = require('lodash'); 3 | 4 | module.exports = _.curry((config, ctx, next) => { 5 | debug('Closing connection: %s', ctx.connectionConfig.loggableUrl); 6 | if (!ctx.connection) return next(null, config, ctx); 7 | ctx.connection.close((err) => { 8 | next(err, config, ctx); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/amqp/tasks/createChannels.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:createChannel'); 2 | const async = require('async'); 3 | const _ = require('lodash'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | debug('Creating %d channels', config.concurrency); 7 | 8 | async.times( 9 | config.concurrency, 10 | (index, cb) => { 11 | ctx.connection.createChannel(cb); 12 | }, 13 | (err, channels) => { 14 | if (err) return next(err, config, ctx); 15 | ctx.channels = channels; 16 | next(null, config, ctx); 17 | }, 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/amqp/tasks/createConnection.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:createConnection'); 2 | const _ = require('lodash'); 3 | const amqplib = require('amqplib/callback_api'); 4 | const async = require('async'); 5 | const format = require('util').format; 6 | const uuid = require('uuid').v4; 7 | 8 | module.exports = _.curry((config, ctx, next) => { 9 | const candidates = config.connections; 10 | 11 | async.retry( 12 | candidates.length, 13 | (cb) => { 14 | const connectionConfig = candidates[ctx.connectionIndex]; 15 | connect(connectionConfig, (err, connection) => { 16 | if (err) { 17 | ctx.connectionIndex = (ctx.connectionIndex + 1) % candidates.length; 18 | return cb(err); 19 | } 20 | ctx.connection = connection; 21 | ctx.connectionConfig = connectionConfig; 22 | cb(); 23 | }); 24 | }, 25 | (err) => { 26 | next(err, config, ctx); 27 | }, 28 | ); 29 | }); 30 | 31 | function connect(connectionConfig, cb) { 32 | debug('Connecting to broker using url: %s', connectionConfig.loggableUrl); 33 | 34 | // See https://github.com/onebeyond/rascal/issues/17 35 | const once = _.once(cb); 36 | let invocations = 0; 37 | 38 | amqplib.connect(connectionConfig.url, connectionConfig.socketOptions, (err, connection) => { 39 | invocations++; 40 | 41 | if (err) { 42 | const betterMessage = format('Failed to connect to: %s. Original message was:', connectionConfig.loggableUrl, err.message); 43 | err.message = betterMessage; 44 | return once(err); 45 | } 46 | 47 | connection._rascal_id = uuid(); 48 | debug('Obtained connection: %s', connection._rascal_id); 49 | 50 | /* 51 | * If an error occurs during initialisation (e.g. if checkExchanges fails), 52 | * and no error handler has been bound to the connection, then the error will bubble up 53 | * to the UncaughtException handler, potentially crashing the node process. 54 | * 55 | * By adding an error handler now, we ensure that instead of being emitted as events 56 | * errors will be passed via the callback chain, so they can still be handled by the caller 57 | * 58 | * This error handle is removed in the vhost after the initialiation has complete 59 | */ 60 | connection.on('error', (err) => { 61 | debug('Received error: %s from %s', err.message, connectionConfig.loggableUrl); 62 | once(err); 63 | }); 64 | 65 | // See https://github.com/squaremo/amqp.node/issues/388 66 | if (invocations > 1) { 67 | debug('Closing superfluous connection: %s previously reported as errored', connection._rascal_id); 68 | return connection.close(); 69 | } 70 | 71 | connection.setMaxListeners(0); 72 | 73 | once(null, connection); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /lib/amqp/tasks/deleteExchanges.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:deleteExchanges'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | async.eachOfLimit( 7 | _.keys(config.exchanges), 8 | config.concurrency, 9 | (name, index, cb) => { 10 | const channel = ctx.channels[index % config.concurrency]; 11 | deleteExchange(channel, config.exchanges[name], cb); 12 | }, 13 | (err) => { 14 | next(err, config, ctx); 15 | }, 16 | ); 17 | }); 18 | 19 | function deleteExchange(channel, config, next) { 20 | if (config.fullyQualifiedName === '') return next(); 21 | debug('Deleting exchange: %s', config.fullyQualifiedName); 22 | channel.deleteExchange(config.fullyQualifiedName, {}, next); 23 | } 24 | -------------------------------------------------------------------------------- /lib/amqp/tasks/deleteQueues.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:deleteQueues'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | async.eachOfLimit( 7 | _.keys(config.queues), 8 | config.concurrency, 9 | (name, index, cb) => { 10 | const channel = ctx.channels[index % config.concurrency]; 11 | deleteQueue(channel, config.queues[name], cb); 12 | }, 13 | (err) => { 14 | next(err, config, ctx); 15 | }, 16 | ); 17 | }); 18 | 19 | function deleteQueue(channel, config, next) { 20 | debug('Deleting queue: %s', config.fullyQualifiedName); 21 | channel.deleteQueue(config.fullyQualifiedName, {}, next); 22 | } 23 | -------------------------------------------------------------------------------- /lib/amqp/tasks/deleteVhost.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:deleteVhost'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | const Client = require('../../management/Client'); 5 | 6 | module.exports = _.curry((config, ctx, next) => { 7 | const vhostConfig = config.vhosts[ctx.vhost.name]; 8 | if (!vhostConfig.assert) return next(null, config, ctx); 9 | 10 | const candidates = vhostConfig.connections; 11 | const client = new Client(ctx.components.agent); 12 | 13 | async.retry( 14 | candidates.length, 15 | (cb) => { 16 | const connectionConfig = candidates[ctx.vhost.connectionIndex]; 17 | client.deleteVhost(vhostConfig.name, connectionConfig.management, (err) => { 18 | if (err) { 19 | ctx.vhost.connectionIndex = (ctx.vhost.connectionIndex + 1) % candidates.length; 20 | return cb(err); 21 | } 22 | ctx.connectionConfig = connectionConfig; 23 | cb(); 24 | }); 25 | }, 26 | (err) => { 27 | next(err, config, ctx); 28 | }, 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /lib/amqp/tasks/forewarnVhost.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config, ctx, next) { 2 | ctx.vhost.forewarn((err) => { 3 | if (err) return next(err); 4 | return next(null, config, ctx); 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/amqp/tasks/index.js: -------------------------------------------------------------------------------- 1 | exports.applyBindings = require('./applyBindings'); 2 | exports.assertExchanges = require('./assertExchanges'); 3 | exports.assertQueues = require('./assertQueues'); 4 | exports.assertVhost = require('./assertVhost'); 5 | exports.bounceVhost = require('./bounceVhost'); 6 | exports.checkExchanges = require('./checkExchanges'); 7 | exports.checkQueues = require('./checkQueues'); 8 | exports.checkVhost = require('./checkVhost'); 9 | exports.closeChannels = require('./closeChannels'); 10 | exports.closeConnection = require('./closeConnection'); 11 | exports.createChannels = require('./createChannels'); 12 | exports.createConnection = require('./createConnection'); 13 | exports.deleteExchanges = require('./deleteExchanges'); 14 | exports.deleteQueues = require('./deleteQueues'); 15 | exports.deleteVhost = require('./deleteVhost'); 16 | exports.initCounters = require('./initCounters'); 17 | exports.initPublications = require('./initPublications'); 18 | exports.initShovels = require('./initShovels'); 19 | exports.initSubscriptions = require('./initSubscriptions'); 20 | exports.initVhosts = require('./initVhosts'); 21 | exports.nukeVhost = require('./nukeVhost'); 22 | exports.purgeQueues = require('./purgeQueues'); 23 | exports.purgeVhost = require('./purgeVhost'); 24 | exports.forewarnVhost = require('./forewarnVhost'); 25 | exports.shutdownVhost = require('./shutdownVhost'); 26 | -------------------------------------------------------------------------------- /lib/amqp/tasks/initCounters.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:initCounters'); 2 | const format = require('util').format; 3 | const _ = require('lodash'); 4 | const async = require('async'); 5 | 6 | module.exports = _.curry((config, ctx, next) => { 7 | ctx.counters = {}; 8 | async.eachSeries( 9 | _.values(config.redeliveries.counters), 10 | (counterConfig, callback) => { 11 | initCounter(counterConfig, ctx, (err, counter) => { 12 | ctx.counters[counterConfig.name] = counter; 13 | callback(err); 14 | }); 15 | }, 16 | (err) => { 17 | next(err, config, ctx); 18 | }, 19 | ); 20 | }); 21 | 22 | function initCounter(config, ctx, next) { 23 | if (!ctx.components.counters[config.type]) return next(new Error(format('Unknown counter type: %s', config.type))); 24 | next(null, ctx.components.counters[config.type](config)); 25 | } 26 | -------------------------------------------------------------------------------- /lib/amqp/tasks/initPublications.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:initPublication'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | const Publication = require('../Publication'); 5 | 6 | module.exports = _.curry((config, ctx, next) => { 7 | async.eachSeries( 8 | _.values(config.publications), 9 | (publicationConfig, callback) => { 10 | initPublication(publicationConfig, ctx, (err, publication) => { 11 | if (err) return callback(err); 12 | ctx.broker._addPublication(publication); 13 | callback(); 14 | }); 15 | }, 16 | (err) => { 17 | next(err, config, ctx); 18 | }, 19 | ); 20 | }); 21 | 22 | function initPublication(config, ctx, next) { 23 | Publication.create(ctx.vhosts[config.vhost], config, next); 24 | } 25 | -------------------------------------------------------------------------------- /lib/amqp/tasks/initShovels.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:initShovels'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | async.eachSeries( 7 | _.values(config.shovels), 8 | (shovelConfig, callback) => { 9 | initShovel(shovelConfig, ctx, callback); 10 | }, 11 | (err) => { 12 | next(err, config, ctx); 13 | }, 14 | ); 15 | }); 16 | 17 | function initShovel(config, ctx, next) { 18 | debug('Initialising shovel: %s', config.name); 19 | 20 | ctx.broker.subscribe(config.subscription, {}, (err, subscription) => { 21 | if (err) return next(err); 22 | 23 | subscription.on('message', (message, content, ackOrNack) => { 24 | ctx.broker.forward(config.publication, message, {}, (err, publication) => { 25 | if (err) return next(err); 26 | publication.on('success', () => { 27 | ackOrNack(); 28 | }); 29 | }); 30 | }); 31 | 32 | subscription.on('error', (err) => { 33 | ctx.broker.emit('error', err); 34 | }); 35 | 36 | subscription.on('cancelled', (err) => { 37 | ctx.broker.emit('cancelled', err) || ctx.broker.emit('error', err); 38 | }); 39 | 40 | next(); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /lib/amqp/tasks/initSubscriptions.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:initSubscriptions'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | const Subscription = require('../Subscription'); 5 | 6 | module.exports = _.curry((config, ctx, next) => { 7 | async.eachSeries( 8 | _.values(config.subscriptions), 9 | (subscriptionConfig, callback) => { 10 | initSubscription(subscriptionConfig, ctx, (err, subscription) => { 11 | ctx.broker._addSubscription(subscription); 12 | callback(err); 13 | }); 14 | }, 15 | (err) => { 16 | next(err, config, ctx); 17 | }, 18 | ); 19 | }); 20 | 21 | function initSubscription(config, ctx, next) { 22 | Subscription.create(ctx.broker, ctx.vhosts[config.vhost], ctx.counters[config.redeliveries.counter], config, next); 23 | } 24 | -------------------------------------------------------------------------------- /lib/amqp/tasks/initVhosts.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:initVhosts'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | const forwardEvents = require('forward-emitter'); 5 | const Vhost = require('../Vhost'); 6 | 7 | module.exports = _.curry((config, ctx, next) => { 8 | ctx.vhosts = {}; 9 | const maxListeners = Math.max(ctx.broker.getMaxListeners(), Object.keys(config.vhosts).length); 10 | ctx.broker.setMaxListeners(maxListeners); 11 | async.eachSeries( 12 | _.values(config.vhosts), 13 | (vhostConfig, callback) => { 14 | initVhost(vhostConfig, ctx.components, (err, vhost) => { 15 | if (err) return callback(err); 16 | vhost.setMaxListeners(0); 17 | forwardEvents(vhost, ctx.broker); 18 | ctx.broker._addVhost(vhost); 19 | ctx.vhosts[vhostConfig.name] = vhost; 20 | callback(); 21 | }); 22 | }, 23 | (err) => { 24 | next(err, config, ctx); 25 | }, 26 | ); 27 | }); 28 | 29 | function initVhost(config, components, next) { 30 | Vhost.create(config, components, next); 31 | } 32 | -------------------------------------------------------------------------------- /lib/amqp/tasks/nukeVhost.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config, ctx, next) { 2 | ctx.vhost.nuke((err) => { 3 | if (err) return next(err); 4 | return next(null, config, ctx); 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/amqp/tasks/purgeQueues.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:tasks:purgeQueues'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | 5 | module.exports = _.curry((config, ctx, next) => { 6 | async.eachOfLimit( 7 | _.keys(config.queues), 8 | config.concurrency, 9 | (name, index, cb) => { 10 | const channel = ctx.channels[index % config.concurrency]; 11 | purgeQueue(channel, config.queues[name], ctx, cb); 12 | }, 13 | (err) => { 14 | next(err, config, ctx); 15 | }, 16 | ); 17 | }); 18 | 19 | function purgeQueue(channel, config, ctx, next) { 20 | if (!config.purge && !ctx.purge) return next(); 21 | debug('Purging queue: %s', config.fullyQualifiedName); 22 | channel.purgeQueue(config.fullyQualifiedName, next); 23 | } 24 | -------------------------------------------------------------------------------- /lib/amqp/tasks/purgeVhost.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config, ctx, next) { 2 | ctx.vhost.purge((err) => { 3 | if (err) return next(err); 4 | return next(null, config, ctx); 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/amqp/tasks/shutdownVhost.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config, ctx, next) { 2 | ctx.vhost.shutdown((err) => { 3 | if (err) return next(err); 4 | return next(null, config, ctx); 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/backoff/exponential.js: -------------------------------------------------------------------------------- 1 | const get = require('lodash').get; 2 | 3 | module.exports = function (options) { 4 | const min = get(options, 'min', 1000); 5 | const max = get(options, 'max', Math.pow(min, 10)); 6 | const factor = get(options, 'factor', 2); 7 | const randomise = get(options, 'randomise', true); 8 | let lower = min; 9 | 10 | function next() { 11 | if (lower > max) return max; 12 | const upper = lower * factor; 13 | const value = randomise ? Math.floor(Math.random() * (upper - lower + 1) + lower) : lower; 14 | const capped = Math.min(max, value); 15 | lower = upper; 16 | return capped; 17 | } 18 | 19 | function reset() { 20 | lower = min; 21 | } 22 | 23 | return { 24 | next, 25 | reset, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/backoff/index.js: -------------------------------------------------------------------------------- 1 | const exponential = require('./exponential'); 2 | const linear = require('./linear'); 3 | 4 | const strategies = { 5 | exponential, 6 | linear, 7 | }; 8 | 9 | module.exports = function (options) { 10 | if (options.delay) return strategies.linear({ min: options.delay }); 11 | if (options.strategy) return strategies[options.strategy](options); 12 | return strategies.exponential(); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/backoff/linear.js: -------------------------------------------------------------------------------- 1 | const get = require('lodash').get; 2 | 3 | module.exports = function (options) { 4 | const min = get(options, 'min', 1000); 5 | const max = get(options, 'max', min); 6 | 7 | function next() { 8 | return Math.floor(Math.random() * (max - min + 1) + min); 9 | } 10 | 11 | // eslint-disable-next-line no-empty-function 12 | function reset() {} 13 | 14 | return { 15 | next, 16 | reset, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/config/baseline.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | defaults: { 3 | vhosts: { 4 | concurrency: 1, 5 | publicationChannelPools: { 6 | regularPool: { 7 | autostart: false, 8 | max: 5, 9 | min: 1, 10 | evictionRunIntervalMillis: 10000, 11 | idleTimeoutMillis: 60000, 12 | rejectionDelayMillis: 1000, 13 | testOnBorrow: true, 14 | acquireTimeoutMillis: 15000, 15 | destroyTimeoutMillis: 1000, 16 | }, 17 | confirmPool: { 18 | autostart: false, 19 | max: 5, 20 | min: 1, 21 | evictionRunIntervalMillis: 10000, 22 | idleTimeoutMillis: 60000, 23 | rejectionDelayMillis: 1000, 24 | testOnBorrow: true, 25 | acquireTimeoutMillis: 15000, 26 | destroyTimeoutMillis: 1000, 27 | }, 28 | }, 29 | connectionStrategy: 'random', 30 | connection: { 31 | slashes: true, 32 | protocol: 'amqp', 33 | hostname: 'localhost', 34 | user: 'guest', 35 | password: 'guest', 36 | port: '5672', 37 | options: {}, 38 | retry: { 39 | min: 1000, 40 | max: 60000, 41 | factor: 2, 42 | strategy: 'exponential', 43 | }, 44 | management: { 45 | slashes: true, 46 | protocol: 'http', 47 | port: 15672, 48 | options: {}, 49 | }, 50 | }, 51 | exchanges: { 52 | assert: true, 53 | type: 'topic', 54 | options: {}, 55 | }, 56 | queues: { 57 | assert: true, 58 | options: { 59 | arguments: {}, 60 | }, 61 | }, 62 | bindings: { 63 | destinationType: 'queue', 64 | bindingKey: '#', 65 | options: {}, 66 | }, 67 | }, 68 | publications: { 69 | vhost: '/', 70 | confirm: true, 71 | timeout: 10000, 72 | options: { 73 | persistent: true, 74 | mandatory: true, 75 | }, 76 | }, 77 | subscriptions: { 78 | vhost: '/', 79 | prefetch: 10, 80 | retry: { 81 | min: 1000, 82 | max: 60000, 83 | factor: 2, 84 | strategy: 'exponential', 85 | }, 86 | redeliveries: { 87 | limit: 100, 88 | timeout: 1000, 89 | counter: 'stub', 90 | }, 91 | options: {}, 92 | }, 93 | redeliveries: { 94 | counters: { 95 | stub: {}, 96 | inMemory: { 97 | size: 1000, 98 | }, 99 | }, 100 | }, 101 | shovels: {}, 102 | }, 103 | publications: {}, 104 | subscriptions: {}, 105 | redeliveries: { 106 | counters: { 107 | stub: {}, 108 | }, 109 | }, 110 | }; 111 | -------------------------------------------------------------------------------- /lib/config/configure.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:config:configure'); 2 | const format = require('util').format; 3 | const url = require('url'); 4 | const _ = require('lodash'); 5 | const uuid = require('uuid').v4; 6 | const XRegExp = require('xregexp'); 7 | const baseline = require('./baseline'); 8 | const fqn = require('./fqn'); 9 | 10 | const { URL } = url; 11 | 12 | module.exports = _.curry((rascalConfig, next) => { 13 | rascalConfig = _.defaultsDeep(rascalConfig, baseline); 14 | 15 | let err; 16 | const connectionIndexes = {}; 17 | 18 | try { 19 | configureVhosts(rascalConfig.vhosts); 20 | configurePublications(rascalConfig.publications, rascalConfig.vhosts); 21 | configureSubscriptions(rascalConfig.subscriptions, rascalConfig.vhosts); 22 | configureShovels(rascalConfig.shovels); 23 | configureCounters(rascalConfig.redeliveries.counters); 24 | } catch (_err) { 25 | err = _err; 26 | } 27 | 28 | return err ? next(err) : next(null, rascalConfig); 29 | 30 | function configureVhosts(vhosts) { 31 | _.each(vhosts, (vhostConfig, name) => { 32 | configureVhost(name, vhostConfig); 33 | configureExchanges(vhostConfig); 34 | configureQueues(vhostConfig); 35 | configureBindings(vhostConfig); 36 | configureVhostPublications(vhostConfig); 37 | configureVhostSubscriptions(vhostConfig); 38 | }); 39 | } 40 | 41 | function configureVhost(name, vhostConfig) { 42 | debug('Configuring vhost: %s', name); 43 | rascalConfig.vhosts[name] = _.defaultsDeep( 44 | vhostConfig, 45 | { 46 | name, 47 | namespace: rascalConfig.defaults.vhosts.namespace, 48 | concurrency: rascalConfig.defaults.vhosts.concurrency, 49 | connectionStrategy: rascalConfig.defaults.vhosts.connectionStrategy, 50 | publicationChannelPools: rascalConfig.defaults.vhosts.publicationChannelPools, 51 | }, 52 | { defaults: rascalConfig.defaults.vhosts }, 53 | ); 54 | rascalConfig.vhosts[name].namespace = vhostConfig.namespace === true ? uuid() : vhostConfig.namespace; 55 | configureConnections(vhostConfig, name); 56 | } 57 | 58 | function configureConnections(vhostConfig, vhostName) { 59 | vhostConfig.connections = _.chain([]) 60 | .concat(vhostConfig.connections, vhostConfig.connection) 61 | .compact() 62 | .uniq() 63 | .map((connection) => { 64 | return _.isString(connection) ? { url: connection } : connection; 65 | }) 66 | .value(); 67 | if (vhostConfig.connections.length === 0) vhostConfig.connections.push({}); 68 | _.each(vhostConfig.connections, (connection, index) => { 69 | configureConnection(vhostConfig, vhostName, connection, index); 70 | }); 71 | vhostConfig.connections = _.sortBy(vhostConfig.connections, 'index').map((connection) => _.omit(connection, 'index')); 72 | delete vhostConfig.connection; 73 | } 74 | 75 | function configureConnection(vhostConfig, vhostName, connection, index) { 76 | const attributesFromUrl = parseConnectionUrl(connection.url); 77 | const attributesFromConfig = getConnectionAttributes(connection); 78 | const defaults = { ...vhostConfig.defaults.connection, vhost: vhostName }; 79 | const connectionAttributes = _.defaultsDeep({}, attributesFromUrl, attributesFromConfig, defaults); 80 | 81 | setConnectionAttributes(connection, connectionAttributes); 82 | setConnectionUrls(connection); 83 | setConnectionIndex(connection, vhostConfig.connectionStrategy, index); 84 | 85 | configureManagementConnection(vhostConfig, vhostName, connection); 86 | } 87 | 88 | function parseConnectionUrl(connectionString) { 89 | if (!connectionString) return {}; 90 | const { 91 | protocol, username: user, password, hostname, port, pathname: vhost, searchParams, 92 | } = new URL(connectionString); 93 | const options = Array.from(searchParams).reduce((attributes, entry) => ({ ...attributes, [entry[0]]: entry[1] }), {}); 94 | return { 95 | protocol, hostname: decodeURIComponent(hostname), port, user: decodeURIComponent(user), password: decodeURIComponent(password), vhost: decodeURIComponent(vhost), options, 96 | }; 97 | } 98 | 99 | function getConnectionAttributes(attributes) { 100 | const { 101 | protocol, hostname, port, user, password, vhost, options, socketOptions, management, 102 | } = attributes; 103 | return { 104 | protocol, hostname, port, user, password, vhost, options, socketOptions, management, 105 | }; 106 | } 107 | 108 | function configureManagementConnection(vhostConfig, vhostName, connection) { 109 | connection.management = _.isString(connection.management) ? { url: connection.management } : connection.management; 110 | const attributesFromUrl = parseConnectionUrl(connection.management.url); 111 | const attributesFromConfig = getConnectionAttributes(connection.management); 112 | const defaults = { user: connection.user, password: connection.password, hostname: connection.hostname }; 113 | 114 | const connectionAttributes = _.defaultsDeep({ options: null }, attributesFromUrl, attributesFromConfig, defaults); 115 | setConnectionAttributes(connection.management, connectionAttributes); 116 | setConnectionUrls(connection.management); 117 | } 118 | 119 | function setConnectionAttributes(connection, attributes, defaults) { 120 | Object.keys(connection).forEach((key) => delete connection[key]); 121 | _.defaultsDeep(connection, attributes, defaults); 122 | } 123 | 124 | function setConnectionUrls(connection) { 125 | const auth = getAuth(connection.user, connection.password); 126 | const pathname = connection.vhost === '/' ? '' : connection.vhost; 127 | const query = connection.options; 128 | connection.url = url.format({ 129 | slashes: true, ...connection, auth, pathname, query, 130 | }); 131 | connection.loggableUrl = connection.url.replace(/:[^:]*@/, ':***@'); 132 | } 133 | 134 | function getAuth(user, password) { 135 | return user && password ? `${user}:${password}` : undefined; 136 | } 137 | 138 | function setConnectionIndex(connection, strategy, index) { 139 | connection.index = strategy === 'fixed' ? index : getConnectionIndex(strategy, `${connection.hostname}:${connection.port}`); 140 | } 141 | 142 | function getConnectionIndex(strategy, hostname) { 143 | if (connectionIndexes[hostname] === undefined) connectionIndexes[hostname] = Math.random(); 144 | return connectionIndexes[hostname]; 145 | } 146 | 147 | function configureVhostPublications(vhostConfig) { 148 | _.each(vhostConfig.publications, (publicationConfig, name) => { 149 | publicationConfig.vhost = vhostConfig.name; 150 | configurePublication(publicationConfig, name); 151 | }); 152 | delete vhostConfig.publications; 153 | } 154 | 155 | function configurePublications(publications, vhosts) { 156 | const defaultPublications = _.reduce( 157 | vhosts, 158 | /* eslint-disable-next-line no-shadow */ 159 | (publications, vhost) => { 160 | _.each(vhost.exchanges, (exchange) => { 161 | const name = vhost.name === '/' ? format('/%s', exchange.name) : format('%s/%s', vhost.name, exchange.name); 162 | publications[name] = { 163 | exchange: exchange.name, 164 | vhost: vhost.name, 165 | autoCreated: true, 166 | }; 167 | }); 168 | _.each(vhost.queues, (queue) => { 169 | const name = vhost.name === '/' ? format('/%s', queue.name) : format('%s/%s', vhost.name, queue.name); 170 | publications[name] = { 171 | queue: queue.name, 172 | vhost: vhost.name, 173 | autoCreated: true, 174 | }; 175 | }); 176 | return publications; 177 | }, 178 | {}, 179 | ); 180 | _.each(_.defaults({}, publications, defaultPublications), configurePublication); 181 | } 182 | 183 | function configurePublication(publicationConfig, name) { 184 | if (publicationConfig.destination) return; 185 | debug('Configuring publication: %s', name); 186 | if (rascalConfig.publications[name] && rascalConfig.publications[name].vhost !== publicationConfig.vhost) throw new Error(format('Duplicate publication: %s', name)); 187 | rascalConfig.publications[name] = _.defaultsDeep(publicationConfig, { name }, rascalConfig.defaults.publications); 188 | if (!rascalConfig.vhosts[publicationConfig.vhost]) return; 189 | const destination = Object.prototype.hasOwnProperty.call(publicationConfig, 'exchange') ? rascalConfig.vhosts[publicationConfig.vhost].exchanges[publicationConfig.exchange] : rascalConfig.vhosts[publicationConfig.vhost].queues[publicationConfig.queue]; 190 | rascalConfig.publications[name].destination = destination.fullyQualifiedName; 191 | 192 | if (publicationConfig.replyTo) { 193 | const replyToQueue = rascalConfig.vhosts[publicationConfig.vhost].queues[publicationConfig.replyTo]; 194 | 195 | if (!replyToQueue) throw new Error(`Publication: ${name} refers to an unknown reply queue: ${publicationConfig.replyTo}`); 196 | publicationConfig.replyTo = replyToQueue.fullyQualifiedName; 197 | } 198 | 199 | if (publicationConfig.encryption && _.isString(publicationConfig.encryption)) { 200 | rascalConfig.publications[name].encryption = _.defaultsDeep({ name: publicationConfig.encryption }, rascalConfig.encryption[publicationConfig.encryption]); 201 | } 202 | } 203 | 204 | function configureVhostSubscriptions(vhostConfig) { 205 | _.each(vhostConfig.subscriptions, (subscriptionConfig, name) => { 206 | subscriptionConfig.vhost = vhostConfig.name; 207 | configureSubscription(subscriptionConfig, name); 208 | }); 209 | delete vhostConfig.subscriptions; 210 | } 211 | 212 | function configureSubscriptions(subscriptions, vhosts) { 213 | const defaultSubscriptions = _.reduce( 214 | vhosts, 215 | /* eslint-disable-next-line no-shadow */ 216 | (subscriptions, vhost) => { 217 | _.each(vhost.queues, (queue) => { 218 | const name = vhost.name === '/' ? format('/%s', queue.name) : format('%s/%s', vhost.name, queue.name); 219 | subscriptions[name] = { 220 | queue: queue.name, 221 | vhost: vhost.name, 222 | autoCreated: true, 223 | }; 224 | }); 225 | return subscriptions; 226 | }, 227 | {}, 228 | ); 229 | _.each(_.defaults({}, subscriptions, defaultSubscriptions), configureSubscription); 230 | } 231 | 232 | function configureSubscription(subscriptionConfig, name) { 233 | debug('Configuring subscription: %s', name); 234 | if (rascalConfig.subscriptions[name] && rascalConfig.subscriptions[name].vhost !== subscriptionConfig.vhost) throw new Error(format('Duplicate subscription: %s', name)); 235 | rascalConfig.subscriptions[name] = _.defaultsDeep(subscriptionConfig, { name }, rascalConfig.defaults.subscriptions); 236 | if (!rascalConfig.vhosts[subscriptionConfig.vhost]) return; 237 | subscriptionConfig.source = rascalConfig.vhosts[subscriptionConfig.vhost].queues[subscriptionConfig.queue].fullyQualifiedName; 238 | subscriptionConfig.encryption = subscriptionConfig.encryption || _.defaultsDeep({}, rascalConfig.encryption); 239 | } 240 | 241 | function configureShovels(shovels) { 242 | rascalConfig.shovels = ensureKeyedCollection(shovels); 243 | _.each(rascalConfig.shovels, configureShovel); 244 | } 245 | 246 | function configureShovel(shovelConfig, name) { 247 | debug('Configuring shovel: %s', name); 248 | const parsedConfig = parseShovelName(name); 249 | rascalConfig.shovels[name] = _.defaultsDeep(shovelConfig, { name }, parsedConfig, rascalConfig.defaults.shovels); 250 | } 251 | 252 | function parseShovelName(name) { 253 | const pattern = XRegExp('(?[\\w:]+)\\s*->\\s*(?[\\w:]+)'); 254 | const match = XRegExp.exec(name, pattern); 255 | return match 256 | ? { 257 | name, 258 | subscription: match.groups.subscription, 259 | publication: match.groups.publication, 260 | } 261 | : { name }; 262 | } 263 | 264 | function configureCounters(counters) { 265 | rascalConfig.redeliveries.counters = ensureKeyedCollection(counters); 266 | _.each(rascalConfig.redeliveries.counters, configureCounter); 267 | } 268 | 269 | function configureCounter(counterConfig, name) { 270 | debug('Configuring counter: %s', name); 271 | const counterType = counterConfig.type || name; 272 | const counterDefaultConfigPath = `defaults.redeliveries.counters.${counterType}`; 273 | const counterDefaults = _.get(rascalConfig, counterDefaultConfigPath); 274 | rascalConfig.redeliveries.counters[name] = _.defaultsDeep(counterConfig, { name, type: name }, counterDefaults); 275 | } 276 | 277 | function configureExchanges(config) { 278 | const defaultExchange = { '': {} }; 279 | config.exchanges = _.defaultsDeep(ensureKeyedCollection(config.exchanges), defaultExchange); 280 | _.each(config.exchanges, (exchangeConfig, name) => { 281 | debug('Configuring exchange: %s', name); 282 | config.exchanges[name] = _.defaultsDeep(exchangeConfig, { name, fullyQualifiedName: fqn.qualify(name, config.namespace) }, config.defaults.exchanges); 283 | }); 284 | } 285 | 286 | function configureQueues(config) { 287 | config.queues = ensureKeyedCollection(config.queues); 288 | _.each(config.queues, (queueConfig, name) => { 289 | debug('Configuring queue: %s', name); 290 | queueConfig.replyTo = queueConfig.replyTo === true ? uuid() : queueConfig.replyTo; 291 | qualifyArguments(config.namespace, queueConfig.options && queueConfig.options.arguments); 292 | config.queues[name] = _.defaultsDeep( 293 | queueConfig, 294 | { 295 | name, 296 | fullyQualifiedName: fqn.qualify(name, config.namespace, queueConfig.replyTo), 297 | }, 298 | config.defaults.queues, 299 | ); 300 | }); 301 | } 302 | 303 | function configureBindings(config) { 304 | config.bindings = expandBindings(ensureKeyedCollection(config.bindings)); 305 | 306 | _.each(config.bindings, (bindingConfig, name) => { 307 | debug('Configuring binding: %s', name); 308 | 309 | config.bindings[name] = _.defaultsDeep(bindingConfig, config.defaults.bindings); 310 | 311 | if (bindingConfig.qualifyBindingKeys) { 312 | config.bindings[name].bindingKey = fqn.qualify(bindingConfig.bindingKey, config.namespace); 313 | } 314 | }); 315 | } 316 | 317 | function parseBindingName(name) { 318 | const pattern = XRegExp('(?[\\w:\\.\\-]+)\\s*(?:\\[\\s*(?.*)\\s*\\])?\\s*->\\s*(?[\\w:\\.\\-]+)'); 319 | const match = XRegExp.exec(name, pattern); 320 | return match 321 | ? { 322 | name, 323 | source: match.groups.source, 324 | destination: match.groups.destination, 325 | bindingKeys: splitBindingKeys(match.groups.keys), 326 | } 327 | : { name }; 328 | } 329 | 330 | function splitBindingKeys(keys) { 331 | return keys ? _.compact(keys.split(/[,\s]+/)) : undefined; 332 | } 333 | 334 | function expandBindings(definitions) { 335 | const result = {}; 336 | _.each(definitions, (bindingConfig, name) => { 337 | const parsedConfig = parseBindingName(name); 338 | const bindingKeys = _.chain([]).concat(bindingConfig.bindingKeys, bindingConfig.bindingKey, parsedConfig.bindingKeys).compact().uniq() 339 | .value(); 340 | if (bindingKeys.length <= 1) { 341 | result[name] = _({ bindingKey: bindingKeys[0] }).defaults(bindingConfig, parsedConfig).omit('bindingKeys').value(); 342 | return result[name]; 343 | } 344 | _.each(bindingKeys, (bindingKey) => { 345 | result[format('%s:%s', name, bindingKey)] = _({ bindingKey }).defaults(bindingConfig, parsedConfig).omit('bindingKeys').value(); 346 | }); 347 | }); 348 | return result; 349 | } 350 | 351 | function qualifyArguments(namespace, args) { 352 | if (!args) return; 353 | _.each(['x-dead-letter-exchange'], (name) => { 354 | args[name] = args[name] !== undefined ? fqn.qualify(args[name], namespace) : args[name]; 355 | }); 356 | } 357 | 358 | function ensureKeyedCollection(collection) { 359 | if (!_.isArray(collection)) return collection; 360 | return _.chain(collection) 361 | .map((item) => { 362 | return _.isString(item) ? { name: item } : _.defaults(item, { name: `unnamed-${uuid()}` }); 363 | }) 364 | .keyBy('name') 365 | .value(); 366 | } 367 | }); 368 | -------------------------------------------------------------------------------- /lib/config/defaults.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | defaults: { 3 | vhosts: { 4 | connection: { 5 | options: { 6 | heartbeat: 10, 7 | connection_timeout: 10000, 8 | channelMax: 100, 9 | }, 10 | socketOptions: { 11 | timeout: 10000, 12 | }, 13 | management: { 14 | options: { 15 | timeout: 1000, 16 | }, 17 | }, 18 | }, 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/config/fqn.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | qualify, 3 | prefix, 4 | suffix, 5 | }; 6 | 7 | function qualify(name, namespace, unique) { 8 | if (name === '') return name; 9 | name = prefix(namespace, name); 10 | name = suffix(unique || undefined, name); 11 | return name; 12 | } 13 | 14 | function prefix(text, name, separator) { 15 | return text ? text + (separator || ':') + name : name; 16 | } 17 | 18 | function suffix(text, name, separator) { 19 | return text ? name + (separator || ':') + text : name; 20 | } 21 | -------------------------------------------------------------------------------- /lib/config/tests.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash').runInContext(); 2 | const defaultConfig = require('./defaults'); 3 | 4 | module.exports = _.defaultsDeep( 5 | { 6 | defaults: { 7 | vhosts: { 8 | connection: { 9 | options: { 10 | heartbeat: 50, 11 | }, 12 | }, 13 | namespace: true, 14 | exchanges: { 15 | options: { 16 | durable: false, 17 | }, 18 | }, 19 | queues: { 20 | purge: true, 21 | options: { 22 | durable: false, 23 | }, 24 | }, 25 | }, 26 | publications: { 27 | options: { 28 | persistent: false, 29 | }, 30 | }, 31 | subscriptions: { 32 | closeTimeout: 500, 33 | }, 34 | }, 35 | redeliveries: { 36 | counters: { 37 | inMemory: { 38 | size: 1000, 39 | }, 40 | }, 41 | }, 42 | }, 43 | defaultConfig, 44 | ); 45 | -------------------------------------------------------------------------------- /lib/config/validate.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('rascal:config:validate'); 2 | const format = require('util').format; 3 | const _ = require('lodash'); 4 | 5 | module.exports = _.curry((config, next) => { 6 | try { 7 | validateVhosts(config.vhosts); 8 | validatePublications(config.publications); 9 | validateSubscriptions(config.subscriptions); 10 | validateEncryptionProfiles(config.encryption); 11 | validateShovels(config.shovels); 12 | } catch (err) { 13 | return next(err, config); 14 | } 15 | next(null, config); 16 | 17 | function validateVhosts(vhosts) { 18 | if (!vhosts || Object.keys(vhosts).length === 0) throw new Error('No vhosts specified'); 19 | _.each(vhosts, validateVhost); 20 | } 21 | 22 | function validateVhost(vhost, vhostName) { 23 | validateAttributes('Vhost', vhost, vhostName, ['defaults', 'namespace', 'name', 'concurrency', 'publicationChannelPools', 'connection', 'connections', 'connectionStrategy', 'exchanges', 'queues', 'bindings', 'check', 'assert']); 24 | validateConnectionStrategy(vhost.connectionStrategy, vhostName); 25 | validateConnectionAttributes(vhost.connection, vhostName, ['slashes', 'protocol', 'hostname', 'user', 'password', 'port', 'vhost', 'options', 'retry', 'auth', 'pathname', 'query', 'url', 'loggableUrl', 'management']); 26 | validateManagementConnectionAttributes(_.get(vhost), 'connection.management', vhostName, ['slashes', 'protocol', 'hostname', 'user', 'password', 'port', 'vhost', 'options', 'auth', 'pathname', 'query', 'url', 'loggableUrl']); 27 | validatePublicationChannelPools(vhost.publicationChannelPools, vhostName); 28 | validateExchanges(vhost, vhostName, vhost.exchanges); 29 | validateQueues(vhost, vhostName, vhost.queues); 30 | validateBindings(vhost, vhostName, vhost.bindings); 31 | } 32 | 33 | function validateAttributes(type, object, objectName, valid) { 34 | const invalid = _.chain(object).omit(valid).keys().value(); 35 | if (invalid.length) throw new Error(format('%s: %s refers to an unsupported attribute: %s', type, objectName, invalid[0])); 36 | } 37 | 38 | function validateConnectionStrategy(strategy, vhostName) { 39 | if (![undefined, 'random', 'fixed'].includes(strategy)) throw new Error(format('Vhost: %s refers to an unknown connection strategy: %s', vhostName, strategy)); 40 | } 41 | 42 | function validateConnectionAttributes(connection, vhostName, valid) { 43 | const invalid = _.chain(connection).omit(valid).keys().value(); 44 | if (invalid.length) throw new Error(format('Vhost: %s connection refers to an unsupported attribute: %s', vhostName, invalid[0])); 45 | } 46 | 47 | function validateManagementConnectionAttributes(connection, vhostName, valid) { 48 | if (!connection) return; 49 | const invalid = _.chain(connection).omit(valid).keys().value(); 50 | if (invalid.length) throw new Error(format('Vhost: %s management connection refers to an unsupported attribute: %s', vhostName, invalid[0])); 51 | } 52 | 53 | function validatePublicationChannelPools(publicationChannelPools, vhostName) { 54 | const invalid = _.chain(publicationChannelPools).omit(['regularPool', 'confirmPool']).keys().value(); 55 | if (invalid.length) throw new Error(format('Publication channel pool in vhost: %s refers to an unsupported attribute: %s', vhostName, invalid[0])); 56 | } 57 | 58 | function validateVhostChildAttributes(vhostName, type, child, childName, valid) { 59 | const invalid = _.chain(child).omit(valid).keys().value(); 60 | if (invalid.length) throw new Error(format('%s: %s in vhost: %s refers to an unsupported attribute: %s', type, childName, vhostName, invalid[0])); 61 | } 62 | 63 | function validateExchanges(vhost, vhostName, exchanges) { 64 | _.each(exchanges, validateExchange.bind(null, vhost, vhostName)); 65 | } 66 | 67 | function validateExchange(vhost, vhostName, exchange, exchangeName) { 68 | validateVhostChildAttributes(vhostName, 'Exchange', exchange, exchangeName, ['fullyQualifiedName', 'name', 'assert', 'check', 'type', 'options']); 69 | } 70 | 71 | function validateQueues(vhost, vhostName, queues) { 72 | _.each(queues, validateQueue.bind(null, vhost, vhostName)); 73 | } 74 | 75 | function validateQueue(vhost, vhostName, queue, queueName) { 76 | validateVhostChildAttributes(vhostName, 'Queue', queue, queueName, ['fullyQualifiedName', 'name', 'assert', 'check', 'type', 'purge', 'replyTo', 'options']); 77 | } 78 | 79 | function validateBindings(vhost, vhostName, bindings) { 80 | _.each(bindings, validateBinding.bind(null, vhost, vhostName)); 81 | } 82 | 83 | function validateBinding(vhost, vhostName, binding, bindingName) { 84 | validateVhostChildAttributes(vhostName, 'Binding', binding, bindingName, ['fullyQualifiedName', 'name', 'source', 'destination', 'destinationType', 'bindingKey', 'bindingKeys', 'qualifyBindingKeys', 'options']); 85 | if (!binding.source) throw new Error(format('Binding: %s in vhost: %s is missing a source', bindingName, vhostName)); 86 | if (!binding.destination) throw new Error(format('Binding: %s in vhost: %s is missing a destination', bindingName, vhostName)); 87 | if (!binding.destinationType) throw new Error(format('Binding: %s in vhost: %s is missing a destination type', bindingName, vhostName)); 88 | if (['queue', 'exchange'].indexOf(binding.destinationType) < 0) throw new Error(format('Binding: %s in vhost: %s has an invalid destination type: %s', bindingName, vhostName, binding.destinationType)); 89 | 90 | if (!vhost.exchanges) throw new Error(format('Binding: %s in vhost: %s refers to an unknown exchange: %s', bindingName, vhostName, binding.source)); 91 | if (!vhost.exchanges[binding.source]) throw new Error(format('Binding: %s in vhost: %s refers to an unknown exchange: %s', bindingName, vhostName, binding.source)); 92 | 93 | if (binding.destinationType === 'queue') { 94 | if (!vhost.queues) throw new Error(format('Binding: %s in vhost: %s refers to an unknown queue: %s', bindingName, vhostName, binding.destination)); 95 | if (!vhost.queues[binding.destination]) throw new Error(format('Binding: %s in vhost: %s refers to an unknown queue: %s', bindingName, vhostName, binding.destination)); 96 | } else if (!vhost.exchanges[binding.destination]) throw new Error(format('Binding: %s in vhost: %s refers to an unknown exchange: %s', bindingName, vhostName, binding.destination)); 97 | } 98 | 99 | function validatePublications(publications) { 100 | _.each(publications, validatePublication); 101 | } 102 | 103 | function validatePublication(publication, publicationName) { 104 | validateAttributes('Publication', publication, publicationName, ['name', 'vhost', 'exchange', 'queue', 'routingKey', 'confirm', 'options', 'destination', 'autoCreated', 'deprecated', 'encryption', 'replyTo', 'timeout']); 105 | if (!publication.vhost) throw new Error(format('Publication: %s is missing a vhost', publicationName)); 106 | if (!(Object.prototype.hasOwnProperty.call(publication, 'exchange') || publication.queue)) throw new Error(format('Publication: %s is missing an exchange or a queue', publicationName)); 107 | if (Object.prototype.hasOwnProperty.call(publication, 'exchange') && publication.queue) throw new Error(format('Publication: %s has an exchange and a queue', publicationName)); 108 | 109 | if (!config.vhosts) throw new Error(format('Publication: %s refers to an unknown vhost: %s', publicationName, publication.vhost)); 110 | if (!config.vhosts[publication.vhost]) throw new Error(format('Publication: %s refers to an unknown vhost: %s', publicationName, publication.vhost)); 111 | 112 | if (Object.prototype.hasOwnProperty.call(publication, 'exchange')) { 113 | if (!config.vhosts[publication.vhost].exchanges) throw new Error(format('Publication: %s refers to an unknown exchange: %s in vhost: %s', publicationName, publication.exchange, publication.vhost)); 114 | if (!config.vhosts[publication.vhost].exchanges[publication.exchange]) throw new Error(format('Publication: %s refers to an unknown exchange: %s in vhost: %s', publicationName, publication.exchange, publication.vhost)); 115 | } else { 116 | if (!config.vhosts[publication.vhost].queues) throw new Error(format('Publication: %s refers to an unknown queue: %s in vhost: %s', publicationName, publication.queue, publication.vhost)); 117 | if (!config.vhosts[publication.vhost].queues[publication.queue]) throw new Error(format('Publication: %s refers to an unknown queue: %s in vhost: %s', publicationName, publication.queue, publication.vhost)); 118 | } 119 | 120 | if (publication.encryption) validateEncryptionProfile(publication.encryption); 121 | } 122 | 123 | function validateSubscriptions(subscriptions) { 124 | _.each(subscriptions, validateSubscription); 125 | } 126 | 127 | function validateSubscription(subscription, subscriptionName) { 128 | validateAttributes('Subscription', subscription, subscriptionName, [ 129 | 'name', 130 | 'vhost', 131 | 'queue', 132 | 'contentType', 133 | 'options', 134 | 'prefetch', 135 | 'channelPrefetch', 136 | 'retry', 137 | 'source', 138 | 'recovery', 139 | 'workflow', 140 | 'handler', 141 | 'workflows', 142 | 'handlers', 143 | 'redeliveries', 144 | 'autoCreated', 145 | 'deprecated', 146 | 'closeTimeout', 147 | 'encryption', 148 | 'promisifyAckOrNack', 149 | ]); 150 | 151 | if (!subscription.vhost) throw new Error(format('Subscription: %s is missing a vhost', subscriptionName)); 152 | if (!subscription.queue) throw new Error(format('Subscription: %s is missing a queue', subscriptionName)); 153 | 154 | if (!config.vhosts) throw new Error(format('Subscription: %s refers to an unknown vhost: %s', subscriptionName, subscription.vhost)); 155 | if (!config.vhosts[subscription.vhost]) throw new Error(format('Subscription: %s refers to an unknown vhost: %s', subscriptionName, subscription.vhost)); 156 | 157 | if (!config.vhosts[subscription.vhost].queues) throw new Error(format('Subscription: %s refers to an unknown queue: %s in vhost: %s', subscriptionName, subscription.queue, subscription.vhost)); 158 | if (!config.vhosts[subscription.vhost].queues[subscription.queue]) throw new Error(format('Subscription: %s refers to an unknown queue: %s in vhost: %s', subscriptionName, subscription.queue, subscription.vhost)); 159 | 160 | if (_.get(config, 'config.redeliveries.counters')) throw new Error(format('Subscription: %s refers to an unknown counter: %s in vhost: %s', subscriptionName, subscription.redeliveries.counter, subscription.vhost)); 161 | if (!config.redeliveries.counters[subscription.redeliveries.counter]) throw new Error(format('Subscription: %s refers to an unknown counter: %s in vhost: %s', subscriptionName, subscription.redeliveries.counter, subscription.vhost)); 162 | 163 | if (subscription.encryption) validateEncryptionProfiles(subscription.encryption); 164 | } 165 | 166 | function validateEncryptionProfiles(encryption) { 167 | _.each(encryption, validateEncryptionProfile); 168 | } 169 | 170 | function validateEncryptionProfile(encryption, encryptionName) { 171 | validateAttributes('Encryption', encryption, encryptionName, ['name', 'key', 'algorithm', 'ivLength']); 172 | if (!encryption.key) throw new Error(format('Encryption profile: %s is missing a key', encryptionName)); 173 | if (!encryption.algorithm) throw new Error(format('Encryption profile: %s is missing an algorithm', encryptionName)); 174 | if (!encryption.ivLength) throw new Error(format('Encryption profile: %s is missing ivLength', encryptionName)); 175 | } 176 | 177 | function validateShovels(shovels) { 178 | _.each(shovels, validateShovel); 179 | } 180 | 181 | function validateShovel(shovel, shovelName) { 182 | validateAttributes('Shovel', shovel, shovelName, ['name', 'subscription', 'publication']); 183 | if (!shovel.subscription) throw new Error(format('Shovel: %s is missing a subscription', shovelName)); 184 | if (!shovel.publication) throw new Error(format('Shovel: %s is missing a publication', shovelName)); 185 | 186 | if (!config.subscriptions[shovel.subscription]) throw new Error(format('Shovel: %s refers to an unknown subscription: %s', shovelName, shovel.subscription)); 187 | if (!config.publications[shovel.publication]) throw new Error(format('Shovel: %s refers to an unknown publication: %s', shovelName, shovel.publication)); 188 | } 189 | }); 190 | -------------------------------------------------------------------------------- /lib/counters/inMemory.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const LRUCache = require('lru-cache'); 3 | 4 | module.exports = function init(options) { 5 | const size = _.get(options, 'size') || 1000; 6 | const cache = new LRUCache({ max: size }); 7 | 8 | return { 9 | incrementAndGet(key, next) { 10 | const redeliveries = cache.get(key) + 1 || 1; 11 | cache.set(key, redeliveries); 12 | next(null, redeliveries); 13 | }, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/counters/inMemoryCluster.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const uuid = require('uuid').v4; 3 | const Stashback = require('stashback'); 4 | const inMemory = require('./inMemory'); 5 | 6 | const debug = 'rascal:counters:inMemoryCluster'; 7 | 8 | module.exports = { 9 | master: function master(options) { 10 | const workers = {}; 11 | const counter = inMemory(options); 12 | 13 | function handleMessage(worker, message) { 14 | if (message.sender !== 'rascal-in-memory-cluster-counter' || message.cmd !== 'incrementAndGet') return; 15 | counter.incrementAndGet(message.key, (err, value) => { 16 | worker.send({ 17 | sender: 'rascal-in-memory-cluster-counter', 18 | correlationId: message.correlationId, 19 | value: err ? 1 : value, 20 | }); 21 | }); 22 | } 23 | 24 | cluster 25 | .on('fork', (worker) => { 26 | workers[worker.id] = worker; 27 | worker.on('message', (message) => { 28 | handleMessage(worker, message); 29 | }); 30 | }) 31 | .on('disconnect', (worker) => { 32 | delete workers[worker.id]; 33 | }) 34 | .on('exit', (worker) => { 35 | delete workers[worker.id]; 36 | }); 37 | }, 38 | worker: function worker(options) { 39 | if (!cluster.isWorker) throw new Error("You cannot use Rascal's in memory cluster counter outside of a cluster"); 40 | if (!options) return worker({}); 41 | const timeout = options.timeout || 100; 42 | const stashback = Stashback({ timeout }); 43 | 44 | process.on('message', (message) => { 45 | if (message.sender !== 'rascal-in-memory-cluster-counter') return; 46 | stashback.unstash(message.correlationId, (err, cb) => { 47 | err ? cb(null, 1) : cb(null, message.value); 48 | }); 49 | }); 50 | 51 | return { 52 | incrementAndGet(key, cb) { 53 | const correlationId = uuid(); 54 | stashback.stash( 55 | correlationId, 56 | (err, value) => { 57 | err ? cb(null, 1) : cb(null, value); 58 | }, 59 | (err) => { 60 | if (err) return cb(null, 1); 61 | process.send({ 62 | sender: 'rascal-in-memory-cluster-counter', 63 | correlationId, 64 | cmd: 'incrementAndGet', 65 | }); 66 | }, 67 | ); 68 | }, 69 | }; 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /lib/counters/index.js: -------------------------------------------------------------------------------- 1 | const stub = require('./stub'); 2 | const inMemory = require('./inMemory'); 3 | const inMemoryCluster = require('./inMemoryCluster'); 4 | 5 | module.exports = { 6 | stub, 7 | inMemory, 8 | inMemoryCluster, 9 | }; 10 | -------------------------------------------------------------------------------- /lib/counters/stub.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | incrementAndGet(key, next) { 4 | next(null, 0); 5 | }, 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /lib/management/Client.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const debug = require('debug')('rascal:management:client'); 3 | const format = require('util').format; 4 | 5 | function Client(agent) { 6 | const self = this; 7 | 8 | this.assertVhost = function (name, config, next) { 9 | debug('Asserting vhost: %s', name); 10 | const url = getUrl(name, config); 11 | self._request('PUT', url, config.options, (err) => { 12 | if (!err) return next(); 13 | const _err = err.status ? new Error(format('Failed to assert vhost: %s. %s returned status %d', name, config.loggableUrl, err.status)) : err; 14 | return next(_err); 15 | }); 16 | }; 17 | 18 | this.checkVhost = function (name, config, next) { 19 | debug('Checking vhost: %s', name); 20 | const url = getUrl(name, config); 21 | self._request('GET', url, config.options, (err) => { 22 | if (!err) return next(); 23 | const _err = err.status ? new Error(format('Failed to check vhost: %s. %s returned status %d', name, config.loggableUrl, err.status)) : err; 24 | return next(_err); 25 | }); 26 | }; 27 | 28 | this.deleteVhost = function (name, config, next) { 29 | debug('Deleting vhost: %s', name); 30 | const url = getUrl(name, config); 31 | self._request('DELETE', url, config.options, (err) => { 32 | if (!err) return next(); 33 | const _err = err.status ? new Error(format('Failed to delete vhost: %s. %s returned status %d', name, config.loggableUrl, err.status)) : err; 34 | return next(_err); 35 | }); 36 | }; 37 | 38 | this._request = function (method, url, options, next) { 39 | const req = http.request(url, { ...options, method, agent }, (res) => { 40 | if (res.statusCode >= 300) { 41 | const err = Object.assign(new Error('HTTP Error'), { status: res.statusCode }); 42 | return next(err); 43 | } 44 | res.on('data', () => {}); 45 | res.on('end', () => next()); 46 | }); 47 | req.on('error', next); 48 | req.end(); 49 | }; 50 | 51 | function getUrl(name, config) { 52 | return format('%s/%s/%s', config.url, 'api/vhosts', name); 53 | } 54 | } 55 | 56 | module.exports = Client; 57 | -------------------------------------------------------------------------------- /lib/utils/setTimeoutUnref.js: -------------------------------------------------------------------------------- 1 | // See https://github.com/onebeyond/rascal/issues/89 2 | module.exports = function (fn, millis) { 3 | const t = setTimeout(fn, millis); 4 | return t.unref ? t.unref() : t; 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rascal", 3 | "version": "20.1.1", 4 | "description": "An advanced RabbitMQ / AMQP client built on amqplib", 5 | "main": "index.js", 6 | "dependencies": { 7 | "async": "^3.2.4", 8 | "debug": "^4.3.4", 9 | "forward-emitter": "^0.1.1", 10 | "generic-pool": "^3.8.2", 11 | "lodash": "^4.17.21", 12 | "lru-cache": "^7.10.1", 13 | "safe-json-parse": "^4.0.0", 14 | "stashback": "^2.0.1", 15 | "uuid": "^8.3.2", 16 | "xregexp": "^5.1.0" 17 | }, 18 | "devDependencies": { 19 | "amqplib": "^0.10.2", 20 | "chalk": "^4.1.2", 21 | "chance": "^1.1.8", 22 | "eslint": "^8.45.0", 23 | "eslint-config-airbnb-base": "^15.0.0", 24 | "eslint-plugin-import": "^2.27.5", 25 | "husky": "^8.0.3", 26 | "lint-staged": "^11.2.4", 27 | "nyc": "^15.1.0", 28 | "random-readable": "^1.0.1", 29 | "zunit": "^4.0.0" 30 | }, 31 | "peerDependencies": { 32 | "amqplib": ">=0.5.5" 33 | }, 34 | "engines": { 35 | "node": ">=14" 36 | }, 37 | "scripts": { 38 | "test": "zUnit", 39 | "lint": "eslint .", 40 | "lint:fix": "eslint --fix .", 41 | "lint-staged": "lint-staged", 42 | "coverage": "nyc --report html --reporter lcov --reporter text-summary zUnit", 43 | "docker": "docker run -d --name rascal-rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.12.9-management-alpine", 44 | "prepare": "husky install" 45 | }, 46 | "lint-staged": { 47 | "**/*.js": "eslint --fix" 48 | }, 49 | "keywords": [ 50 | "rabbitmq", 51 | "rabbit", 52 | "amqplib", 53 | "amqp" 54 | ], 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/onebeyond/rascal.git" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/onebeyond/rascal/issues" 61 | }, 62 | "homepage": "https://onebeyond.github.io/rascal/", 63 | "author": "Stephen Cresswell", 64 | "license": "MIT", 65 | "zUnit": { 66 | "pollute": true, 67 | "pattern": "^[\\w-]+.tests.js$" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "describe": "readonly", 4 | "xdescribe": "readonly", 5 | "odescribe": "readonly", 6 | "it": "readonly", 7 | "xit": "readonly", 8 | "oit": "readonly", 9 | "before": "readonly", 10 | "beforeEach": "readonly", 11 | "after": "readonly", 12 | "afterEach": "readonly", 13 | "include": "readonly" 14 | }, 15 | "rules": { 16 | "no-shadow": 0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/backoff/exponential.tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const exponential = require('../../lib/backoff/exponential'); 3 | 4 | describe('Exponential Backoff', () => { 5 | it('should backoff by 1 seconds by default', () => { 6 | const backoff = exponential({ randomise: false }); 7 | assert.strictEqual(backoff.next(), 1000); 8 | assert.strictEqual(backoff.next(), 2000); 9 | assert.strictEqual(backoff.next(), 4000); 10 | }); 11 | 12 | it('should backoff by the specified value', () => { 13 | const backoff = exponential({ min: 2000, factor: 3, randomise: false }); 14 | assert.strictEqual(backoff.next(), 2000); 15 | assert.strictEqual(backoff.next(), 6000); 16 | assert.strictEqual(backoff.next(), 18000); 17 | }); 18 | 19 | it('should backoff between the specified values', () => { 20 | const backoff = exponential({ min: 2000, factor: 3, randomise: true }); 21 | const results = []; 22 | for (let i = 0; i < 10; i++) { 23 | const value = backoff.next(); 24 | if (results.indexOf(value) < 0) results.push(value); 25 | } 26 | assert(results[0] >= 2000 && results[0] <= 6000, results[0]); 27 | assert(results[1] >= 6000 && results[1] <= 18000, results[1]); 28 | assert(results[2] >= 18000 && results[2] <= 54000, results[2]); 29 | assert(results[3] >= 54000 && results[3] <= 162000, results[3]); 30 | assert(results[4] >= 162000 && results[4] <= 486000, results[4]); 31 | assert(results[5] >= 486000 && results[5] <= 1458000, results[5]); 32 | assert(results[6] >= 1458000 && results[6] <= 4374000, results[6]); 33 | }); 34 | 35 | it('should cap values', () => { 36 | const backoff = exponential({ 37 | min: 2000, 38 | factor: 3, 39 | randomise: true, 40 | max: 18000, 41 | }); 42 | const results = []; 43 | for (let i = 0; i < 700; i++) { 44 | const value = backoff.next(); 45 | results.push(value); 46 | } 47 | assert(results[0] >= 2000 && results[0] <= 6000, results[0]); 48 | assert(results[1] >= 6000 && results[1] <= 18000, results[1]); 49 | for (let i = 2; i < 700; i++) { 50 | assert(results[i] === 18000, results[i]); 51 | } 52 | }); 53 | 54 | it('should reset values', () => { 55 | const backoff = exponential({ 56 | min: 2000, 57 | factor: 3, 58 | randomise: true, 59 | max: 16000, 60 | }); 61 | const results = []; 62 | for (let i = 0; i < 10; i++) { 63 | const value = backoff.next(); 64 | if (results.indexOf(value) < 0) results.push(value); 65 | } 66 | backoff.reset(); 67 | assert(results[0] >= 2000 && results[0] <= 6000, backoff.next()); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/backoff/linear.tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const linear = require('../../lib/backoff/linear'); 3 | 4 | describe('Linear Backoff', () => { 5 | it('should backoff by 1 seconds by default', () => { 6 | const backoff = linear(); 7 | assert.strictEqual(backoff.next(), 1000); 8 | assert.strictEqual(backoff.next(), 1000); 9 | assert.strictEqual(backoff.next(), 1000); 10 | }); 11 | 12 | it('should backoff by the specified value', () => { 13 | const backoff = linear({ min: 2000 }); 14 | assert.strictEqual(backoff.next(), 2000); 15 | assert.strictEqual(backoff.next(), 2000); 16 | assert.strictEqual(backoff.next(), 2000); 17 | }); 18 | 19 | it('should backoff by within a range', () => { 20 | const backoff = linear({ min: 1000, max: 1002 }); 21 | const results = []; 22 | for (let i = 0; i < 1000; i++) { 23 | const value = backoff.next(); 24 | if (results.indexOf(value) < 0) results.push(value); 25 | } 26 | assert.deepEqual(results.sort(), [1000, 1001, 1002]); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/broker.tests.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const assert = require('assert'); 3 | const _ = require('lodash'); 4 | const uuid = require('uuid').v4; 5 | const format = require('util').format; 6 | const random = require('random-readable'); 7 | const amqplib = require('amqplib/callback_api'); 8 | const testConfig = require('../lib/config/tests'); 9 | const Broker = require('..').Broker; 10 | const AmqpUtils = require('./utils/amqputils'); 11 | 12 | describe('Broker', () => { 13 | let broker; 14 | let amqputils; 15 | let namespace; 16 | let vhosts; 17 | let publications; 18 | let subscriptions; 19 | 20 | beforeEach((test, done) => { 21 | namespace = uuid(); 22 | 23 | vhosts = { 24 | '/': { 25 | connection: { 26 | management: { 27 | options: { 28 | timeout: 5000, 29 | }, 30 | }, 31 | }, 32 | namespace, 33 | exchanges: { 34 | e1: { 35 | assert: true, 36 | }, 37 | }, 38 | queues: { 39 | q1: { 40 | assert: true, 41 | }, 42 | }, 43 | subscriptions: { 44 | s1: { 45 | queue: 'q1', 46 | }, 47 | }, 48 | publications: { 49 | p1: { 50 | exchange: 'e1', 51 | }, 52 | p2: { 53 | exchange: 'e1', 54 | confirm: false, 55 | }, 56 | }, 57 | bindings: { 58 | b1: { 59 | source: 'e1', 60 | destination: 'q1', 61 | }, 62 | }, 63 | }, 64 | }; 65 | 66 | publications = { 67 | p1: { 68 | vhost: '/', 69 | exchange: 'e1', 70 | routingKey: 'foo', 71 | }, 72 | }; 73 | 74 | subscriptions = { 75 | s1: { 76 | vhost: '/', 77 | queue: 'q1', 78 | }, 79 | }; 80 | 81 | amqplib.connect((err, connection) => { 82 | if (err) return done(err); 83 | amqputils = AmqpUtils.init(connection); 84 | done(); 85 | }); 86 | }); 87 | 88 | afterEach((test, done) => { 89 | amqputils.disconnect(() => { 90 | if (broker) return broker.nuke(done); 91 | done(); 92 | }); 93 | }); 94 | 95 | it('should assert vhosts', (test, done) => { 96 | const vhostName = uuid(); 97 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 98 | customVhosts[vhostName].assert = true; 99 | 100 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 101 | createBroker(config, (err) => { 102 | assert.ifError(err); 103 | done(); 104 | }); 105 | }); 106 | 107 | it('should fail to assert vhost when unable to connect to management plugin', (test, done) => { 108 | const vhostName = uuid(); 109 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 110 | customVhosts[vhostName].assert = true; 111 | customVhosts[vhostName].connection.management.port = 65535; 112 | 113 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 114 | createBroker(config, (err) => { 115 | assert.ok(err); 116 | const code = err.errors ? err.errors[0].code : err.code; 117 | assert.strictEqual(code, 'ECONNREFUSED'); 118 | done(); 119 | }); 120 | }); 121 | 122 | it('should fail when checking vhosts that dont exist', (test, done) => { 123 | const vhostName = uuid(); 124 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 125 | customVhosts[vhostName].check = true; 126 | 127 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 128 | createBroker(config, (err) => { 129 | assert.ok(err); 130 | assert.strictEqual(err.message, format('Failed to check vhost: %s. http://guest:***@localhost:15672 returned status 404', vhostName)); 131 | done(); 132 | }); 133 | }); 134 | 135 | it('should fail to check vhost when unable to connect to management plugin', (test, done) => { 136 | const vhostName = uuid(); 137 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 138 | customVhosts[vhostName].check = true; 139 | customVhosts[vhostName].connection.management.port = 65535; 140 | 141 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 142 | createBroker(config, (err) => { 143 | assert.ok(err); 144 | const code = err.errors ? err.errors[0].code : err.code; 145 | assert.strictEqual(code, 'ECONNREFUSED'); 146 | done(); 147 | }); 148 | }); 149 | 150 | it('should succeed when checking vhosts that do exist', (test, done) => { 151 | const vhostName = uuid(); 152 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 153 | customVhosts[vhostName].assert = true; 154 | customVhosts[vhostName].check = true; 155 | 156 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 157 | createBroker(config, (err) => { 158 | assert.ifError(err); 159 | done(); 160 | }); 161 | }); 162 | 163 | it('should delete vhosts', (test, done) => { 164 | const vhostName = uuid(); 165 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 166 | customVhosts[vhostName].assert = true; 167 | 168 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 169 | createBroker(config, (err, broker) => { 170 | assert.ifError(err); 171 | broker.nuke((err) => { 172 | assert.ifError(err); 173 | config.vhosts[vhostName].assert = false; 174 | config.vhosts[vhostName].check = true; 175 | createBroker(config, (err) => { 176 | assert.ok(err); 177 | assert.strictEqual(err.message, format('Failed to check vhost: %s. http://guest:***@localhost:15672 returned status 404', vhostName)); 178 | done(); 179 | }); 180 | }); 181 | }); 182 | }); 183 | 184 | it('should support custom http agent', (test, done) => { 185 | const agent = new http.Agent(); 186 | agent.createConnection = (options, cb) => { 187 | cb(new Error('Custom Agent')); 188 | }; 189 | 190 | const components = { agent }; 191 | const vhostName = uuid(); 192 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 193 | customVhosts[vhostName].assert = true; 194 | 195 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 196 | createBroker(config, components, (err) => { 197 | assert.strictEqual(err.message, 'Custom Agent'); 198 | done(); 199 | }); 200 | }); 201 | 202 | it('should provide fully qualified name', (test, done) => { 203 | const config = _.defaultsDeep({ vhosts }, testConfig); 204 | createBroker(config, (err, broker) => { 205 | assert.ifError(err); 206 | assert.strictEqual(`${namespace}:q1`, broker.getFullyQualifiedName('/', 'q1')); 207 | done(); 208 | }); 209 | }); 210 | 211 | it('should not modify configuration', (test, done) => { 212 | const config = _.defaultsDeep({ vhosts }, testConfig); 213 | const json = JSON.stringify(config, null, 2); 214 | createBroker(config, (err) => { 215 | assert.ifError(err); 216 | assert.strictEqual(json, JSON.stringify(config, null, 2)); 217 | done(); 218 | }); 219 | }); 220 | 221 | it('should nuke', (test, done) => { 222 | const config = _.defaultsDeep({ vhosts }, testConfig); 223 | createBroker(config, (err, broker) => { 224 | assert.ifError(err); 225 | broker.nuke((err) => { 226 | assert.ifError(err); 227 | done(); 228 | }); 229 | }); 230 | }); 231 | 232 | it('should tolerate unsubscribe timeouts when nuking', (test, done) => { 233 | const config = _.defaultsDeep({ vhosts }, testConfig); 234 | config.vhosts['/'].subscriptions.s1.closeTimeout = 100; 235 | 236 | let timeoutErr; 237 | 238 | createBroker(config, (err, broker) => { 239 | assert.ifError(err); 240 | 241 | broker.subscribe('s1', (err, subscription) => { 242 | assert.ifError(err); 243 | subscription.on('message', () => { 244 | broker.nuke((err) => { 245 | assert.ifError(err); 246 | assert.strictEqual(timeoutErr.code, 'ETIMEDOUT'); 247 | done(); 248 | }); 249 | }); 250 | }); 251 | 252 | broker.publish('p1', 'test message', (err) => { 253 | assert.ifError(err); 254 | }); 255 | 256 | broker.on('error', (err) => { 257 | timeoutErr = err; 258 | }); 259 | }); 260 | }); 261 | 262 | it('should cancel subscriptions', (test, done) => { 263 | const config = _.defaultsDeep( 264 | { 265 | vhosts, 266 | publications, 267 | subscriptions, 268 | }, 269 | testConfig, 270 | ); 271 | 272 | createBroker(config, (err, broker) => { 273 | assert.ifError(err); 274 | 275 | broker.subscribe('s1', (err, subscription) => { 276 | assert.ifError(err); 277 | 278 | subscription.on('message', () => { 279 | subscription.cancel((err) => { 280 | done(err); 281 | }); 282 | assert(false, 'No message should have been received'); 283 | }); 284 | 285 | broker.unsubscribeAll((err) => { 286 | assert.ifError(err); 287 | 288 | broker.publish('p1', 'test message', (err) => { 289 | assert.ifError(err); 290 | setTimeout(done, 500); 291 | }); 292 | }); 293 | }); 294 | }); 295 | }); 296 | 297 | it('should not return from unsubscribeAll until underlying channels have been closed', (test, done) => { 298 | const config = _.defaultsDeep( 299 | { 300 | vhosts, 301 | publications, 302 | subscriptions, 303 | }, 304 | testConfig, 305 | ); 306 | 307 | config.vhosts['/'].subscriptions.s1.closeTimeout = 200; 308 | 309 | createBroker(config, (err, broker) => { 310 | assert.ifError(err); 311 | 312 | broker.subscribe('s1', (err, subscription) => { 313 | assert.ifError(err); 314 | 315 | broker.publish('p1', 'test message', (err) => { 316 | assert.ifError(err); 317 | }); 318 | 319 | // eslint-disable-next-line no-empty-function 320 | subscription.on('message', () => { 321 | const before = Date.now(); 322 | broker.unsubscribeAll((err) => { 323 | assert.strictEqual(err.code, 'ETIMEDOUT'); 324 | const after = Date.now(); 325 | assert.ok(after >= before + 200, 'Did not defer returning from unsubscibeAll'); 326 | done(); 327 | }); 328 | }); 329 | }); 330 | }); 331 | }); 332 | 333 | it('should connect', (test, done) => { 334 | const config = _.defaultsDeep({ vhosts }, testConfig); 335 | createBroker(config, (err, broker) => { 336 | assert.ifError(err); 337 | broker.connect('/', (err, connection) => { 338 | assert.ifError(err); 339 | assert.ok(connection._rascal_id); 340 | connection.close(done); 341 | }); 342 | }); 343 | }); 344 | 345 | it('should tolerate unsubscribe timeouts when shuting down', (test, done) => { 346 | const config = _.defaultsDeep({ vhosts }, testConfig); 347 | config.vhosts['/'].subscriptions.s1.closeTimeout = 100; 348 | 349 | let timeoutErr; 350 | 351 | createBroker(config, (err, broker) => { 352 | assert.ifError(err); 353 | 354 | broker.subscribe('s1', (err, subscription) => { 355 | assert.ifError(err); 356 | subscription.on('message', () => { 357 | broker.shutdown((err) => { 358 | assert.ifError(err); 359 | assert.strictEqual(timeoutErr.code, 'ETIMEDOUT'); 360 | done(); 361 | }); 362 | }); 363 | }); 364 | 365 | broker.publish('p1', 'test message', (err) => { 366 | assert.ifError(err); 367 | }); 368 | 369 | broker.on('error', (err) => { 370 | timeoutErr = err; 371 | }); 372 | }); 373 | }); 374 | 375 | it('should bounce vhosts', (test, done) => { 376 | const config = _.defaultsDeep({ vhosts }, testConfig); 377 | createBroker(config, (err, broker) => { 378 | assert.ifError(err); 379 | broker.bounce(done); 380 | }); 381 | }); 382 | 383 | it('should tolerate unsubscribe timeouts when bouncing', (test, done) => { 384 | const config = _.defaultsDeep({ vhosts }, testConfig); 385 | config.vhosts['/'].subscriptions.s1.closeTimeout = 100; 386 | 387 | let timeoutErr; 388 | 389 | createBroker(config, (err, broker) => { 390 | assert.ifError(err); 391 | 392 | broker.subscribe('s1', (err, subscription) => { 393 | assert.ifError(err); 394 | subscription.on('message', () => { 395 | broker.bounce((err) => { 396 | assert.ifError(err); 397 | assert.strictEqual(timeoutErr.code, 'ETIMEDOUT'); 398 | done(); 399 | }); 400 | }); 401 | }); 402 | 403 | broker.publish('p1', 'test message', (err) => { 404 | assert.ifError(err); 405 | }); 406 | 407 | broker.on('error', (err) => { 408 | timeoutErr = err; 409 | }); 410 | }); 411 | }); 412 | 413 | it('should purge vhosts', (test, done) => { 414 | const config = _.defaultsDeep({ vhosts }, testConfig); 415 | createBroker(config, (err, broker) => { 416 | assert.ifError(err); 417 | broker.publish('/q1', 'test message', (err) => { 418 | assert.ifError(err); 419 | setTimeout(() => { 420 | broker.purge((err) => { 421 | assert.ifError(err); 422 | amqputils.assertMessageAbsent('q1', namespace, done); 423 | }); 424 | }, 200); 425 | }); 426 | }); 427 | }); 428 | 429 | it( 430 | 'should emit busy/ready events', 431 | (test, done) => { 432 | /* 433 | This test needs to publish messages faster than the channel can cope with in order to 434 | trigger a 'busy' event. It may fail on fast systems. 435 | */ 436 | 437 | if (process.env.CI) return done(); 438 | 439 | const config = _.defaultsDeep({ vhosts }, testConfig); 440 | createBroker(config, (err, broker) => { 441 | assert.ifError(err); 442 | 443 | let busyOn; 444 | let readyOn; 445 | 446 | const stream = random.createRandomStream().on('data', (data) => { 447 | broker.publish('p2', data, (err, publication) => { 448 | if (err) throw err; 449 | publication.on('error', (err) => { 450 | throw err; 451 | }); 452 | }); 453 | }); 454 | 455 | broker.once('busy', () => { 456 | busyOn = Date.now(); 457 | assert.strictEqual(readyOn, undefined); 458 | stream.pause(); 459 | }); 460 | 461 | broker.once('ready', () => { 462 | readyOn = Date.now(); 463 | assert.ok(busyOn <= readyOn); 464 | done(); 465 | }); 466 | }); 467 | }, 468 | { timeout: 60000 }, 469 | ); 470 | 471 | it('should subscribe to all subscriptions', (test, done) => { 472 | const config = _.defaultsDeep( 473 | { 474 | vhosts, 475 | publications, 476 | subscriptions, 477 | }, 478 | testConfig, 479 | ); 480 | 481 | createBroker(config, (err, broker) => { 482 | assert.ifError(err); 483 | broker.subscribeAll((err, subscriptions) => { 484 | assert.ifError(err); 485 | assert.strictEqual(subscriptions.length, 2); 486 | assert.strictEqual(subscriptions[0].constructor.name, 'SubscriberSession'); 487 | assert.strictEqual(subscriptions[0].name, 's1'); 488 | assert.strictEqual(subscriptions[1].name, '/q1'); 489 | done(); 490 | }); 491 | }); 492 | }); 493 | 494 | it('should subscribe to all filtered subscriptions', (test, done) => { 495 | const config = _.defaultsDeep( 496 | { 497 | vhosts, 498 | publications, 499 | subscriptions, 500 | }, 501 | testConfig, 502 | ); 503 | 504 | createBroker(config, (err, broker) => { 505 | assert.ifError(err); 506 | broker.subscribeAll( 507 | (subscriptionConfig) => { 508 | return !subscriptionConfig.autoCreated; 509 | }, 510 | (err, subscriptions) => { 511 | assert.ifError(err); 512 | assert.strictEqual(subscriptions.length, 1); 513 | assert.strictEqual(subscriptions[0].constructor.name, 'SubscriberSession'); 514 | assert.strictEqual(subscriptions[0].name, 's1'); 515 | done(); 516 | }, 517 | ); 518 | }); 519 | }); 520 | 521 | it('should get vhost connections', (test, done) => { 522 | const config = _.defaultsDeep({ vhosts }, testConfig); 523 | createBroker(config, (err, broker) => { 524 | assert.ifError(err); 525 | const connections = broker.getConnections(); 526 | assert.strictEqual(connections.length, 1); 527 | assert.strictEqual(connections[0].vhost, '/'); 528 | assert.strictEqual(connections[0].connectionUrl, 'amqp://guest:***@localhost:5672?heartbeat=50&connection_timeout=10000&channelMax=100', broker.getConnections()['/']); 529 | done(); 530 | }); 531 | }); 532 | 533 | function createBroker(config, components, next) { 534 | if (arguments.length === 2) return createBroker(config, {}, arguments[1]); 535 | Broker.create(config, components, (err, _broker) => { 536 | broker = _broker; 537 | next(err, broker); 538 | }); 539 | } 540 | }, { timeout: 6000 }); 541 | -------------------------------------------------------------------------------- /test/brokerAsPromised.tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const _ = require('lodash'); 3 | const uuid = require('uuid').v4; 4 | const format = require('util').format; 5 | const testConfig = require('../lib/config/tests'); 6 | const BrokerAsPromised = require('..').BrokerAsPromised; 7 | 8 | describe('Broker As Promised', () => { 9 | let broker; 10 | let namespace; 11 | let vhosts; 12 | let publications; 13 | let subscriptions; 14 | 15 | beforeEach(() => { 16 | namespace = uuid(); 17 | 18 | vhosts = { 19 | '/': { 20 | connection: { 21 | management: { 22 | options: { 23 | timeout: 5000, 24 | }, 25 | }, 26 | }, 27 | namespace, 28 | exchanges: { 29 | e1: { 30 | assert: true, 31 | }, 32 | }, 33 | queues: { 34 | q1: { 35 | assert: true, 36 | }, 37 | }, 38 | subscriptions: { 39 | s1: { 40 | queue: 'q1', 41 | }, 42 | }, 43 | publications: { 44 | p1: { 45 | exchange: 'e1', 46 | }, 47 | }, 48 | bindings: { 49 | b1: { 50 | source: 'e1', 51 | destination: 'q1', 52 | }, 53 | }, 54 | }, 55 | }; 56 | 57 | publications = { 58 | p1: { 59 | vhost: '/', 60 | exchange: 'e1', 61 | routingKey: 'foo', 62 | }, 63 | }; 64 | 65 | subscriptions = { 66 | s1: { 67 | vhost: '/', 68 | queue: 'q1', 69 | }, 70 | }; 71 | }); 72 | 73 | afterEach(() => { 74 | if (broker) return broker.nuke(); 75 | }); 76 | 77 | it('should assert vhosts', () => { 78 | const vhostName = uuid(); 79 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 80 | customVhosts[vhostName].assert = true; 81 | 82 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 83 | return createBroker(config); 84 | }); 85 | 86 | it('should fail when checking vhosts that dont exist', () => { 87 | const vhostName = uuid(); 88 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 89 | customVhosts[vhostName].check = true; 90 | 91 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 92 | return createBroker(config).catch((err) => { 93 | assert.ok(err); 94 | assert.strictEqual(err.message, format('Failed to check vhost: %s. http://guest:***@localhost:15672 returned status 404', vhostName)); 95 | }); 96 | }); 97 | 98 | it('should not fail when checking vhosts that do exist', () => { 99 | const vhostName = uuid(); 100 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 101 | customVhosts[vhostName].assert = true; 102 | customVhosts[vhostName].check = true; 103 | 104 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 105 | return createBroker(config); 106 | }); 107 | 108 | it('should delete vhosts', () => { 109 | const vhostName = uuid(); 110 | const customVhosts = _.set({}, vhostName, _.cloneDeep(vhosts)['/']); 111 | customVhosts[vhostName].assert = true; 112 | 113 | const config = _.defaultsDeep({ vhosts: customVhosts }, testConfig); 114 | return createBroker(config).then((broker) => { 115 | return broker.nuke().then(() => { 116 | config.vhosts[vhostName].assert = false; 117 | config.vhosts[vhostName].check = true; 118 | return createBroker(config).catch((err) => { 119 | assert.ok(err); 120 | assert.strictEqual(err.message, format('Failed to check vhost: %s. http://guest:***@localhost:15672 returned status 404', vhostName)); 121 | }); 122 | }); 123 | }); 124 | }); 125 | 126 | it('should provide fully qualified name', () => { 127 | const config = _.defaultsDeep({ vhosts }, testConfig); 128 | return createBroker(config).then((broker) => { 129 | assert.strictEqual(`${namespace}:q1`, broker.getFullyQualifiedName('/', 'q1')); 130 | }); 131 | }); 132 | 133 | it('should not modify configuration', () => { 134 | const config = _.defaultsDeep({ vhosts }, testConfig); 135 | const json = JSON.stringify(config, null, 2); 136 | return createBroker(config).then(() => { 137 | assert.strictEqual(json, JSON.stringify(config, null, 2)); 138 | }); 139 | }); 140 | 141 | it('should nuke', () => { 142 | const config = _.defaultsDeep({ vhosts }, testConfig); 143 | return createBroker(config).then((broker) => { 144 | return broker.nuke(); 145 | }); 146 | }); 147 | 148 | it('should cancel subscriptions', (test, done) => { 149 | const config = _.defaultsDeep( 150 | { 151 | vhosts, 152 | publications, 153 | subscriptions, 154 | }, 155 | testConfig, 156 | ); 157 | 158 | createBroker(config).then((broker) => { 159 | broker.subscribe('s1').then((subscription) => { 160 | subscription.on('message', () => { 161 | assert(false, 'No message should have been received'); 162 | }); 163 | 164 | broker.unsubscribeAll().then(() => { 165 | broker.publish('p1', 'test message').then(() => { 166 | setTimeout(done, 500); 167 | }); 168 | }); 169 | }); 170 | }); 171 | }); 172 | 173 | it('should connect', () => { 174 | const config = _.defaultsDeep({ vhosts }, testConfig); 175 | return createBroker(config).then((broker) => { 176 | return broker.connect('/').then((connection) => { 177 | assert.ok(connection._rascal_id); 178 | return connection.close(); 179 | }); 180 | }); 181 | }); 182 | 183 | it('should subscribe to all subscriptions', () => { 184 | const config = _.defaultsDeep( 185 | { 186 | vhosts, 187 | publications, 188 | subscriptions, 189 | }, 190 | testConfig, 191 | ); 192 | 193 | return createBroker(config).then((broker) => { 194 | return broker.subscribeAll().then((subscriptions) => { 195 | assert.strictEqual(subscriptions.length, 2); 196 | assert.strictEqual(subscriptions[0].constructor.name, 'SubscriberSessionAsPromised'); 197 | assert.strictEqual(subscriptions[0].name, 's1'); 198 | assert.strictEqual(subscriptions[1].name, '/q1'); 199 | }); 200 | }); 201 | }); 202 | 203 | it('should subscribe to all filtered subscriptions', () => { 204 | const config = _.defaultsDeep( 205 | { 206 | vhosts, 207 | publications, 208 | subscriptions, 209 | }, 210 | testConfig, 211 | ); 212 | 213 | return createBroker(config).then((broker) => { 214 | return broker 215 | .subscribeAll((subscriptionConfig) => { 216 | return !subscriptionConfig.autoCreated; 217 | }) 218 | .then((subscriptions) => { 219 | assert.strictEqual(subscriptions.length, 1); 220 | assert.strictEqual(subscriptions[0].constructor.name, 'SubscriberSessionAsPromised'); 221 | assert.strictEqual(subscriptions[0].name, 's1'); 222 | }); 223 | }); 224 | }); 225 | 226 | it('should get vhost connections', () => { 227 | const config = _.defaultsDeep({ vhosts }, testConfig); 228 | return createBroker(config).then((broker) => { 229 | const connections = broker.getConnections(); 230 | assert.strictEqual(connections.length, 1); 231 | assert.strictEqual(connections[0].vhost, '/'); 232 | assert.strictEqual(connections[0].connectionUrl, 'amqp://guest:***@localhost:5672?heartbeat=50&connection_timeout=10000&channelMax=100', broker.getConnections()['/']); 233 | }); 234 | }); 235 | 236 | function createBroker(config) { 237 | return BrokerAsPromised.create(config) 238 | .catch((err) => { 239 | if (err.broker) broker = err[err.broker]; 240 | throw err; 241 | }) 242 | .then((_broker) => { 243 | broker = _broker; 244 | return broker; 245 | }); 246 | } 247 | }, { timeout: 6000 }); 248 | -------------------------------------------------------------------------------- /test/caches/inMemory.tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const async = require('async'); 3 | const inMemory = require('../../lib/counters/inMemory'); 4 | 5 | describe('In Memory Counter', () => { 6 | let counter; 7 | 8 | beforeEach(() => { 9 | counter = inMemory({ size: 3 }); 10 | }); 11 | 12 | it('should return increment and get entries', (test, done) => { 13 | const results = {}; 14 | async.eachSeries( 15 | ['one', 'two', 'one'], 16 | (key, cb) => { 17 | counter.incrementAndGet(key, (err, value) => { 18 | if (err) return cb(err); 19 | results[key] = value; 20 | cb(); 21 | }); 22 | }, 23 | (err) => { 24 | assert.ifError(err); 25 | assert.strictEqual(results.one, 2); 26 | assert.strictEqual(results.two, 1); 27 | done(); 28 | }, 29 | ); 30 | }); 31 | 32 | it('should limit the counter size', (test, done) => { 33 | const results = {}; 34 | async.eachSeries( 35 | ['one', 'two', 'three', 'four', 'one'], 36 | (key, cb) => { 37 | counter.incrementAndGet(key, (err, value) => { 38 | if (err) return cb(err); 39 | results[key] = value; 40 | cb(); 41 | }); 42 | }, 43 | (err) => { 44 | assert.ifError(err); 45 | assert.strictEqual(results.one, 1); 46 | done(); 47 | }, 48 | ); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/shovel.tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const _ = require('lodash'); 3 | const uuid = require('uuid').v4; 4 | const testConfig = require('../lib/config/tests'); 5 | const Broker = require('..').Broker; 6 | 7 | describe('Shovel', () => { 8 | let broker; 9 | let namespace; 10 | let config; 11 | 12 | beforeEach((test, done) => { 13 | namespace = uuid(); 14 | config = { 15 | vhosts: { 16 | '/': { 17 | namespace, 18 | exchanges: { 19 | e1: { 20 | assert: true, 21 | }, 22 | e2: { 23 | assert: true, 24 | }, 25 | }, 26 | queues: { 27 | q1: { 28 | assert: true, 29 | }, 30 | q2: { 31 | assert: true, 32 | }, 33 | }, 34 | bindings: { 35 | b1: { 36 | source: 'e1', 37 | destination: 'q1', 38 | bindingKey: 'foo', 39 | }, 40 | b2: { 41 | source: 'e2', 42 | destination: 'q2', 43 | bindingKey: 'bar', 44 | }, 45 | }, 46 | }, 47 | }, 48 | publications: { 49 | p1: { 50 | exchange: 'e1', 51 | routingKey: 'foo', 52 | }, 53 | p2: { 54 | exchange: 'e2', 55 | routingKey: 'bar', 56 | }, 57 | }, 58 | subscriptions: { 59 | s1: { 60 | queue: 'q1', 61 | }, 62 | s2: { 63 | queue: 'q2', 64 | options: { 65 | noAck: true, 66 | }, 67 | }, 68 | }, 69 | shovels: { 70 | x1: { 71 | subscription: 's1', 72 | publication: 'p2', 73 | }, 74 | }, 75 | }; 76 | done(); 77 | }); 78 | 79 | afterEach((test, done) => { 80 | if (!broker) return done(); 81 | broker.nuke(done); 82 | }); 83 | 84 | it('should transfer message from subscriber to publication', (test, done) => { 85 | createBroker(config, (err, broker) => { 86 | assert.ifError(err); 87 | broker.publish('p1', 'Test Message', assert.ifError); 88 | broker.subscribe('s2', (err, subscription) => { 89 | assert.ifError(err); 90 | subscription.on('message', () => { 91 | done(); 92 | }); 93 | }); 94 | }); 95 | }); 96 | 97 | function createBroker(brokerConfig, next) { 98 | brokerConfig = _.defaultsDeep(brokerConfig, testConfig); 99 | Broker.create(brokerConfig, (err, _broker) => { 100 | broker = _broker; 101 | next(err, broker); 102 | }); 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /test/utils/amqputils.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const assert = require('assert'); 3 | const _ = require('lodash'); 4 | const async = require('async'); 5 | 6 | module.exports = { 7 | init, 8 | }; 9 | 10 | function init(connection) { 11 | function disconnect(next) { 12 | connection.close(next); 13 | } 14 | 15 | function checkExchange(present, name, namespace, next) { 16 | connection.createChannel((err, channel) => { 17 | assert.ifError(err); 18 | channel.checkExchange(`${namespace}:${name}`, (err) => { 19 | present ? assert(!err) : assert(!!err); 20 | next(); 21 | }); 22 | }); 23 | } 24 | 25 | function createQueue(name, namespace, next) { 26 | connection.createChannel((err, channel) => { 27 | assert.ifError(err); 28 | channel.assertQueue(`${namespace}:${name}`, {}, (err) => { 29 | assert.ifError(err); 30 | next(); 31 | }); 32 | }); 33 | } 34 | 35 | function checkQueue(present, name, namespace, next) { 36 | connection.createChannel((err, channel) => { 37 | assert.ifError(err); 38 | channel.checkQueue(`${namespace}:${name}`, (err) => { 39 | present ? assert(!err) : assert(!!err); 40 | next(); 41 | }); 42 | }); 43 | } 44 | 45 | function deleteQueue(name, namespace, next) { 46 | connection.createChannel((err, channel) => { 47 | assert.ifError(err); 48 | channel.deleteQueue(`${namespace}:${name}`, next); 49 | }); 50 | } 51 | 52 | function publishMessage(exchange, namespace, message, options, next) { 53 | _publishMessage(`${namespace}:${exchange}`, message, options, next); 54 | } 55 | 56 | function publishMessageToQueue(queue, namespace, message, options, next) { 57 | options.routingKey = `${namespace}:${queue}`; 58 | _publishMessage('', message, options, next); 59 | } 60 | 61 | function _publishMessage(fqExchange, message, options, next) { 62 | connection.createChannel((err, channel) => { 63 | assert.ifError(err); 64 | channel.publish(fqExchange, options.routingKey, Buffer.from(message), options); 65 | next && next(); 66 | }); 67 | } 68 | 69 | function getMessage(queue, namespace, next) { 70 | connection.createChannel((err, channel) => { 71 | assert.ifError(err); 72 | channel.get(`${namespace}:${queue}`, { noAck: true }, (err, message) => { 73 | if (err) return next(err); 74 | next(null, message); 75 | }); 76 | }); 77 | } 78 | 79 | function assertMessage(queue, namespace, expected, next) { 80 | getMessage(queue, namespace, (err, message) => { 81 | assert.ifError(err); 82 | assert.ok(message, 'Message was not present'); 83 | assert.strictEqual(message.content.toString(), expected); 84 | next(); 85 | }); 86 | } 87 | 88 | function assertMessageAbsent(queue, namespace, next) { 89 | getMessage(queue, namespace, (err, message) => { 90 | assert.ifError(err); 91 | assert.ok(!message, 'Message was present'); 92 | next(); 93 | }); 94 | } 95 | 96 | function waitForConnections(next) { 97 | let connections = []; 98 | let attempts = 0; 99 | async.whilst( 100 | (cb) => { 101 | cb(null, attempts < 100 && connections.length === 0); 102 | }, 103 | (cb) => { 104 | setTimeout(() => { 105 | attempts++; 106 | fetchConnections((err, _connections) => { 107 | if (err) return cb(err); 108 | connections = _connections; 109 | cb(null, connections); 110 | }); 111 | }, 100); 112 | }, 113 | next, 114 | ); 115 | } 116 | 117 | function fetchConnections(next) { 118 | http.get('http://guest:guest@localhost:15672/api/connections', (response) => { 119 | let data = ''; 120 | response.on('data', (chunk) => { 121 | data += chunk; 122 | }).on('end', () => { 123 | next(null, JSON.parse(data)); 124 | }); 125 | }).on('error', next).end(); 126 | } 127 | 128 | function closeConnections(connections, reason, next) { 129 | async.each(connections, (connection, cb) => { 130 | closeConnection(connection.name, reason, cb); 131 | }, next); 132 | } 133 | 134 | function closeConnection(name, reason, next) { 135 | const headers = { 136 | 'x-reason': reason, 137 | }; 138 | http.request(`http://guest:guest@localhost:15672/api/connections/${name}`, { method: 'delete', headers }, () => { 139 | next(); 140 | }).on('error', next).end(); 141 | } 142 | 143 | return { 144 | disconnect, 145 | checkExchange: _.curry(checkExchange), 146 | createQueue, 147 | checkQueue: _.curry(checkQueue), 148 | deleteQueue, 149 | publishMessage: _.curry(publishMessage), 150 | publishMessageToQueue, 151 | getMessage: _.curry(getMessage), 152 | assertMessage: _.curry(assertMessage), 153 | assertMessageAbsent: _.curry(assertMessageAbsent), 154 | assertExchangePresent: checkExchange.bind(null, true), 155 | assertExchangeAbsent: checkExchange.bind(null, false), 156 | assertQueuePresent: checkQueue.bind(null, true), 157 | assertQueueAbsent: checkQueue.bind(null, false), 158 | closeConnections, 159 | waitForConnections, 160 | }; 161 | } 162 | -------------------------------------------------------------------------------- /test/vhost.tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const async = require('async'); 3 | const _ = require('lodash'); 4 | const amqplib = require('amqplib/callback_api'); 5 | const format = require('util').format; 6 | const uuid = require('uuid').v4; 7 | const testConfig = require('../lib/config/tests'); 8 | const Broker = require('..').Broker; 9 | const AmqpUtils = require('./utils/amqputils'); 10 | 11 | describe('Vhost', () => { 12 | let broker; 13 | let amqputils; 14 | 15 | beforeEach((test, done) => { 16 | amqplib.connect((err, connection) => { 17 | if (err) return done(err); 18 | amqputils = AmqpUtils.init(connection); 19 | done(); 20 | }); 21 | }); 22 | 23 | afterEach((test, done) => { 24 | amqputils.disconnect(() => { 25 | if (broker) return broker.nuke(done); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should timeout connections', (test, done) => { 31 | const namespace = uuid(); 32 | createBroker( 33 | { 34 | vhosts: { 35 | '/': { 36 | connection: { 37 | hostname: '10.255.255.1', 38 | socketOptions: { 39 | timeout: 100, 40 | }, 41 | }, 42 | namespace, 43 | }, 44 | }, 45 | }, 46 | (err) => { 47 | assert.ok(err.message.match('connect ETIMEDOUT')); 48 | done(); 49 | }, 50 | ); 51 | }); 52 | 53 | it('should create exchanges', (test, done) => { 54 | const namespace = uuid(); 55 | createBroker( 56 | { 57 | vhosts: { 58 | '/': { 59 | namespace, 60 | exchanges: { 61 | e1: { 62 | assert: true, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | (err) => { 69 | assert.ifError(err); 70 | amqputils.assertExchangePresent('e1', namespace, done); 71 | }, 72 | ); 73 | }); 74 | 75 | it('should create objects concurrently', (test, done) => { 76 | // This test is too slow for CI 77 | if (process.env.CI) return done(); 78 | 79 | function createAllTheThings(concurrency, cb) { 80 | const namespace = uuid(); 81 | const exchanges = new Array(100) 82 | .fill() 83 | .map((_, index) => `e${index + 1}`) 84 | .reduce( 85 | (acc, name) => Object.assign(acc, { 86 | [name]: { 87 | assert: true, 88 | }, 89 | }), 90 | {}, 91 | ); 92 | 93 | const queues = new Array(100) 94 | .fill() 95 | .map((_, index) => `q${index + 1}`) 96 | .reduce( 97 | (acc, name) => Object.assign(acc, { 98 | [name]: { 99 | assert: true, 100 | }, 101 | }), 102 | {}, 103 | ); 104 | 105 | const bindings = new Array(100).fill().map((_, index) => `e${index + 1}[a.b.c] -> q${index + 1}`); 106 | 107 | const before = Date.now(); 108 | createBroker( 109 | { 110 | vhosts: { 111 | '/': { 112 | concurrency, 113 | namespace, 114 | exchanges, 115 | queues, 116 | bindings, 117 | }, 118 | }, 119 | }, 120 | (err) => { 121 | assert.ifError(err); 122 | const after = Date.now(); 123 | amqputils.assertExchangePresent('e100', namespace, (err) => { 124 | if (err) return cb(err); 125 | broker.nuke((err) => { 126 | cb(err, after - before); 127 | }); 128 | }); 129 | }, 130 | ); 131 | } 132 | 133 | const reps = 5; 134 | const serialTest = (n, cb) => createAllTheThings(1, cb); 135 | const concurrentTest = (n, cb) => createAllTheThings(10, cb); 136 | async.series([(cb) => async.timesSeries(reps, serialTest, cb), (cb) => async.timesSeries(reps, concurrentTest, cb)], (err, results) => { 137 | if (err) return done(err); 138 | const [a, b] = results; 139 | const averageA = a.reduce((a, b) => a + b, 0) / reps; 140 | const averageB = b.reduce((a, b) => a + b, 0) / reps; 141 | assert.ok(averageB < averageA / 2); 142 | return done(); 143 | }); 144 | }, { timeout: 60000 }); 145 | 146 | it('should create queues', (test, done) => { 147 | const namespace = uuid(); 148 | createBroker( 149 | { 150 | vhosts: { 151 | '/': { 152 | namespace, 153 | queues: { 154 | q1: { 155 | assert: true, 156 | }, 157 | }, 158 | }, 159 | }, 160 | }, 161 | (err) => { 162 | assert.ifError(err); 163 | amqputils.assertQueuePresent('q1', namespace, done); 164 | }, 165 | ); 166 | }); 167 | 168 | it('should fail when checking a missing exchange', (test, done) => { 169 | createBroker( 170 | { 171 | vhosts: { 172 | '/': { 173 | exchanges: { 174 | e1: { 175 | assert: false, 176 | check: true, 177 | }, 178 | }, 179 | }, 180 | }, 181 | }, 182 | (err) => { 183 | assert.ok(err); 184 | assert.ok(/NOT-FOUND/.test(err.message), format('%s did not match the expected format', err.message)); 185 | done(); 186 | }, 187 | ); 188 | }); 189 | 190 | it('should fail when checking a missing queue', (test, done) => { 191 | createBroker( 192 | { 193 | vhosts: { 194 | '/': { 195 | queues: { 196 | q1: { 197 | assert: false, 198 | check: true, 199 | }, 200 | }, 201 | }, 202 | }, 203 | }, 204 | (err) => { 205 | assert.ok(err); 206 | assert.ok(/NOT-FOUND/.test(err.message), format('%s did not match the expected format', err.message)); 207 | done(); 208 | }, 209 | ); 210 | }); 211 | 212 | it('should create bindings', (test, done) => { 213 | const namespace = uuid(); 214 | 215 | createBroker( 216 | { 217 | vhosts: { 218 | '/': { 219 | namespace, 220 | exchanges: { 221 | e1: { 222 | assert: true, 223 | }, 224 | e2: { 225 | assert: true, 226 | }, 227 | }, 228 | queues: { 229 | q1: { 230 | assert: true, 231 | }, 232 | }, 233 | bindings: { 234 | b1: { 235 | source: 'e1', 236 | destination: 'e2', 237 | destinationType: 'exchange', 238 | }, 239 | b2: { 240 | source: 'e1', 241 | destination: 'q1', 242 | }, 243 | }, 244 | }, 245 | }, 246 | }, 247 | (err) => { 248 | assert.ifError(err); 249 | amqputils.publishMessage('e1', namespace, 'test message', {}, (err) => { 250 | assert.ifError(err); 251 | amqputils.assertMessage('q1', namespace, 'test message', done); 252 | }); 253 | }, 254 | ); 255 | }); 256 | 257 | it('should reconnect on error', (test, done) => { 258 | createBroker({ 259 | vhosts: { 260 | '/': { 261 | queues: ['q'], 262 | }, 263 | }, 264 | }, (err, broker) => { 265 | assert.ifError(err); 266 | 267 | broker.on('error', (err) => { 268 | assert.equal(err.message, 'Connection closed: 320 (CONNECTION-FORCED) with message "CONNECTION_FORCED - VHOST TEST"'); 269 | broker.on('connect', () => done()); 270 | }); 271 | 272 | amqputils.waitForConnections((err, connections) => { 273 | amqputils.closeConnections(connections, 'VHOST TEST', (err) => { 274 | assert.ifError(err); 275 | }); 276 | }); 277 | }); 278 | }, { timeout: 10000 }); 279 | 280 | function createBroker(config, next) { 281 | config = _.defaultsDeep(config, testConfig); 282 | Broker.create(config, (err, _broker) => { 283 | broker = _broker; 284 | next(err, broker); 285 | }); 286 | } 287 | }, { timeout: 2000 }); 288 | --------------------------------------------------------------------------------