rascal 18.0.0 → 19.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Change Log
2
2
 
3
+ ## 19.0.0
4
+ - I am not aware of any breaking changes in this release, but emitting error events asynchronously could have subtle side effects, hence the major release
5
+ - Deprecate session 'cancelled' event in favour of 'cancel' (both will work)
6
+ - Refactor reconnection and resubscription code
7
+ - Emit errors asynchronously to prevent them being caught by the amqplib main accept loop
8
+ - Fix bug which throw an exception in the error handler when a close event was emitted with no error argument
9
+
10
+ ## 18.0.1
11
+
12
+ - Removed console.log when the channel pool destroyed a channel
13
+ - Add streams example
14
+
3
15
  ## 18.0.0
4
16
 
5
17
  - Fixes https://github.com/onebeyond/rascal/issues/227 by requiring special characters to be URL encoded.
package/README.md CHANGED
@@ -130,7 +130,9 @@ The reason Rascal nacks the message is because the alternatives are to leave the
130
130
 
131
131
  ## Very Important Section About Event Handling
132
132
 
133
- [amqplib](https://www.npmjs.com/package/amqplib) emits error events when a connection or channel encounters a problem. Rascal will listen for these and provided you use the default configuration will attempt automatic recovery (reconnection etc), however these events can indicate errors in your code, so it's also important to bring them to your attention. Rascal does this by re-emitting the error event, which means if you don't handle them, they will bubble up to the uncaught error handler and crash your application. There are four places where you should do this
133
+ [amqplib](https://www.npmjs.com/package/amqplib) emits error events when a connection or channel encounters a problem. Rascal will listen for these and provided you use the default configuration will attempt automatic recovery (reconnection etc), however these events can indicate errors in your code, so it's also important to bring them to your attention. Rascal does this by re-emitting the error event, which means if you don't handle them, they will bubble up to the uncaught error handler and crash your application. It is insufficient to register a global uncaughtException handler - doing so without registering individual handlers will prevent your application from crashing, but also prevent Rascal from recovering.
134
+
135
+ There are four places where you need to register error handlers.
134
136
 
135
137
  1. Immediately after obtaining a broker instance
136
138
 
@@ -753,6 +755,62 @@ To define a queue with extensions such as `x-queue-type` add arguments to the op
753
755
 
754
756
  Refer to the [amqplib](https://www.squaremobius.net/amqp.node/channel_api.html) documentation for further queue options.
755
757
 
758
+ #### streams
759
+
760
+ Rascal supports [RabbitMQ Streams](https://www.rabbitmq.com/docs/streams) via x-queue-type argument, i.e.
761
+
762
+ ```json
763
+ {
764
+ "queues": {
765
+ "q1": {
766
+ "options": {
767
+ "arguments": {
768
+ "x-queue-type": "stream"
769
+ }
770
+ }
771
+ }
772
+ }
773
+ }
774
+ ```
775
+
776
+ The [Stream Plugin](https://www.rabbitmq.com/docs/stream) and associated binary protocol extension are not supported.
777
+
778
+ Streams are **not** a replacement for regular messaging - instead they are best suited for when you can tolerate occasional message loss and need for higher throughput, such as sampling web based analytics.
779
+
780
+ When working with streams you need to think carefully about [data retention](https://www.rabbitmq.com/docs/streams#retention). Unless you specify retention configuration, messages will never be deleted and eventually you will run out of space. Conversely, if you automatically delete messages based on queue size or age, they may be lost without ever being read.
781
+
782
+ You also need to think about how you will [track the consumer offset](https://www.rabbitmq.com/blog/2021/09/13/rabbitmq-streams-offset-tracking). Typically you will need to store this in a database after successfully processing the message and use it to tell the broker where to resume from after your application restarts. For example...
783
+
784
+ ```js
785
+ const initialOffset = (await loadOffset('/my-queue')) || 'first';
786
+
787
+ const overrides = {
788
+ options: {
789
+ arguments: {
790
+ 'x-stream-offset': initialOffset
791
+ }
792
+ }
793
+ };
794
+
795
+ const subscription = await broker.subscribe('/my-queue', overrides);
796
+
797
+ subscription.on('message', async (message, content, ackOrNack) => {
798
+ const currentOffset = message.properties.headers['x-stream-offset'];
799
+ try {
800
+ await handleMessage(content);
801
+ await updateOffset('/my-queue', currentOffset);
802
+ } catch (err) {
803
+ await handleError('/my-queue', currentOffset, err);
804
+ } finally {
805
+ ackOrNack(); // Streams do not support nack so do not pass the error argument
806
+ }
807
+ });
808
+ ```
809
+
810
+ However, if your application is offline for too long, and messages are still being published to the stream, it may not be able to resume from where you left off, since those messages may have been deleted. Furthermore, if your application consumes messages concurrently, you need to think about how you will recover should one fail. If you naively override the previouly saved offset, you may be replacing a higher/later offset with an lower/older one, causing in your application to restart from the wrong point. Finally, you also need to decide what to do if the message cannot be processed. You cannot simply replay the message since you are working with a stream, rather than a queue. You could cancel the subscription and resume from the current offset, but this will lead to duplicates if you have been consuming messages concurrently. Alternatively you could republish the failures to a dead letter queue and process them separately.
811
+
812
+ For the above reasons, we only recommend considering streams when you genuinely need the extra throughput.
813
+
756
814
  #### bindings
757
815
 
758
816
  You can bind exchanges to exchanges, or exchanges to queues.
@@ -1302,7 +1360,7 @@ If the message has not been auto-acknowledged you should ackOrNack it. **If you
1302
1360
 
1303
1361
  The RabbitMQ broker may [cancel](https://www.rabbitmq.com/consumer-cancel.html) the consumer if the queue is deleted or the node on which the queue is located fails. [amqplib](https://www.squaremobius.net/amqp.node/channel_api.html#channel_consume) handles this by delivering a `null` message. When Rascal receives the null message it will
1304
1362
 
1305
- 1. Emit a `cancelled` event from the subscription.
1363
+ 1. Emit a `cancel` event from the subscription.
1306
1364
  1. Emit an `error` event from the subscription if the `cancel` event was not handled
1307
1365
  1. Optionally attempt to resubscribe as per normal retry configuration. If the queue was deleted rather than being failed over, the queue will not automatically be re-created and retry attempts will fail indefinitely.
1308
1366
 
@@ -0,0 +1,13 @@
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
+ }
@@ -0,0 +1,14 @@
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
+ }
@@ -0,0 +1,37 @@
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
+ }
@@ -0,0 +1,38 @@
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
+ });
@@ -0,0 +1,28 @@
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
+ }
@@ -0,0 +1,25 @@
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
+ });
@@ -95,24 +95,27 @@ function Publication(vhost, borrowChannelFn, returnChannelFn, destroyChannelFn,
95
95
  session._removePausedListener();
96
96
  if (err) return session.emit('error', err, messageId);
97
97
  if (session.isAborted()) return abortPublish(channel, messageId);
98
- const errorHandler = _.once(handleChannelError.bind(null, channel, messageId, session, config));
98
+
99
+ const disconnectionHandler = makeDisconnectionHandler(channel, messageId, session, config);
99
100
  const returnHandler = session.emit.bind(session, 'return');
100
- addListeners(channel, errorHandler, returnHandler);
101
+ addListeners(channel, disconnectionHandler, returnHandler);
102
+
101
103
  try {
102
104
  session._startPublish();
105
+
103
106
  publishFn(channel, buffer, publishConfig, (err, ok) => {
104
107
  session._endPublish();
105
108
  if (err) {
106
- destroyChannel(channel, errorHandler, returnHandler);
109
+ destroyChannel(channel, disconnectionHandler, returnHandler);
107
110
  return session.emit('error', err, messageId);
108
111
  }
109
112
 
110
- ok ? returnChannel(channel, errorHandler, returnHandler) : deferReturnChannel(channel, errorHandler, returnHandler);
113
+ ok ? returnChannel(channel, disconnectionHandler, returnHandler) : deferReturnChannel(channel, disconnectionHandler, returnHandler);
111
114
 
112
115
  session.emit('success', messageId);
113
116
  });
114
117
  } catch (err) {
115
- returnChannel(channel, errorHandler, returnHandler);
118
+ returnChannel(channel, disconnectionHandler, returnHandler);
116
119
  return session.emit('error', err, messageId);
117
120
  }
118
121
  });
