rascal 9.3.0 → 9.4.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,14 @@
1
1
  # Change Log
2
2
 
3
+ ## 9.4.0
4
+ ### Added
5
+ - vhost_initialised event
6
+ - publication paused notifications
7
+ - publication.abort
8
+
9
+ ### Updated
10
+ - broker error events now include vhost connection details
11
+
3
12
  ## 9.3.0
4
13
  ### Updated
5
14
  - [Fixed #78](https://github.com/guidesmiths/rascal/issues/78) by using a baseline config, and only laying connection options via withDefaultConfig
package/README.md CHANGED
@@ -114,8 +114,8 @@ The reason Rascal nacks the message is because the alternative is to rollback an
114
114
  1. Immediately after obtaining a broker instance
115
115
 
116
116
  ```js
117
- broker.on('error', (err) => {
118
- console.error('Broker error', err)
117
+ broker.on('error', (err, { vhost, connectionUrl }) => {
118
+ console.error('Broker error', err, vhost, connectionUrl)
119
119
  })
120
120
  ```
121
121
 
@@ -195,6 +195,28 @@ The reason Rascal nacks the message is because the alternative is to rollback an
195
195
  })
196
196
  ```
197
197
 
198
+ ### Other Broker Events
199
+
200
+ #### vhost_initialised
201
+ The broker emits the `vhost_initialised` event after recovering from a connection error. An object containing the vhost name and connection url (with obfuscated password) are passed to he event handler. e.g.
202
+
203
+ ```js
204
+ broker.on('vhost_initialised', ({ vhost, connectionUrl }) => {
205
+ console.log(`Vhost: ${vhost} was initialised using connection: ${connectionUrl}`);
206
+ })
207
+ ```
208
+
209
+ #### blocked / unblocked
210
+ RabbitMQ notifies clients of [blocked and unblocked](https://www.rabbitmq.com/connection-blocked.html) connections, which rascal forwards from the connection to the broker. e.g.
211
+ ```js
212
+ broker.on('blocked', (reason, { vhost, connectionUrl }) => {
213
+ console.log(`Vhost: ${vhost} was blocked using connection: ${connectionUrl}. Reason: ${reason}`);
214
+ })
215
+ broker.on('unblocked', ({ vhost, connectionUrl }) => {
216
+ console.log(`Vhost: ${vhost} was unblocked using connection: ${connectionUrl}.`);
217
+ })
218
+ ```
219
+
198
220
  ## Configuration
199
221
  Rascal is highly configurable, but ships with what we consider to be sensible defaults (optimised for reliability rather than speed) for production and test environments.
200
222
 
@@ -766,7 +788,7 @@ try {
766
788
  console.log("Message id was: ", messageId)
767
789
  }).on("error", (err, messageId) => {
768
790
  console.error("Error was: ", err.message)
769
- }).on("return", (messageId) => {
791
+ }).on("return", (message) => {
770
792
  console.warn("Message was returned: ", message.properties.messageId)
771
793
  })
772
794
  } catch (err) {
@@ -817,6 +839,39 @@ When you publish a message using a confirm channel, amqplib will wait for an ack
817
839
  }
818
840
  ```
819
841
 
842
+ #### Aborting
843
+ Rascal uses a channel pool to publish messages. Access to the channel pool is synchronised via an in memory queue, which will be paused if the connection to the broker is temporarily lost. Consequently instead of erroring, publishes will be held until the connection is re-established. If you would rather abort under these circumstances, you can listen for the publication 'paused' event, and call `publication.abort()`. When the connection is re-established any aborted messages will be dropped instead of published.
844
+
845
+ ```js
846
+ broker.publish("p1", "some message", (err, publication) => {
847
+ if (err) throw err; // publication didn't exist
848
+ publication.on("success", (messageId) => {
849
+ console.log("Message id was: ", messageId)
850
+ }).on("error", (err, messageId) => {
851
+ console.error("Error was: ", err.message)
852
+ }).on("paused", (messageId) => {
853
+ console.warn("Publication was paused. Aborting message: ", messageId)
854
+ publication.abort();
855
+ })
856
+ })
857
+ ```
858
+
859
+ ```js
860
+ try {
861
+ const publication = await broker.publish("p1", "some message")
862
+ publication.on("success", (messageId) => {
863
+ console.log("Message id was: ", messageId)
864
+ }).on("error", (err, messageId) => {
865
+ console.error("Error was: ", err.message)
866
+ }).on("paused", (messageId) => {
867
+ console.warn("Publication was paused. Aborting message: ", messageId)
868
+ publication.abort();
869
+ })
870
+ } catch (err) {
871
+ // publication didn't exist
872
+ }
873
+ ```
874
+
820
875
  #### Encrypting messages
821
876
  Rascal can be configured to automatically encrypt outbound messages.
