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 +12 -0
- package/README.md +5 -2
- package/examples/simple/config.js +3 -1
- package/lib/amqp/SubscriberError.js +5 -5
- package/lib/amqp/Subscription.js +1 -2
- package/lib/amqp/Vhost.js +49 -25
- package/lib/amqp/tasks/createConnection.js +2 -0
- package/lib/config/baseline.js +2 -0
- package/package.json +1 -1
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.
|
|
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": {
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
package/lib/amqp/Subscription.js
CHANGED
|
@@ -198,8 +198,7 @@ function Subscription(broker, vhost, config, counter) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
function immediateNack(message) {
|
|
201
|
-
if (_.get(message,
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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.
|
|
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('
|
|
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
|
}
|
package/lib/config/baseline.js
CHANGED
|
@@ -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.
|
|
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": {
|