rascal 13.0.5 → 13.1.2

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
+ ## 13.1.2
4
+ - Fixed various issues when queue names contained period characters. Reported in https://github.com/guidesmiths/rascal/issues/166
5
+
6
+ ## 13.1.1
7
+ - Moved setMaxListeners to createConnection task to suppress misleading 'Possible EventEmitter memory leak detected' warning. See https://github.com/guidesmiths/rascal/issues/164 for more details.
8
+
9
+ ## 13.1.0
10
+ - Fixed bug where Rascal could wait indefinitely for channels to be destroyed if shutdown was called following a heartbeat timeout. See https://github.com/guidesmiths/rascal/issues/158 for more details.
11
+
12
+ ## 13.0.6
13
+ - Fixed bug where Rascal attempted to remove a listener from a nulled connection and crashed.
14
+
3
15
  ## 13.0.5
4
16
  - Set channel pool acquireTimeoutMillis in default configuration - thanks @matej-prokop
5
17
  - Add snyk package health badge
package/README.md CHANGED
@@ -39,13 +39,16 @@ Rascal seeks to either solve these problems, make them easier to deal with or br
39
39
  * TDD support
40
40
 
41
41
  ## Concepts
42
- Rascal extends the existing [RabbitMQ Concepts](https://www.rabbitmq.com/tutorials/amqp-concepts.html) of Brokers, Vhosts, Exchanges Queues, Channels and Connections with with two new ones
42
+ Rascal extends the existing [RabbitMQ Concepts](https://www.rabbitmq.com/tutorials/amqp-concepts.html) of Brokers, Vhosts, Exchanges, Queues, Channels and Connections with with two new ones
43
43
 
44
44
  1. Publications
45
45
  1. Subscriptions
46
46
 
47
47
  A **publication** is a named configuration for publishing a message, including the destination queue or exchange, routing configuration, encryption profile and reliability guarantees, message options, etc. A **subscription** is a named configuration for consuming messages, including the source queue, encryption profile, content encoding, delivery options (e.g. acknowledgement handling and prefetch), etc. These must be [configured](#configuration) and supplied when creating the Rascal broker. After the broker has been created the subscriptions and publications can be retrivied from the broker and used to publish and consume messages.
48
48
 
49
+ ### Special Note
50
+ RabbitMQ 3.8.0 introduced [quorum queues](https://www.rabbitmq.com/quorum-queues.html). Although quorum queues may not be suitable in all situations, they provide [poison message handling](https://www.rabbitmq.com/quorum-queues.html#poison-message-handling) without the need for an external [redelivery counter](https://github.com/guidesmiths/rascal#dealing-with-redeliveries) and offer better data safety in the event of a network partition. You can read more about them [here](https://www.cloudamqp.com/blog/reasons-you-should-switch-to-quorum-queues.html) and [here](https://blog.rabbitmq.com/posts/2020/06/quorum-queues-local-delivery).
51
+
49
52
  ## Examples
50
53
 
51
54
  ### Async/Await
@@ -1135,7 +1138,7 @@ Rascal can be configured to automatically decrypt inbound messages.
1135
1138
  Any message that was published using the "well-known-v1" encryption profile will be automatically decrypted by the subscriber.
1136
1139
 
1137
1140
  #### Dealing With Redeliveries
1138
- If your app crashes before acknowledging a message, the message will be rolled back. It is common for node applications to automatically restart, however if the crash was caused by something in the message content, it will crash and restart indefinitely, thrashing the host. Unfortunately RabbitMQ doesn't allow you to limit the number of redeliveries per message or provide a redelivery count. For this reason subscribers can be configured with a redelivery counter and will update the ```message.properties.headers.rascal.redeliveries``` header with the number of hits. If the number of redeliveries exceeds the subscribers limit, the subscriber will emit a "redeliveries_exceeded" event, and can be handled by your application. e.g.
1141
+ If your app crashes before acknowledging a message, the message will be rolled back. It is common for node applications to automatically restart, however if the crash was caused by something in the message content, it will crash and restart indefinitely, thrashing the host. Prior to version 3.8.0, RabbitMQ didn't allow you to limit the number of redeliveries per message or provide a redelivery count. This is now possible using [quorum queues](https://www.rabbitmq.com/quorum-queues.html#poison-message-handling), but for those on older versions, or in situations where quorum queues are not appropriate, subscribers can be configured with a redelivery counter and will update the ```message.properties.headers.rascal.redeliveries``` header with the number of hits. If the number of redeliveries exceeds the subscribers limit, the subscriber will emit a "redeliveries_exceeded" event, and can be handled by your application. e.g.
1139
1142
 
1140
1143
  ```json
1141
1144
  "subscriptions": {
@@ -7,7 +7,9 @@ module.exports = {
7
7
  }
8
8
  },
9
9
  connection: {
10
- heartbeat: 1,
10
+ options: {
11
+ heartbeat: 10
12
+ },
11
13
  socketOptions: {
12
14
  timeout: 1000
13
15
  }
@@ -64,7 +64,7 @@ module.exports = function SubscriptionRecovery(broker, vhost) {
64
64
  debug('Republishing message: %s', message.properties.messageId);
65
65
 
66
66
  const originalQueue = _.get(message, 'properties.headers.rascal.originalQueue');
67
- const republished = _.get(message, format('properties.headers.rascal.recovery.%s.republished', originalQueue), 0);
67
+ const republished = _.get(message, ['properties', 'headers', 'rascal', 'recovery', originalQueue, 'republished'], 0);
68
68
 
69
69
  if (strategyConfig.attempts && strategyConfig.attempts <= republished) {
70
70
  debug('Skipping recovery - message: %s has already been republished %d times.', message.properties.messageId, republished);
@@ -72,14 +72,14 @@ module.exports = function SubscriptionRecovery(broker, vhost) {
72
72
  }
73
73
 
74
74
  const publishOptions = _.cloneDeep(message.properties);
75
- _.set(publishOptions, format('headers.rascal.recovery.%s.republished', originalQueue), republished + 1);
75
+ _.set(publishOptions, ['headers', 'rascal', 'recovery', originalQueue, 'republished'], republished + 1);
76
76
  _.set(publishOptions, 'headers.rascal.originalExchange', message.fields.exchange);
77
77
  _.set(publishOptions, 'headers.rascal.originalRoutingKey', message.fields.routingKey);
78
78
  _.set(publishOptions, 'headers.rascal.error.message', _.truncate(err.message, { length: 1024 }));
79
79
  _.set(publishOptions, 'headers.rascal.error.code', err.code);
80
80
  _.set(publishOptions, 'headers.rascal.restoreRoutingHeaders', _.has(strategyConfig, 'restoreRoutingHeaders') ? strategyConfig.restoreRoutingHeaders : true);
81
81
 
82
- if (strategyConfig.immediateNack) _.set(publishOptions, format('headers.rascal.recovery.%s.immediateNack', originalQueue), true);
82
+ if (strategyConfig.immediateNack) _.set(publishOptions, ['headers', 'rascal', 'recovery', originalQueue, 'immediateNack'], true);
83
83
 
84
84
  vhost.getConfirmChannel((err, publisherChannel) => {
85
85
 
@@ -105,7 +105,7 @@ module.exports = function SubscriptionRecovery(broker, vhost) {
105
105
  debug('Forwarding message: %s', message.properties.messageId);
106
106
 
107
107
  const originalQueue = _.get(message, 'properties.headers.rascal.originalQueue');
108
- const forwarded = _.get(message, format('properties.headers.rascal.recovery.%s.forwarded', originalQueue), 0);
108
+ const forwarded = _.get(message, ['properties', 'headers', 'rascal', 'recovery', originalQueue, 'forwarded'], 0);
109
109
 
110
110
  if (strategyConfig.attempts && strategyConfig.attempts <= forwarded) {
111
111
  debug('Skipping recovery - message: %s has already been forwarded %d times.', message.properties.messageId, forwarded);
@@ -117,7 +117,7 @@ module.exports = function SubscriptionRecovery(broker, vhost) {
117
117
 
118
118
  const forwardOverrides = _.cloneDeep(strategyConfig.options) || {};
119
119
  _.set(forwardOverrides, 'restoreRoutingHeaders', _.has(strategyConfig, 'restoreRoutingHeaders') ? strategyConfig.restoreRoutingHeaders : true);
120
- _.set(forwardOverrides, format('options.headers.rascal.recovery.%s.forwarded', originalQueue), forwarded + 1);
120
+ _.set(forwardOverrides, ['options', 'headers', 'rascal', 'recovery', originalQueue, 'forwarded'], forwarded + 1);
121
121
  _.set(forwardOverrides, 'options.headers.rascal.error.message', _.truncate(err.message, { length: 1024 }));
122
122
  _.set(forwardOverrides, 'options.headers.rascal.error.code', err.code);
123
123
 
@@ -198,8 +198,7 @@ function Subscription(broker, vhost, config, counter) {
198
198
  }
199
199
 
200
200
  function immediateNack(message) {
201
- if (_.get(message, format('properties.headers.rascal.recovery.%s.immediateNack', message.properties.headers.rascal.originalQueue))) return true;
202
- if (_.get(message, format('properties.headers.rascal.recovery.%s.immediateNack', message.properties.headers.rascal.originalQueue))) return true;
201
+ if (_.get(message, ['properties', 'headers', 'rascal', 'recovery', message.properties.headers.rascal.originalQueue, 'immediateNack'])) return true;
203
202
  return false;
204
203
  }
205
204
 
package/lib/amqp/Vhost.js CHANGED
@@ -34,6 +34,7 @@ function Vhost(config) {
34
34
  let timer = backoff({});
35
35
  let paused = true;
36
36
  let shuttingDown = false;
37
+ let reconnectTimeout;
37
38
 
38
39
  this.name = config.name;
39
40
  this.connectionIndex = 0;
@@ -41,6 +42,11 @@ function Vhost(config) {
41
42
  pauseChannelAllocation();
42
43
 
43
44
  this.init = function(next) {
45
+ if (shuttingDown) {
46
+ debug('Aborting initialisation. Vhost %s is shutting down.', self.name);
47
+ return next();
48
+ }
49
+
44
50
  debug('Initialising vhost: %s', self.name);
45
51
  pauseChannelAllocation();
46
52
 
@@ -68,7 +74,7 @@ function Vhost(config) {
68
74
  };
69
75
 
70
76
  this.forewarn = function(next) {
71
- debug('Forwarning vhost: %s about impending shutdown', self.name);
77
+ debug('Forewarning vhost: %s about impending shutdown', self.name);
72
78
  pauseChannelAllocation();
73
79
  shuttingDown = true;
74
80
  channelCreator.resume();
@@ -77,6 +83,7 @@ function Vhost(config) {
77
83
 
78
84
  this.shutdown = function(next) {
79
85
  debug('Shutting down vhost: %s', self.name);
86
+ clearTimeout(reconnectTimeout);
80
87
  pauseChannelAllocation();
81
88
  drainChannelPools((err) => {
82
89
  if (err) return next(err);
@@ -144,7 +151,7 @@ function Vhost(config) {
144
151
  };
145
152
 
146
153
  this.borrowChannel = function(next) {
147
- if (!regularChannelPool) return next(new Error(format('VHost: %s must be initialised before you can borrow a channel', self.name)));
154
+ if (!regularChannelPool) return next(new Error(format('Vhost: %s must be initialised before you can borrow a channel', self.name)));
148
155
  regularChannelPool.borrow(next);
149
156
  };
150
157
 
@@ -159,7 +166,7 @@ function Vhost(config) {
159
166
  };
160
167
 
161
168
  this.borrowConfirmChannel = function(next) {
162
- if (!confirmChannelPool) return next(new Error(format('VHost: %s must be initialised before you can borrow a confirm channel', self.name)));
169
+ if (!confirmChannelPool) return next(new Error(format('Vhost: %s must be initialised before you can borrow a confirm channel', self.name)));
163
170
  confirmChannelPool.borrow(next);
164
171
  };
165
172
 
@@ -192,19 +199,14 @@ function Vhost(config) {
192
199
  return new Promise((resolve, reject) => {
193
200
  debug('Creating pooled %s channel for vhost: %s', mode, config.name);
194
201
  createChannelWhenInitialised(options.confirm, (err, channel) => {
195
- if (err) {
196
- debug('Error creating pooled %s channel for vhost: %s. %s', mode, config.name, err.message);
197
- return deferRejection(reject, err);
198
- }
199
- if (!channel) {
200
- debug('Error creating pooled %s channel for vhost: %s. Vhost is shutting down', mode, config.name);
201
- return deferRejection(reject, new Error('Vhost is shutting down'));
202
- }
202
+ if (err) return deferRejection(reject, err);
203
+ if (!channel) return deferRejection(reject, new Error('Vhost is shutting down'));
203
204
  const destroyChannel = _.once(() => {
205
+ debug('Destroying %s channel: %s for vhost: %s due to error or close event', mode, channel._rascal_id, config.name);
204
206
  channel._rascal_closed = true;
205
207
  if (pool.isBorrowedResource(channel)) {
206
208
  pool.destroy(channel).catch((err) => {
207
- debug('Error destroying pooled %s channel: %s for vhost: %s. %s', mode, config.name, err.message);
209
+ debug('Error destroying %s channel: %s for vhost: %s. %s', mode, channel._rascal_id, config.name, err.message);
208
210
  });
209
211
  }
210
212
  });
@@ -215,12 +217,24 @@ function Vhost(config) {
215
217
  });
216
218
  },
217
219
  destroy(channel) {
218
- return new Promise((resolve) => {
219
- debug('Destroying channel: %s', channel._rascal_id);
220
+ return new Promise((resolve, reject) => {
221
+ debug('Destroying %s channel: %s for vhost: %s', mode, channel._rascal_id, config.name);
220
222
  if (channel._rascal_closed) return resolve();
221
- channel.close(() => {
223
+ channel.removeAllListeners();
224
+ channel.on('error', reject);
225
+ const closeChannelCb = (err) => {
226
+ if (err) return reject(err);
222
227
  resolve();
223
- });
228
+ };
229
+ // When a connection drops it may take a while for the heartbeat protocol or TCP keep alives to notice
230
+ // Consequently a publication using confirm channels may timeout and attempt to close the channel
231
+ // before Rascal notices that the connection died. In this circumstance the channel close command
232
+ // will never receive a response from the broker, and the callback will never yield.
233
+ const once = _.once(closeChannelCb);
234
+ setTimeoutUnref(() => {
235
+ once(new Error(format('Timeout after %dms closing %s channel: %s for vhost: %s', options.pool.destroyTimeoutMillis, mode, channel._rascal_id, config.name)));
236
+ }, 1000);
237
+ channel.close(once);
224
238
  });
225
239
  },
226
240
  validate(channel) {
@@ -302,6 +316,12 @@ function Vhost(config) {
302
316
 
303
317
  debug('Creating %s channel pool %o', mode, options.pool);
304
318
  pool = genericPool.createPool(factory, options.pool);
319
+ pool.on('factoryCreateError', (err) => {
320
+ debug('Create error emitted by %s channel pool: %s', mode, err.message);
321
+ });
322
+ pool.on('factoryDestroyError', (err) => {
323
+ debug('Destroy error emitted by %s channel pool: %s', mode, err.message);
324
+ });
305
325
 
306
326
  poolQueue = async.queue((__, next) => {
307
327
  pool.acquire().then((channel) => {
@@ -325,19 +345,24 @@ function Vhost(config) {
325
345
  function createChannelWhenInitialised(confirm, next) {
326
346
  if (connection) return createChannel(confirm, next);
327
347
  debug('Vhost: %s is not initialised. Deferring channel creation', self.name);
328
- self.once('vhost_initialised', () => {
329
- debug('Vhost: %s was initialised. Resuming channel creation', self.name);
330
- createChannel(confirm, next);
348
+ setTimeoutUnref(() => {
349
+ self.removeListener('vhost_initialised', onVhostInitialised);
350
+ next(new Error('Timedout acquiring channel'), 5000);
331
351
  });
352
+ function onVhostInitialised() {
353
+ debug('Vhost: %s was initialised. Resuming channel creation', self.name);
354
+ createChannel(confirm, next);
355
+ }
356
+ self.once('vhost_initialised', onVhostInitialised);
332
357
  }
333
358
 
334
359
  function createChannel(confirm, next) {
335
360
 
336
361
  if (shuttingDown) {
337
- debug('Ignoring create channel request. VHost is shutting down.');
362
+ debug('Ignoring create channel request. Vhost: %s is shutting down.', self.name);
338
363
  return next();
339
364
  }
340
- if (!connection) return next(new Error(format('VHost: %s must be initialised before you can create a channel', self.name)));
365
+ if (!connection) return next(new Error(format('Vhost: %s must be initialised before you can create a channel', self.name)));
341
366
 
342
367
  // Same problem as https://github.com/guidesmiths/rascal/issues/17
343
368
  const once = _.once(next);
@@ -358,8 +383,8 @@ function Vhost(config) {
358
383
 
359
384
  function callback(err, channel) {
360
385
  invocations++;
361
- connection.removeListener('close', closeHandler);
362
- connection.removeListener('error', errorHandler);
386
+ connection && connection.removeListener('close', closeHandler);
387
+ connection && connection.removeListener('error', errorHandler);
363
388
  if (err) {
364
389
  debug('Error creating channel: %s from %s: %s', channelId, connectionConfig.loggableUrl, err.message);
365
390
  return once(err);
@@ -367,7 +392,6 @@ function Vhost(config) {
367
392
 
368
393
  channel._rascal_id = channelId;
369
394
  channel.connection._rascal_id = connection._rascal_id;
370
- channel.connection.setMaxListeners(0);
371
395
  debug('Created %s channel: %s from connection: %s', getChannelMode(confirm), channel._rascal_id, connection._rascal_id);
372
396
 
373
397
  // See https://github.com/squaremo/amqp.node/issues/388
@@ -442,7 +466,7 @@ function Vhost(config) {
442
466
  if (!err) return;
443
467
  const delay = timer.next();
444
468
  debug('Will attempt reconnection in in %dms', delay);
445
- setTimeoutUnref(handleConnectionError.bind(null, borked, config, err), delay);
469
+ reconnectTimeout = setTimeoutUnref(handleConnectionError.bind(null, borked, config, err), delay);
446
470
  });
447
471
  }
448
472
  }
@@ -65,6 +65,8 @@ function connect(connectionConfig, cb) {
65
65
  return connection.close();
66
66
  }
67
67
 
68
+ connection.setMaxListeners(0);
69
+
68
70
  once(null, connection);
69
71
  });
70
72
  }
@@ -11,6 +11,7 @@ module.exports = {
11
11
  rejectionDelayMillis: 1000,
12
12
  testOnBorrow: true,
13
13
  acquireTimeoutMillis: 15000,
14
+ destroyTimeoutMillis: 1000,
14
15
  },
15
16
  confirmPool: {
16
17
  autostart: false,
@@ -21,6 +22,7 @@ module.exports = {
21
22
  rejectionDelayMillis: 1000,
22
23
  testOnBorrow: true,
23
24
  acquireTimeoutMillis: 15000,
25
+ destroyTimeoutMillis: 1000,
24
26
  },
25
27
  },
26
28
  connectionStrategy: 'random',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rascal",
3
- "version": "13.0.5",
3
+ "version": "13.1.2",
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": {