822
877
  ```json
@@ -2,8 +2,8 @@ var debug = require('debug')('rascal:Publication');
2
2
  var format = require('util').format;
3
3
  var _ = require('lodash');
4
4
  var uuid = require('uuid').v4;
5
- var EventEmitter = require('events').EventEmitter;
6
5
  var crypto = require('crypto');
6
+ var PublicationSession = require('./PublicationSession');
7
7
  var setTimeoutUnref = require('../utils/setTimeoutUnref');
8
8
 
9
9
  module.exports = {
@@ -30,7 +30,7 @@ function Publication(vhost, borrowChannelFn, returnChannelFn, destroyChannelFn,
30
30
 
31
31
  this.init = function(next) {
32
32
  debug('Initialising publication: %s', config.name);
33
- return next(null, self);
33
+ next(null, self);
34
34
  };
35
35
 
36
36
  this.publish = function(payload, overrides, next) {
@@ -39,7 +39,7 @@ function Publication(vhost, borrowChannelFn, returnChannelFn, destroyChannelFn,
39
39
  publishConfig.options.contentType = publishConfig.options.contentType || content.type;
40
40
  publishConfig.options.messageId = publishConfig.options.messageId || uuid();
41
41
 
42
- return publishConfig.encryption
42
+ publishConfig.encryption
43
43
  ? _publishEncrypted(content.buffer, publishConfig, next)
44
44
  : _publish(content.buffer, publishConfig, next);
45
45
  };
@@ -87,32 +87,39 @@ function Publication(vhost, borrowChannelFn, returnChannelFn, destroyChannelFn,
87
87
  }
88
88
 
89
89
  function _publish(buffer, publishConfig, next) {
90
- var emitter = new EventEmitter();
91
90
  var messageId = publishConfig.options.messageId;
91
+ var session = new PublicationSession(vhost, messageId);
92
92
  borrowChannelFn(function(err, channel) {
93
- if (err) return emitter.emit('error', err, messageId);
94
- var errorHandler = _.once(handleChannelError.bind(null, channel, messageId, emitter, config));
95
- var returnHandler = emitter.emit.bind(emitter, 'return');
93
+ session._removePausedListener();
94
+ if (err) return session.emit('error', err, messageId);
95
+ if (session.isAborted()) return abortPublish(channel, messageId);
96
+ var errorHandler = _.once(handleChannelError.bind(null, channel, messageId, session, config));
97
+ var returnHandler = session.emit.bind(session, 'return');
96
98
  addListeners(channel, errorHandler, returnHandler);
97
99
  try {
98
100
  publishFn(channel, buffer, publishConfig, function(err, ok) {
99
101
  if (err) {
100
102
  destroyChannel(channel, errorHandler, returnHandler);
101
- return emitter.emit('error', err, messageId);
103
+ return session.emit('error', err, messageId);
102
104
  }
103
105
 
104
106
  ok ? returnChannel(channel, errorHandler, returnHandler)
105
107
  : deferReturnChannel(channel, errorHandler, returnHandler);
106
108
 
107
- emitter.emit('success', messageId);
109
+ session.emit('success', messageId);
108
110
  });
109
111
  } catch(err) {
110
112
  returnChannel(channel, errorHandler, returnHandler);
111
- return emitter.emit('error', err, messageId);
113
+ return session.emit('error', err, messageId);
112
114
  };
113
115
  });
114
116
 
115
- next(null, emitter);
117
+ next(null, session);
118
+ }
119
+
120
+ function abortPublish(channel, messageId) {
121
+ debug('Publication of message: %s was aborted', messageId);
122
+ returnChannelFn(channel);
116
123
  }
117
124
 
118
125
  function returnChannel(channel, errorHandler, returnHandler) {
@@ -0,0 +1,36 @@
1
+ var debug = require('debug')('rascal:SubscriberSession');
2
+ var EventEmitter = require('events').EventEmitter;
3
+ var inherits = require('util').inherits;
4
+
5
+ module.exports = PublicationSession;
6
+
7
+ inherits(PublicationSession, EventEmitter);
8
+
9
+ function PublicationSession(vhost, messageId) {
10
+
11
+ var self = this;
12
+ var aborted = false;
13
+
14
+ this.abort = function() {
15
+ aborted = true;
16
+ };
17
+
18
+ this.isAborted = function() {
19
+ return aborted;
20
+ };
21
+
22
+ this._removePausedListener = function() {
23
+ vhost.removeListener('paused',emitPaused);
24
+ };
25
+
26
+ function emitPaused() {
27
+ self.emit('paused', messageId);
28
+ }
29
+
30
+ vhost.on('paused', emitPaused);
31
+
32
+ self.on('newListener', function(event) {
33
+ if (event !== 'paused') return;
34
+ if (vhost.isPaused()) emitPaused();
35
+ });
36
+ }
package/lib/amqp/Vhost.js CHANGED
@@ -3,7 +3,6 @@ var format = require('util').format;
3
3
  var inherits = require('util').inherits;
4
4
  var EventEmitter = require('events').EventEmitter;
5
5
  var async = require('async');
6
- var forwardEvents = require('forward-emitter');
7
6
  var genericPool = require('generic-pool');
8
7
  var tasks = require('./tasks');
9
8
  var uuid = require('uuid').v4;
@@ -33,6 +32,7 @@ function Vhost(config) {
33
32
  var purge = async.compose(tasks.closeConnection, tasks.closeChannel, tasks.purgeQueues, tasks.createChannel, tasks.createConnection);
34
33
  var nuke = async.compose(tasks.closeConnection, tasks.closeChannel, tasks.deleteQueues, tasks.deleteExchanges, tasks.createChannel, tasks.createConnection);
35
34
  var timer = backoff({});
35
+ var paused = true;
36
36
 
37
37
  this.name = config.name;
38
38
  this.connectionIndex = 0;
@@ -45,22 +45,22 @@ function Vhost(config) {
45
45
 
46
46
  init(config, { connectionIndex: self.connectionIndex }, function(err, config, ctx) {
47
47
  if (err) return next(err);
48
- self.emit('connect');
49
-
50
- attachErrorHandlers(ctx.connection, config);
51
48
 
52
- forwardEvents(ctx.connection, self, function(eventName) {
53
- return eventName === 'blocked' || eventName === 'unblocked';
54
- });
55
- debug('vhost: %s was initialised with connection: %s', self.name, ctx.connection._rascal_id);
56
49
  connection = ctx.connection;
57
50
  self.connectionIndex = ctx.connectionIndex;
58
51
  connectionConfig = ctx.connectionConfig;
59
52
  timer = backoff(ctx.connectionConfig.retry);
60
53
 
54
+ attachErrorHandlers(config);
55
+ forwardRabbitMQConnectionEvents();
61
56
  ensureChannelPools();
62
57
  resumeChannelAllocation();
63
58
 
59
+ debug('vhost: %s was initialised with connection: %s', self.name, connection._rascal_id);
60
+
61
+ self.emit('connect');
62
+ self.emit('vhost_initialised', getConnectionDetails());
63
+
64
64
  return next(null, self);
65
65
  });
66
66
  return self;
@@ -162,6 +162,10 @@ function Vhost(config) {
162
162
  confirmChannelPool.destroy(channel);
163
163
  };
164
164
 
165
+ this.isPaused = function() {
166
+ return paused;
167
+ };
168
+
165
169
  function createChannelPool(options) {
166
170
  var pool, poolQueue;
167
171
  var mode = getChannelMode(options.confirm);
@@ -326,12 +330,25 @@ function Vhost(config) {
326
330
  channelCreator.pause();
327
331
  regularChannelPool && regularChannelPool.pause();
328
332
  confirmChannelPool && confirmChannelPool.pause();
333
+ paused = true;
334
+ self.emit('paused', { vhost: self.name });
329
335
  }
330
336
 
331
337
  function resumeChannelAllocation() {
332
338
  channelCreator.resume();
333
339
  regularChannelPool && regularChannelPool.resume();
334
340
  confirmChannelPool && confirmChannelPool.resume();
341
+ paused = false;
342
+ self.emit('resumed', { vhost: self.name });
343
+ }
344
+
345
+ function forwardRabbitMQConnectionEvents() {
346
+ connection.on('blocked', function(reason) {
347
+ self.emit('blocked', reason, getConnectionDetails());
348
+ });
349
+ connection.on('unblocked', function() {
350
+ self.emit('unblocked', getConnectionDetails());
351
+ });
335
352
  }
336
353
 
337
354
  function ensureChannelPools() {
@@ -350,7 +367,7 @@ function Vhost(config) {
350
367
  ], next);
351
368
  }
352
369
 
353
- function attachErrorHandlers(connection, config) {
370
+ function attachErrorHandlers(config) {
354
371
  connection.removeAllListeners('error');
355
372
  var errorHandler = _.once(handleConnectionError.bind(null, connection, config));
356
373
  connection.once('error', errorHandler);
@@ -359,10 +376,10 @@ function Vhost(config) {
359
376
 
360
377
  function handleConnectionError(borked, config, err) {
361
378
  debug('Handling connection error: %s initially from connection: %s, %s', err.message, borked._rascal_id, connectionConfig.loggableUrl);
362
- self.emit('disconnect');
363
379
  pauseChannelAllocation();
364
380
  connection = undefined;
365
- self.emit('error', err);
381
+ self.emit('disconnect');
382
+ self.emit('error', err, getConnectionDetails());
366
383
  connectionConfig.retry && self.init(function(err) {
367
384
  if (!err) return;
368
385
  var delay = timer.next();
@@ -370,4 +387,8 @@ function Vhost(config) {
370
387
  setTimeoutUnref(handleConnectionError.bind(null, borked, config, err), delay);
371
388
  });
372
389
  }
390
+
391
+ function getConnectionDetails() {
392
+ return { vhost: self.name, connectionUrl: connectionConfig.loggableUrl };
393
+ }
373
394
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rascal",
3
- "version": "9.3.0",
3
+ "version": "9.4.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": {