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 +12 -0
- package/README.md +60 -2
- package/examples/streams/package-lock.json +13 -0
- package/examples/streams/package.json +14 -0
- package/examples/streams/publisher-config.json +37 -0
- package/examples/streams/publisher.js +38 -0
- package/examples/streams/subscriber-config.json +28 -0
- package/examples/streams/subscriber.js +25 -0
- package/lib/amqp/Publication.js +37 -19
- package/lib/amqp/Subscription.js +50 -35
- package/lib/amqp/Vhost.js +35 -14
- package/package.json +1 -1
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.
|
|
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 `
|
|
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
|
+
});
|
package/lib/amqp/Publication.js
CHANGED
|
@@ -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
|
-
|
|
98
|
+
|
|
99
|
+
const disconnectionHandler = makeDisconnectionHandler(channel, messageId, session, config);
|
|
99
100
|
const returnHandler = session.emit.bind(session, 'return');
|
|
100
|
-
addListeners(channel,
|
|
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,
|
|
109
|
+
destroyChannel(channel, disconnectionHandler, returnHandler);
|
|
107
110
|
return session.emit('error', err, messageId);
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
ok ? returnChannel(channel,
|
|
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,
|
|
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,
|
|
129
|
-
removeListeners(channel,
|
|
131
|
+
function returnChannel(channel, disconnectionHandler, returnHandler) {
|
|
132
|
+
removeListeners(channel, disconnectionHandler, returnHandler);
|
|
130
133
|
returnChannelFn(channel);
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
function deferReturnChannel(channel,
|
|
136
|
+
function deferReturnChannel(channel, disconnectionHandler, returnHandler) {
|
|
134
137
|
channel.once('drain', () => {
|
|
135
|
-
returnChannel(channel,
|
|
138
|
+
returnChannel(channel, disconnectionHandler, returnHandler);
|
|
136
139
|
});
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
function destroyChannel(channel,
|
|
140
|
-
removeListeners(channel,
|
|
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
|
|
167
|
-
|
|
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',
|
|
170
|
-
channel.connection.once('close',
|
|
182
|
+
channel.connection.once('error', disconnectionHandler);
|
|
183
|
+
channel.connection.once('close', disconnectionHandler);
|
|
171
184
|
}
|
|
172
185
|
|
|
173
|
-
function removeListeners(channel,
|
|
186
|
+
function removeListeners(channel, disconnectionHandler, returnHandler) {
|
|
174
187
|
channel.removeAllListeners('drain');
|
|
175
|
-
channel.removeListener('error',
|
|
188
|
+
channel.removeListener('error', disconnectionHandler);
|
|
176
189
|
channel.removeListener('return', returnHandler);
|
|
177
|
-
channel.connection.removeListener('error',
|
|
178
|
-
channel.connection.removeListener('close',
|
|
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
|
+
}
|
package/lib/amqp/Subscription.js
CHANGED
|
@@ -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
|
|
65
|
-
const onMessage = _onMessage.bind(null, session, config,
|
|
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
|
-
|
|
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,
|
|
92
|
-
if (!message) return handleConsumerCancel(session, config,
|
|
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
|
|
252
|
+
function attachDisconnectionHandlers(channel, session, config) {
|
|
253
253
|
/* eslint-disable no-use-before-define */
|
|
254
254
|
const connection = channel.connection;
|
|
255
|
-
const
|
|
256
|
-
channel.removeListener('error',
|
|
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',
|
|
261
|
-
connection.removeListener('close',
|
|
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,
|
|
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 (
|
|
283
|
+
if (removeDisconnectionHandler) removeDisconnectionHandler();
|
|
273
284
|
session.emit('error', err);
|
|
274
|
-
config
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
|
474
|
+
function attachDisconnectionHandlers(config) {
|
|
476
475
|
connection.removeAllListeners('error');
|
|
477
|
-
const
|
|
478
|
-
connection.on('error',
|
|
479
|
-
connection.on('close',
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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": "
|
|
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": {
|