UNPKG

amqp-connection-manager

Version:
313 lines (312 loc) 13.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const amqp = __importStar(require("amqplib")); const events_1 = require("events"); const promise_breaker_1 = __importDefault(require("promise-breaker")); const url_1 = require("url"); const ChannelWrapper_js_1 = __importDefault(require("./ChannelWrapper.js")); const helpers_js_1 = require("./helpers.js"); // Default heartbeat time. const HEARTBEAT_IN_SECONDS = 5; /* istanbul ignore next */ function neverThrows() { return (err) => setImmediate(() => { throw new Error(`AmqpConnectionManager - should never get here: ${err.message}\n` + err.stack); }); } // // Events: // * `connect({connection, url})` - Emitted whenever we connect to a broker. // * `connectFailed({err, url})` - Emitted whenever we fail to connect to a broker. // * `disconnect({err})` - Emitted whenever we disconnect from a broker. // * `blocked({reason})` - Emitted whenever connection is blocked by a broker. // * `unblocked()` - Emitted whenever connection is unblocked by a broker. // class AmqpConnectionManager extends events_1.EventEmitter { /** * Create a new AmqplibConnectionManager. * * @param urls - An array of brokers to connect to. * Takes url strings or objects {url: string, connectionOptions?: object} * If present, a broker's [connectionOptions] will be used instead * of [options.connectionOptions] when passed to the amqplib connect method. * AmqplibConnectionManager will round-robin between them whenever it * needs to create a new connection. * @param [options={}] - * @param [options.heartbeatIntervalInSeconds=5] - The interval, * in seconds, to send heartbeats. * @param [options.reconnectTimeInSeconds] - The time to wait * before trying to reconnect. If not specified, defaults to * `heartbeatIntervalInSeconds`. * @param [options.connectionOptions] - Passed to the amqplib * connect method. * @param [options.findServers] - A `fn(callback)` or a `fn()` * which returns a Promise. This should resolve to one or more servers * to connect to, either a single URL or an array of URLs. This is handy * when you're using a service discovery mechanism such as Consul or etcd. * Note that if this is supplied, then `urls` is ignored. */ constructor(urls, options = {}) { super(); this._closed = false; if (!urls && !options.findServers) { throw new Error('Must supply either `urls` or `findServers`'); } this._channels = []; this._currentUrl = 0; this.connectionOptions = options.connectionOptions; this.heartbeatIntervalInSeconds = options.heartbeatIntervalInSeconds || options.heartbeatIntervalInSeconds === 0 ? options.heartbeatIntervalInSeconds : HEARTBEAT_IN_SECONDS; this.reconnectTimeInSeconds = options.reconnectTimeInSeconds || this.heartbeatIntervalInSeconds; // There will be one listener per channel, and there could be a lot of channels, so disable warnings from node. this.setMaxListeners(0); this._findServers = options.findServers || (() => Promise.resolve(urls)); } /** * Start the connect retries and await the first connect result. Even if the initial connect fails or timeouts, the * reconnect attempts will continue in the background. * @param [options={}] - * @param [options.timeout] - Time to wait for initial connect */ async connect({ timeout } = {}) { this._connect(); let reject; const onConnectFailed = ({ err }) => { // Ignore disconnects caused bad credentials. if (err.message.includes('ACCESS-REFUSED') || err.message.includes('403')) { reject(err); } }; let waitTimeout; if (timeout) { waitTimeout = (0, helpers_js_1.wait)(timeout); } try { await Promise.race([ (0, events_1.once)(this, 'connect'), new Promise((_resolve, innerReject) => { reject = innerReject; this.on('connectFailed', onConnectFailed); }), ...(waitTimeout ? [ waitTimeout.promise.then(() => { throw new Error('amqp-connection-manager: connect timeout'); }), ] : []), ]); } finally { waitTimeout === null || waitTimeout === void 0 ? void 0 : waitTimeout.cancel(); this.removeListener('connectFailed', onConnectFailed); } } // `options` here are any options that can be passed to ChannelWrapper. createChannel(options = {}) { const channel = new ChannelWrapper_js_1.default(this, options); this._channels.push(channel); channel.once('close', () => { this._channels = this._channels.filter((c) => c !== channel); }); return channel; } close() { if (this._closed) { return Promise.resolve(); } this._closed = true; if (this._cancelRetriesHandler) { this._cancelRetriesHandler(); this._cancelRetriesHandler = undefined; } return Promise.resolve(this._connectPromise).then(() => { return Promise.all(this._channels.map((channel) => channel.close())) .catch(function () { // Ignore errors closing channels. }) .then(() => { this._channels = []; if (this._currentConnection) { this._currentConnection.removeAllListeners('close'); return this._currentConnection.close(); } else { return null; } }) .then(() => { this._currentConnection = undefined; }); }); } isConnected() { return !!this._currentConnection; } /** Force reconnect - noop unless connected */ reconnect() { if (this._closed) { throw new Error('cannot reconnect after close'); } // If we have a connection, close it and immediately connect again. // Wait for ordinary reconnect otherwise. if (this._currentConnection) { this._currentConnection.removeAllListeners(); this._currentConnection .close() .catch(() => { // noop }) .then(() => { this._currentConnection = undefined; this.emit('disconnect', { err: new Error('forced reconnect') }); return this._connect(); }) .catch(neverThrows); } } /** The current connection. */ get connection() { return this._currentConnection; } /** Returns the number of registered channels. */ get channelCount() { return this._channels.length; } _connect() { if (this._connectPromise) { return this._connectPromise; } if (this._closed || this.isConnected()) { return Promise.resolve(null); } let attemptedUrl; const result = (this._connectPromise = Promise.resolve() .then(() => { if (!this._urls || this._currentUrl >= this._urls.length) { this._currentUrl = 0; return promise_breaker_1.default.call(this._findServers, 0, null); } else { return this._urls; } }) .then((urls) => { var _a; if (Array.isArray(urls)) { this._urls = urls; } else if (urls) { this._urls = [urls]; } if (!this._urls || this._urls.length === 0) { throw new Error('amqp-connection-manager: No servers found'); } // Round robin between brokers const url = this._urls[this._currentUrl]; this._currentUrl++; // Set connectionOptions to the setting in the class instance (which came via the constructor) let connectionOptions = this.connectionOptions; let originalUrl; let connect; if (typeof url === 'object' && 'url' in url) { originalUrl = connect = url.url; // If URL is an object, pull out any specific URL connectionOptions for it or use the // instance connectionOptions if none were provided for this specific URL. connectionOptions = url.connectionOptions || this.connectionOptions; } else if (typeof url === 'string') { originalUrl = connect = url; } else { originalUrl = url; connect = { ...url, heartbeat: (_a = url.heartbeat) !== null && _a !== void 0 ? _a : this.heartbeatIntervalInSeconds, }; } attemptedUrl = originalUrl; // Add the `heartbeastIntervalInSeconds` to the connection options. if (typeof connect === 'string') { const u = new url_1.URL(connect); if (!u.searchParams.get('heartbeat')) { u.searchParams.set('heartbeat', `${this.heartbeatIntervalInSeconds}`); } connect = u.toString(); } return amqp.connect(connect, connectionOptions).then((connection) => { this._currentConnection = connection; //emit 'blocked' when RabbitMQ server decides to block the connection (resources running low) connection.on('blocked', (reason) => this.emit('blocked', { reason })); connection.on('unblocked', () => this.emit('unblocked')); connection.on('error', ( /* err */) => { // if this event was emitted, then the connection was already closed, // so no need to call #close here // also, 'close' is emitted after 'error', // so no need for work already done in 'close' handler }); // Reconnect if the connection closes connection.on('close', (err) => { this._currentConnection = undefined; this.emit('disconnect', { err }); const handle = (0, helpers_js_1.wait)(this.reconnectTimeInSeconds * 1000); this._cancelRetriesHandler = handle.cancel; handle.promise .then(() => this._connect()) // `_connect()` should never throw. .catch(neverThrows); }); this._connectPromise = undefined; this.emit('connect', { connection, url: originalUrl }); // Need to return null here, or Bluebird will complain - #171. return null; }); }) .catch((err) => { this.emit('connectFailed', { err, url: attemptedUrl }); // Connection failed... this._currentConnection = undefined; this._connectPromise = undefined; let handle; if (err.name === 'OperationalError' && err.message === 'connect ETIMEDOUT') { handle = (0, helpers_js_1.wait)(0); } else { handle = (0, helpers_js_1.wait)(this.reconnectTimeInSeconds * 1000); } this._cancelRetriesHandler = handle.cancel; return handle.promise.then(() => this._connect()); })); return result; } } exports.default = AmqpConnectionManager;