Skip to content

Commit 449f5db

Browse files
authored
Merge pull request #181 from luddd3/feat/expose-amqpconnectionmanager
feat: expose AmqpConnectionManagerClass
2 parents b2c89ac + 835a81f commit 449f5db

File tree

10 files changed

+406
-24
lines changed

10 files changed

+406
-24
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# [3.6.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.5.2...v3.6.0) (2021-08-27)
2+
3+
### Features
4+
5+
- reconnect and cancelAll consumers ([fb0c00b](https://github.com/jwalton/node-amqp-connection-manager/commit/fb0c00becc224ffedd28e810cbb314187d21efdb))
6+
17
## [3.5.2](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.5.1...v3.5.2) (2021-08-26)
28

39
### Bug Fixes

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "amqp-connection-manager",
3-
"version": "3.5.2",
3+
"version": "3.6.0",
44
"description": "Auto-reconnect and round robin support for amqplib.",
55
"module": "./dist/esm/index.js",
66
"main": "./dist/cjs/index.js",

src/AmqpConnectionManager.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import amqp, { Connection } from 'amqplib';
2-
import { EventEmitter } from 'events';
2+
import { EventEmitter, once } from 'events';
33
import { TcpSocketConnectOpts } from 'net';
44
import pb from 'promise-breaker';
55
import { ConnectionOptions } from 'tls';
@@ -82,6 +82,8 @@ export interface IAmqpConnectionManager {
8282
addListener(event: 'unblocked', listener: () => void): this;
8383
addListener(event: 'disconnect', listener: (arg: { err: Error }) => void): this;
8484

85+
listeners(eventName: string | symbol): any;
86+
8587
on(event: string, listener: (...args: any[]) => void): this;
8688
on(event: 'connect', listener: ConnectListener): this;
8789
on(event: 'blocked', listener: (arg: { reason: string }) => void): this;
@@ -108,6 +110,8 @@ export interface IAmqpConnectionManager {
108110

109111
removeListener(event: string, listener: (...args: any[]) => void): this;
110112

113+
connect(options?: { timeout?: number }): Promise<void>;
114+
reconnect(): void;
111115
createChannel(options?: CreateChannelOpts): ChannelWrapper;
112116
close(): Promise<void>;
113117
isConnected(): boolean;
@@ -196,8 +200,43 @@ export default class AmqpConnectionManager extends EventEmitter implements IAmqp
196200
this.setMaxListeners(0);
197201

198202
this._findServers = options.findServers || (() => Promise.resolve(urls));
203+
}
199204

205+
/**
206+
* Start the connect retries and await the first connect result. Even if the initial connect fails or timeouts, the
207+
* reconnect attempts will continue in the background.
208+
* @param [options={}] -
209+
* @param [options.timeout] - Time to wait for initial connect
210+
*/
211+
async connect({ timeout }: { timeout?: number } = {}): Promise<void> {
200212
this._connect();
213+
214+
let reject: (reason?: any) => void;
215+
const onDisconnect = ({ err }: { err: any }) => {
216+
// Ignore disconnects caused by dead servers etc., but throw on operational errors like bad credentials.
217+
if (err.isOperational) {
218+
reject(err);
219+
}
220+
};
221+
222+
try {
223+
await Promise.race([
224+
once(this, 'connect'),
225+
new Promise((_resolve, innerReject) => {
226+
reject = innerReject;
227+
this.on('disconnect', onDisconnect);
228+
}),
229+
...(timeout
230+
? [
231+
wait(timeout).promise.then(() => {
232+
throw new Error('amqp-connection-manager: connect timeout');
233+
}),
234+
]
235+
: []),
236+
]);
237+
} finally {
238+
this.removeListener('disconnect', onDisconnect);
239+
}
201240
}
202241

203242
// `options` here are any options that can be passed to ChannelWrapper.

src/ChannelWrapper.ts

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ interface SendToQueueMessage {
4444
reject: (err: Error) => void;
4545
}
4646

47+
interface ConsumerOptions extends amqplib.Options.Consume {
48+
prefetch?: number
49+
}
50+
51+
interface Consumer {
52+
consumerTag: string | null;
53+
queue: string;
54+
onMessage: (msg: amqplib.ConsumeMessage) => void;
55+
options: ConsumerOptions;
56+
}
57+
4758
type Message = PublishMessage | SendToQueueMessage;
4859

4960
const IRRECOVERABLE_ERRORS = [
@@ -87,6 +98,8 @@ export default class ChannelWrapper extends EventEmitter {
8798
private _unconfirmedMessages: Message[] = [];
8899
/** Reason code during publish or sendtoqueue messages. */
89100
private _irrecoverableCode: number | undefined;
101+
/** Consumers which will be reconnected on channel errors etc. */
102+
private _consumers: Consumer[] = [];
90103

91104
/**
92105
* The currently connected channel. Note that not all setup functions
@@ -324,6 +337,8 @@ export default class ChannelWrapper extends EventEmitter {
324337

325338
// Array of setup functions to call.
326339
this._setups = [];
340+
this._consumers = [];
341+
327342
if (options.setup) {
328343
this._setups.push(options.setup);
329344
}
@@ -359,10 +374,13 @@ export default class ChannelWrapper extends EventEmitter {
359374
this.emit('error', err, { name: this.name });
360375
})
361376
)
362-
).then(() => {
363-
this._settingUp = undefined;
364-
});
365-
377+
)
378+
.then(() => {
379+
return Promise.all(this._consumers.map((c) => this._reconnectConsumer(c)));
380+
})
381+
.then(() => {
382+
this._settingUp = undefined;
383+
});
366384
await this._settingUp;
367385

368386
if (!this._channel) {
@@ -581,6 +599,89 @@ export default class ChannelWrapper extends EventEmitter {
581599
}
582600
}
583601

602+
/**
603+
* Setup a consumer
604+
* This consumer will be reconnected on cancellation and channel errors.
605+
*/
606+
async consume(
607+
queue: string,
608+
onMessage: Consumer['onMessage'],
609+
options: ConsumerOptions = {}
610+
): Promise<void> {
611+
const consumer: Consumer = {
612+
consumerTag: null,
613+
queue,
614+
onMessage,
615+
options,
616+
};
617+
this._consumers.push(consumer);
618+
await this._consume(consumer);
619+
}
620+
621+
private async _consume(consumer: Consumer): Promise<void> {
622+
if (!this._channel) {
623+
return;
624+
}
625+
626+
const { prefetch, ...options } = consumer.options;
627+
if (typeof prefetch === 'number') {
628+
this._channel.prefetch(prefetch, false);
629+
}
630+
631+
const { consumerTag } = await this._channel.consume(
632+
consumer.queue,
633+
(msg) => {
634+
if (!msg) {
635+
consumer.consumerTag = null;
636+
this._reconnectConsumer(consumer).catch((err) => {
637+
if (err.isOperational && err.message.includes('BasicConsume; 404')) {
638+
// Ignore errors caused by queue not declared. In
639+
// those cases the connection will reconnect and
640+
// then consumers reestablished. The full reconnect
641+
// might be avoided if we assert the queue again
642+
// before starting to consume.
643+
return;
644+
}
645+
throw err;
646+
});
647+
return;
648+
}
649+
consumer.onMessage(msg);
650+
},
651+
options
652+
);
653+
consumer.consumerTag = consumerTag;
654+
}
655+
656+
private async _reconnectConsumer(consumer: Consumer): Promise<void> {
657+
if (!this._consumers.includes(consumer)) {
658+
// Intentionally canceled
659+
return;
660+
}
661+
await this._consume(consumer);
662+
}
663+
664+
/**
665+
* Cancel all consumers
666+
*/
667+
async cancelAll(): Promise<void> {
668+
const consumers = this._consumers;
669+
this._consumers = [];
670+
if (!this._channel) {
671+
return;
672+
}
673+
674+
const channel = this._channel;
675+
await Promise.all(
676+
consumers.reduce<any[]>((acc, consumer) => {
677+
if (consumer.consumerTag) {
678+
acc.push(channel.cancel(consumer.consumerTag));
679+
}
680+
return acc;
681+
}, [])
682+
);
683+
}
684+
584685
/** Send an `ack` to the underlying channel. */
585686
ack(message: amqplib.Message, allUpTo?: boolean): void {
586687
this._channel && this._channel.ack(message, allUpTo);

src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@ export function connect(
1515
urls: ConnectionUrl | ConnectionUrl[] | undefined | null,
1616
options?: AmqpConnectionManagerOptions
1717
): IAmqpConnectionManager {
18-
return new AmqpConnectionManager(urls, options);
18+
const conn = new AmqpConnectionManager(urls, options);
19+
conn.connect().catch(() => {
20+
/* noop */
21+
});
22+
return conn;
1923
}
2024

25+
export { AmqpConnectionManager as AmqpConnectionManagerClass };
26+
2127
const amqp = { connect };
2228

2329
export default amqp;

0 commit comments

Comments
 (0)