@@ -125,19 +128,19 @@ function Publication(vhost, borrowChannelFn, returnChannelFn, destroyChannelFn,
125
128
  returnChannelFn(channel);
126
129
  }
127
130
 
128
- function returnChannel(channel, errorHandler, returnHandler) {
129
- removeListeners(channel, errorHandler, returnHandler);
131
+ function returnChannel(channel, disconnectionHandler, returnHandler) {
132
+ removeListeners(channel, disconnectionHandler, returnHandler);
130
133
  returnChannelFn(channel);
131
134
  }
132
135
 
133
- function deferReturnChannel(channel, errorHandler, returnHandler) {
136
+ function deferReturnChannel(channel, disconnectionHandler, returnHandler) {
134
137
  channel.once('drain', () => {
135
- returnChannel(channel, errorHandler, returnHandler);
138
+ returnChannel(channel, disconnectionHandler, returnHandler);
136
139
  });
137
140
  }
138
141
 
139
- function destroyChannel(channel, errorHandler, returnHandler) {
140
- removeListeners(channel, errorHandler, returnHandler);
142
+ function destroyChannel(channel, disconnectionHandler, returnHandler) {
143
+ removeListeners(channel, disconnectionHandler, returnHandler);
141
144
  destroyChannelFn(channel);
142
145
  }
143
146
 
@@ -163,19 +166,29 @@ function Publication(vhost, borrowChannelFn, returnChannelFn, destroyChannelFn,
163
166
  }
164
167
  }
165
168
 
166
- function addListeners(channel, errorHandler, returnHandler) {
167
- channel.on('error', errorHandler);
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);
168
181
  channel.on('return', returnHandler);
169
- channel.connection.once('error', errorHandler);
170
- channel.connection.once('close', errorHandler);
182
+ channel.connection.once('error', disconnectionHandler);
183
+ channel.connection.once('close', disconnectionHandler);
171
184
  }
172
185
 
173
- function removeListeners(channel, errorHandler, returnHandler) {
186
+ function removeListeners(channel, disconnectionHandler, returnHandler) {
174
187
  channel.removeAllListeners('drain');
175
- channel.removeListener('error', errorHandler);
188
+ channel.removeListener('error', disconnectionHandler);
176
189
  channel.removeListener('return', returnHandler);
177
- channel.connection.removeListener('error', errorHandler);
178
- channel.connection.removeListener('close', errorHandler);
190
+ channel.connection.removeListener('error', disconnectionHandler);
191
+ channel.connection.removeListener('close', disconnectionHandler);
179
192
  }
180
193
 
181
194
  function publishToExchange(channel, content, config, next) {
@@ -252,3 +265,8 @@ function handleChannelError(borked, messageId, emitter, config, err) {
252
265
  debug('Channel error: %s during publication of message: %s to %s using channel: %s', err.message, messageId, config.name, borked._rascal_id);
253
266
  emitter.emit('error', err, messageId);
254
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
+ }
@@ -61,13 +61,13 @@ function Subscription(broker, vhost, subscriptionConfig, counter) {
61
61
  _configureQos(config, channel, (err) => {
62
62
  if (err) return done(err);
63
63
 
64
- const removeErrorHandlers = attachErrorHandlers(channel, session, config);
65
- const onMessage = _onMessage.bind(null, session, config, removeErrorHandlers);
64
+ const removeDisconnectionHandlers = attachDisconnectionHandlers(channel, session, config);
65
+ const onMessage = _onMessage.bind(null, session, config, removeDisconnectionHandlers);
66
66
 
67
67
  channel.consume(config.source, onMessage, config.options, (err, response) => {
68
68
  if (err) {
69
69
  debug('Error subscribing to %s using channel: %s. %s', config.source, channel._rascal_id, err.message);
70
- removeErrorHandlers();
70
+ removeDisconnectionHandlers();
71
71
  return done(err);
72
72
  }
73
73
  session._open(channel, response.consumerTag, (err) => {
@@ -88,8 +88,8 @@ function Subscription(broker, vhost, subscriptionConfig, counter) {
88
88
  async.series(qos, next);
89
89
  }
90
90
 
91
- function _onMessage(session, config, removeErrorHandlers, message) {
92
- if (!message) return handleConsumerCancel(session, config, removeErrorHandlers);
91
+ function _onMessage(session, config, removeDisconnectionHandlers, message) {
92
+ if (!message) return handleConsumerCancel(session, config, removeDisconnectionHandlers);
93
93
 
94
94
  debug('Received message: %s from queue: %s', message.properties.messageId, config.queue);
95
95
  session._incrementUnacknowledgeMessageCount(message.fields.consumerTag);
@@ -249,51 +249,66 @@ function Subscription(broker, vhost, subscriptionConfig, counter) {
249
249
  if (err) session.emit('error', err);
250
250
  }
251
251
 
252
- function attachErrorHandlers(channel, session, config) {
252
+ function attachDisconnectionHandlers(channel, session, config) {
253
253
  /* eslint-disable no-use-before-define */
254
254
  const connection = channel.connection;
255
- const removeErrorHandlers = _.once(() => {
256
- channel.removeListener('error', errorHandler);
255
+ const removeDisconnectionHandlers = _.once(() => {
256
+ channel.removeListener('error', disconnectionHandler);
257
257
  channel.on('error', (err) => {
258
258
  debug('Suppressing error on cancelled session: %s to prevent connection errors. %s', channel._rascal_id, err.message);
259
259
  });
260
- connection.removeListener('error', errorHandler);
261
- connection.removeListener('close', errorHandler);
260
+ connection.removeListener('error', disconnectionHandler);
261
+ connection.removeListener('close', disconnectionHandler);
262
+ });
263
+
264
+ const disconnectionHandler = makeDisconnectionHandler(session, config, removeDisconnectionHandlers);
265
+ channel.on('error', disconnectionHandler);
266
+ connection.once('error', disconnectionHandler);
267
+ connection.once('close', disconnectionHandler);
268
+ return removeDisconnectionHandlers;
269
+ }
270
+
271
+ function makeDisconnectionHandler(session, config, removeDisconnectionHandlers) {
272
+ return _.once((err) => {
273
+ // Use setImmediate to avoid amqplib accept loop swallowing errors
274
+ setImmediate(() => (err
275
+ // Treat close events with errors as error events
276
+ ? handleChannelError(session, config, removeDisconnectionHandlers, 0, err)
277
+ : handleChannelClose(session, config, removeDisconnectionHandlers, 0)));
262
278
  });
263
- const errorHandler = _.once(handleChannelError.bind(null, session, config, removeErrorHandlers, 0));
264
- channel.on('error', errorHandler);
265
- connection.once('error', errorHandler);
266
- connection.once('close', errorHandler);
267
- return removeErrorHandlers;
268
279
  }
269
280
 
270
- function handleChannelError(session, config, removeErrorHandlers, attempts, err) {
281
+ function handleChannelError(session, config, removeDisconnectionHandler, attempt, err) {
271
282
  debug('Handling channel error: %s from %s using channel: %s', err.message, config.name, session._getRascalChannelId());
272
- if (removeErrorHandlers) removeErrorHandlers();
283
+ if (removeDisconnectionHandler) removeDisconnectionHandler();
273
284
  session.emit('error', err);
274
- config.retry
275
- && subscribeNow(session, config, (err) => {
276
- if (!err) return;
277
- const delay = timer.next();
278
- debug('Will attempt resubscription(%d) to %s in %dms', attempts + 1, config.name, delay);
279
- session._schedule(handleChannelError.bind(null, session, config, null, attempts + 1, err), delay);
280
- });
285
+ retrySubscription(session, config, attempt + 1);
281
286
  }
282
287
 
283
- function handleConsumerCancel(session, config, removeErrorHandlers) {
288
+ function handleChannelClose(session, config, removeDisconnectionHandler, attempt) {
289
+ debug('Handling channel close from %s using channel: %s', config.name, session._getRascalChannelId());
290
+ removeDisconnectionHandler();
291
+ session.emit('close');
292
+ retrySubscription(session, config, attempt + 1);
293
+ }
294
+
295
+ function handleConsumerCancel(session, config, removeDisconnectionHandler) {
284
296
  debug('Received consumer cancel from %s using channel: %s', config.name, session._getRascalChannelId());
285
- removeErrorHandlers();
297
+ removeDisconnectionHandler();
298
+ const cancelErr = new Error(format('Subscription: %s was cancelled by the broker', config.name));
299
+ session.emit('cancelled', cancelErr) || session.emit('cancel', cancelErr) || session.emit('error', cancelErr);
286
300
  session._close((err) => {
287
301
  if (err) debug('Error cancelling subscription: %s', err.message);
288
- const cancelErr = new Error(format('Subscription: %s was cancelled by the broker', config.name));
289
- session.emit('cancelled', cancelErr) || session.emit('error', cancelErr);
290
- config.retry
291
- && subscribeNow(session, config, (err) => {
292
- if (!err) return;
293
- const delay = timer.next();
294
- debug('Will attempt resubscription(%d) to %s in %dms', 1, config.name, delay);
295
- session._schedule(handleChannelError.bind(null, session, config, null, 1, err), delay);
296
- });
302
+ retrySubscription(session, config, 1);
303
+ });
304
+ }
305
+
306
+ function retrySubscription(session, config, attempt) {
307
+ config.retry && subscribeNow(session, config, (err) => {
308
+ if (!err) return;
309
+ const delay = timer.next();
310
+ debug('Will attempt resubscription(%d) to %s in %dms', attempt, config.name, delay);
311
+ session._schedule(handleChannelError.bind(null, session, config, null, attempt, err), delay);
297
312
  });
298
313
  }
299
314
  }
package/lib/amqp/Vhost.js CHANGED
@@ -57,7 +57,7 @@ function Vhost(vhostConfig, components) {
57
57
  connectionConfig = ctx.connectionConfig;
58
58
  timer = backoff(ctx.connectionConfig.retry);
59
59
 
60
- attachErrorHandlers(config);
60
+ attachDisconnectionHandlers(config);
61
61
  forwardRabbitMQConnectionEvents();
62
62
  ensureChannelPools();
63
63
  resumeChannelAllocation();
@@ -201,8 +201,7 @@ function Vhost(vhostConfig, components) {
201
201
  createChannelWhenInitialised(options.confirm, (err, channel) => {
202
202
  if (err) return deferRejection(reject, err);
203
203
  if (!channel) return deferRejection(reject, new Error('Vhost is shutting down'));
204
- const destroyChannel = _.once((err) => {
205
- console.log('Destroying channel', err);
204
+ const destroyChannel = _.once(() => {
206
205
  debug('Destroying %s channel: %s for vhost: %s due to error or close event', mode, channel._rascal_id, vhostConfig.name);
207
206
  channel._rascal_closed = true;
208
207
  if (pool.isBorrowedResource(channel)) {
@@ -472,11 +471,21 @@ function Vhost(vhostConfig, components) {
472
471
  );
473
472
  }
474
473
 
475
- function attachErrorHandlers(config) {
474
+ function attachDisconnectionHandlers(config) {
476
475
  connection.removeAllListeners('error');
477
- const errorHandler = _.once(handleConnectionError.bind(null, connection, config));
478
- connection.on('error', errorHandler);
479
- connection.on('close', errorHandler);
476
+ const disconectionHandler = makeDisconnectionHandler(config);
477
+ connection.on('error', disconectionHandler);
478
+ connection.on('close', disconectionHandler);
479
+ }
480
+
481
+ function makeDisconnectionHandler(config) {
482
+ return _.once((err) => {
483
+ // Use setImmediate to avoid amqplib accept loop swallowing errors
484
+ setImmediate(() => (err
485
+ // Treat close events with errors as error events
486
+ ? handleConnectionError(connection, config, err)
487
+ : handleConnectionClose(connection, config)));
488
+ });
480
489
  }
481
490
 
482
491
  function handleConnectionError(borked, config, err) {
@@ -485,12 +494,24 @@ function Vhost(vhostConfig, components) {
485
494
  connection = undefined;
486
495
  self.emit('disconnect');
487
496
  self.emit('error', err, self.getConnectionDetails());
488
- connectionConfig.retry
489
- && self.init((err) => {
490
- if (!err) return;
491
- const delay = timer.next();
492
- debug('Will attempt reconnection in in %dms', delay);
493
- reconnectTimeout = setTimeoutUnref(handleConnectionError.bind(null, borked, config, err), delay);
494
- });
497
+ retryConnection(borked, config);
498
+ }
499
+
500
+ function handleConnectionClose(borked, config) {
501
+ debug('Handling connection close initially from connection: %s, %s', borked._rascal_id, connectionConfig.loggableUrl);
502
+ pauseChannelAllocation();
503
+ connection = undefined;
504
+ self.emit('disconnect');
505
+ self.emit('close', self.getConnectionDetails());
506
+ retryConnection(borked, config);
507
+ }
508
+
509
+ function retryConnection(borked, config) {
510
+ connectionConfig.retry && self.init((err) => {
511
+ if (!err) return;
512
+ const delay = timer.next();
513
+ debug('Will attempt reconnection in in %dms', delay);
514
+ reconnectTimeout = setTimeoutUnref(handleConnectionError.bind(null, borked, config, err), delay);
515
+ });
495
516
  }
496
517
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rascal",
3
- "version": "18.0.0",
3
+ "version": "19.0.0",
4
4
  "description": "A config driven wrapper for amqplib supporting multi-host connections, automatic error recovery, redelivery flood protection, transparent encryption / decryption, channel pooling and publication timeouts",
5
5
  "main": "index.js",
6
6
  "dependencies": {