amqplib 1.0.6 → 1.1.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 +7 -0
- package/README.md +111 -0
- package/callback_api.js +16 -1
- package/channel_api.js +9 -1
- package/lib/callback_model.js +1 -1
- package/lib/channel.js +9 -8
- package/lib/channel_model.js +1 -1
- package/lib/connection.js +9 -8
- package/lib/recovery.js +466 -0
- package/lib/safe_emit.js +13 -0
- package/package.json +1 -1
- package/test/callback_api.test.js +11 -0
- package/test/channel.test.js +307 -1
- package/test/channel_api.test.js +9 -0
- package/test/connection.test.js +351 -0
- package/test/recovery.test.js +194 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Change log for amqplib
|
|
2
2
|
|
|
3
|
+
## v1.1.0
|
|
4
|
+
- Add connection recovery via `connectWithRecoveryPromise` and `connectWithRecoveryCallback` — automatically reconnects on connection loss with configurable backoff delay, jitter, and retry limits (thanks [@ShiriNmi1520](https://github.com/ShiriNmi1520))
|
|
5
|
+
|
|
6
|
+
## v1.0.7
|
|
7
|
+
- Fix `update-secret-ok` event not being forwarded by `ChannelModel` and `CallbackModel` (fixes #849)
|
|
8
|
+
- Add `handler-error` event to connections and channels. If a user-supplied event handler (e.g. `connection.on('close', ...)`, `channel.on('error', ...)`, `channel.on('delivery', ...)` etc.) throws a synchronous error, amqplib will emit a `handler-error` event on the same emitter with the thrown error and the name of the event whose handler threw — provided a `handler-error` listener is registered. If no `handler-error` listener is registered, behaviour is unchanged from previous versions. Note: in previous versions, errors thrown in connection `close` event handlers were silently swallowed; errors thrown in channel event handlers (other than `delivery`/`return`) would kill the channel and possibly the connection (fixes #334).
|
|
9
|
+
|
|
3
10
|
## v1.0.6
|
|
4
11
|
- Fix channel.get() not invoking callback with error on channel close; previously only an error event was emitted (fixes #832). **Note:** if you use the callback API, ensure your channel.get() callbacks handle errors — they will now be invoked in error cases where previously they were not. If you use the promise API, the returned promise now rejects with a proper Error object (with .code, .classId and .methodId properties) rather than a raw close frame.
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -43,10 +43,16 @@ const queue = 'tasks';
|
|
|
43
43
|
amqplib.connect('amqp://localhost', (err, conn) => {
|
|
44
44
|
if (err) throw err;
|
|
45
45
|
|
|
46
|
+
conn.on('error', (err) => { console.error('Connection error:', err); });
|
|
47
|
+
conn.on('handler-error', (err, event) => { console.error(`Uncaught exception in connection ${event} listener:`, err); });
|
|
48
|
+
|
|
46
49
|
// Listener
|
|
47
50
|
conn.createChannel((err, ch2) => {
|
|
48
51
|
if (err) throw err;
|
|
49
52
|
|
|
53
|
+
ch2.on('error', (err) => { console.error('Channel error:', err); });
|
|
54
|
+
ch2.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
|
|
55
|
+
|
|
50
56
|
ch2.assertQueue(queue);
|
|
51
57
|
|
|
52
58
|
ch2.consume(queue, (msg) => {
|
|
@@ -63,6 +69,8 @@ amqplib.connect('amqp://localhost', (err, conn) => {
|
|
|
63
69
|
conn.createChannel((err, ch1) => {
|
|
64
70
|
if (err) throw err;
|
|
65
71
|
|
|
72
|
+
ch1.on('error', (err) => { console.error('Channel error:', err); });
|
|
73
|
+
ch1.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
|
|
66
74
|
ch1.assertQueue(queue);
|
|
67
75
|
|
|
68
76
|
setInterval(() => {
|
|
@@ -80,8 +88,12 @@ const amqplib = require('amqplib');
|
|
|
80
88
|
(async () => {
|
|
81
89
|
const queue = 'tasks';
|
|
82
90
|
const conn = await amqplib.connect('amqp://localhost');
|
|
91
|
+
conn.on('error', (err) => { console.error('Connection error:', err); });
|
|
92
|
+
conn.on('handler-error', (err, event) => { console.error(`Uncaught exception in connection ${event} listener:`, err); });
|
|
83
93
|
|
|
84
94
|
const ch1 = await conn.createChannel();
|
|
95
|
+
ch1.on('error', (err) => { console.error('Channel error:', err); });
|
|
96
|
+
ch1.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
|
|
85
97
|
await ch1.assertQueue(queue);
|
|
86
98
|
|
|
87
99
|
// Listener
|
|
@@ -96,6 +108,8 @@ const amqplib = require('amqplib');
|
|
|
96
108
|
|
|
97
109
|
// Sender
|
|
98
110
|
const ch2 = await conn.createChannel();
|
|
111
|
+
ch2.on('error', (err) => { console.error('Channel error:', err); });
|
|
112
|
+
ch2.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
|
|
99
113
|
|
|
100
114
|
setInterval(() => {
|
|
101
115
|
ch2.sendToQueue(queue, Buffer.from('something to do'));
|
|
@@ -104,6 +118,103 @@ const amqplib = require('amqplib');
|
|
|
104
118
|
|
|
105
119
|
```
|
|
106
120
|
|
|
121
|
+
## Opt-in recovery
|
|
122
|
+
|
|
123
|
+
Automatic recovery is available as an opt-in feature through `connect` options:
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
const amqplib = require('amqplib');
|
|
127
|
+
|
|
128
|
+
const connection = await amqplib.connect('amqp://localhost', {
|
|
129
|
+
recovery: {
|
|
130
|
+
initialDelay: 200, // ms
|
|
131
|
+
maxDelay: 5000, // ms
|
|
132
|
+
factor: 2,
|
|
133
|
+
jitter: 0.2,
|
|
134
|
+
maxRetries: Infinity,
|
|
135
|
+
async setup(model) {
|
|
136
|
+
// Called after every successful (re)connect.
|
|
137
|
+
// Recreate topology/consumers here.
|
|
138
|
+
const ch = await model.createChannel();
|
|
139
|
+
await ch.assertQueue('tasks', {durable: true});
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
connection.on('connect', () => {
|
|
145
|
+
console.log('connected');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
connection.on('disconnect', (err) => {
|
|
149
|
+
console.warn('disconnected', err.message);
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Callback API supports the same option:
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
const amqplib = require('amqplib/callback_api');
|
|
157
|
+
|
|
158
|
+
amqplib.connect(
|
|
159
|
+
'amqp://localhost',
|
|
160
|
+
{
|
|
161
|
+
recovery: {
|
|
162
|
+
initialDelay: 200,
|
|
163
|
+
maxDelay: 5000,
|
|
164
|
+
setup(model, done) {
|
|
165
|
+
model.createChannel((err, ch) => {
|
|
166
|
+
if (err) return done(err);
|
|
167
|
+
ch.assertQueue('tasks', {durable: true}, done);
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
(err, conn) => {
|
|
173
|
+
if (err) throw err;
|
|
174
|
+
conn.on('connect', () => console.log('connected'));
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Without `recovery` options, behavior is unchanged.
|
|
180
|
+
|
|
181
|
+
## Error handling in event handlers
|
|
182
|
+
|
|
183
|
+
If a user-supplied event handler throws a synchronous error, the throw will
|
|
184
|
+
propagate into amqplib internals. Depending on where in the call stack it
|
|
185
|
+
escapes, this can silently swallow the error, or close the channel or
|
|
186
|
+
connection.
|
|
187
|
+
|
|
188
|
+
To avoid this, register a `handler-error` listener on the connection and on
|
|
189
|
+
each channel. If a listener is present, amqplib will catch any throw from a
|
|
190
|
+
user event handler and deliver it there instead of letting it propagate
|
|
191
|
+
internally. The listener receives the thrown error and the name of the event
|
|
192
|
+
whose handler threw.
|
|
193
|
+
|
|
194
|
+
Note that `handler-error` is not a replacement for the `error` event.
|
|
195
|
+
The `error` event is emitted by amqplib itself when the connection or channel
|
|
196
|
+
encounters a protocol-level error. The `handler-error` event is only emitted
|
|
197
|
+
when *your own* event listener throws.
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
const connection = await amqp.connect('amqp://localhost');
|
|
201
|
+
|
|
202
|
+
connection.on('error', (err) => { /* handle protocol errors */ });
|
|
203
|
+
connection.on('handler-error', (err, event) => {
|
|
204
|
+
console.error(`Uncaught exception in connection ${event} listener:`, err);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const channel = await connection.createChannel();
|
|
208
|
+
|
|
209
|
+
channel.on('error', (err) => { /* handle protocol errors */ });
|
|
210
|
+
channel.on('handler-error', (err, event) => {
|
|
211
|
+
console.error(`Uncaught exception in channel ${event} listener:`, err);
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
If no `handler-error` listener is registered, behaviour is unchanged from
|
|
216
|
+
previous versions.
|
|
217
|
+
|
|
107
218
|
## Running tests
|
|
108
219
|
|
|
109
220
|
npm test
|
package/callback_api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const raw_connect = require('./lib/connect').connect;
|
|
2
2
|
const CallbackModel = require('./lib/callback_model').CallbackModel;
|
|
3
|
+
const recovery = require('./lib/recovery');
|
|
3
4
|
|
|
4
5
|
// Supports three shapes:
|
|
5
6
|
// connect(url, options, callback)
|
|
@@ -15,7 +16,21 @@ function connect(url, options, cb) {
|
|
|
15
16
|
options = false;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
const {connectionOptions, recovery: recoveryOptions} = recovery.splitConnectionOptions(options);
|
|
20
|
+
if (recovery.recoveryEnabled(recoveryOptions)) {
|
|
21
|
+
const openModel = () => {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
raw_connect(url, connectionOptions, (err, c) => {
|
|
24
|
+
if (err === null) resolve(new CallbackModel(c));
|
|
25
|
+
else reject(err);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return recovery.connectWithRecoveryCallback(openModel, recoveryOptions, cb);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
raw_connect(url, connectionOptions, (err, c) => {
|
|
19
34
|
if (err === null) cb(null, new CallbackModel(c));
|
|
20
35
|
else cb(err);
|
|
21
36
|
});
|
package/channel_api.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
const raw_connect = require('./lib/connect').connect;
|
|
2
2
|
const ChannelModel = require('./lib/channel_model').ChannelModel;
|
|
3
3
|
const promisify = require('node:util').promisify;
|
|
4
|
+
const recovery = require('./lib/recovery');
|
|
4
5
|
|
|
5
6
|
function connect(url, connOptions) {
|
|
6
|
-
|
|
7
|
+
const {connectionOptions, recovery: recoveryOptions} = recovery.splitConnectionOptions(connOptions);
|
|
8
|
+
if (recovery.recoveryEnabled(recoveryOptions)) {
|
|
9
|
+
const openModel = () => promisify((cb) => raw_connect(url, connectionOptions, cb))().then((conn) => new ChannelModel(conn));
|
|
10
|
+
|
|
11
|
+
return recovery.connectWithRecoveryPromise(openModel, recoveryOptions);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return promisify((cb) => raw_connect(url, connectionOptions, cb))().then((conn) => new ChannelModel(conn));
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
module.exports.connect = connect;
|
package/lib/callback_model.js
CHANGED
|
@@ -7,7 +7,7 @@ class CallbackModel extends EventEmitter {
|
|
|
7
7
|
constructor(connection) {
|
|
8
8
|
super();
|
|
9
9
|
this.connection = connection;
|
|
10
|
-
['error', 'close', 'blocked', 'unblocked'].forEach((ev) => {
|
|
10
|
+
['error', 'close', 'blocked', 'unblocked', 'update-secret-ok'].forEach((ev) => {
|
|
11
11
|
connection.on(ev, this.emit.bind(this, ev));
|
|
12
12
|
});
|
|
13
13
|
}
|
package/lib/channel.js
CHANGED
|
@@ -7,6 +7,7 @@ const EventEmitter = require('node:events');
|
|
|
7
7
|
const fmt = require('node:util').format;
|
|
8
8
|
const IllegalOperationError = require('./error').IllegalOperationError;
|
|
9
9
|
const stackCapture = require('./error').stackCapture;
|
|
10
|
+
const safeEmit = require('./safe_emit');
|
|
10
11
|
|
|
11
12
|
class Channel extends EventEmitter {
|
|
12
13
|
constructor(connection) {
|
|
@@ -128,7 +129,7 @@ class Channel extends EventEmitter {
|
|
|
128
129
|
invalidateSend(this, 'Channel closed', capturedStack);
|
|
129
130
|
this.accept = invalidOp('Channel closed', capturedStack);
|
|
130
131
|
this.connection.releaseChannel(this.ch);
|
|
131
|
-
this
|
|
132
|
+
safeEmit(this, 'close');
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
// Stop being able to send and receive methods and content. Used when
|
|
@@ -188,7 +189,7 @@ class Channel extends EventEmitter {
|
|
|
188
189
|
error.classId = defs.info(id).classId;
|
|
189
190
|
error.methodId = defs.info(id).methodId;
|
|
190
191
|
}
|
|
191
|
-
this
|
|
192
|
+
safeEmit(this, 'error', error);
|
|
192
193
|
});
|
|
193
194
|
}
|
|
194
195
|
|
|
@@ -250,7 +251,7 @@ class Channel extends EventEmitter {
|
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
onBufferDrain() {
|
|
253
|
-
this
|
|
254
|
+
safeEmit(this, 'drain');
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
accept(f) {
|
|
@@ -264,12 +265,12 @@ class Channel extends EventEmitter {
|
|
|
264
265
|
|
|
265
266
|
// confirmations, need to do confirm.select first
|
|
266
267
|
case defs.BasicAck:
|
|
267
|
-
return this
|
|
268
|
+
return safeEmit(this, 'ack', f.fields);
|
|
268
269
|
case defs.BasicNack:
|
|
269
|
-
return this
|
|
270
|
+
return safeEmit(this, 'nack', f.fields);
|
|
270
271
|
case defs.BasicCancel:
|
|
271
272
|
// The broker can send this if e.g., the queue is deleted.
|
|
272
|
-
return this
|
|
273
|
+
return safeEmit(this, 'cancel', f.fields);
|
|
273
274
|
|
|
274
275
|
case defs.ChannelClose: {
|
|
275
276
|
// Any remote closure is an error to us. Reject the pending reply
|
|
@@ -287,7 +288,7 @@ class Channel extends EventEmitter {
|
|
|
287
288
|
error.code = f.fields.replyCode;
|
|
288
289
|
error.classId = f.fields.classId;
|
|
289
290
|
error.methodId = f.fields.methodId;
|
|
290
|
-
this
|
|
291
|
+
safeEmit(this, 'error', error);
|
|
291
292
|
|
|
292
293
|
const s = stackCapture(emsg);
|
|
293
294
|
this.toClosed(s);
|
|
@@ -352,7 +353,7 @@ function acceptDeliveryOrReturn(f) {
|
|
|
352
353
|
const fields = f.fields;
|
|
353
354
|
return acceptMessage((message) => {
|
|
354
355
|
message.fields = fields;
|
|
355
|
-
this
|
|
356
|
+
safeEmit(this, event, message);
|
|
356
357
|
});
|
|
357
358
|
}
|
|
358
359
|
|
package/lib/channel_model.js
CHANGED
|
@@ -10,7 +10,7 @@ class ChannelModel extends EventEmitter {
|
|
|
10
10
|
super();
|
|
11
11
|
this.connection = connection;
|
|
12
12
|
|
|
13
|
-
['error', 'close', 'blocked', 'unblocked'].forEach((ev) => {
|
|
13
|
+
['error', 'close', 'blocked', 'unblocked', 'update-secret-ok'].forEach((ev) => {
|
|
14
14
|
connection.on(ev, this.emit.bind(this, ev));
|
|
15
15
|
});
|
|
16
16
|
}
|
package/lib/connection.js
CHANGED
|
@@ -17,6 +17,7 @@ const fmt = require('node:util').format;
|
|
|
17
17
|
const PassThrough = require('node:stream').PassThrough;
|
|
18
18
|
const IllegalOperationError = require('./error').IllegalOperationError;
|
|
19
19
|
const stackCapture = require('./error').stackCapture;
|
|
20
|
+
const safeEmit = require('./safe_emit');
|
|
20
21
|
|
|
21
22
|
// High-water mark for channel write buffers, in 'objects' (which are
|
|
22
23
|
// encoded frames as buffers).
|
|
@@ -281,7 +282,7 @@ class Connection extends EventEmitter {
|
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
closeWithError(reason, code, error) {
|
|
284
|
-
this
|
|
285
|
+
safeEmit(this, 'error', error);
|
|
285
286
|
this.closeBecause(reason, code);
|
|
286
287
|
}
|
|
287
288
|
|
|
@@ -290,7 +291,7 @@ class Connection extends EventEmitter {
|
|
|
290
291
|
// forestall any more calls to onSocketError, since we're signed
|
|
291
292
|
// up for `'error'` *and* `'end'`
|
|
292
293
|
this.expectSocketClose = true;
|
|
293
|
-
this
|
|
294
|
+
safeEmit(this, 'error', err);
|
|
294
295
|
const s = stackCapture('Socket error');
|
|
295
296
|
this.toClosed(s, err);
|
|
296
297
|
}
|
|
@@ -345,7 +346,7 @@ class Connection extends EventEmitter {
|
|
|
345
346
|
// This is certainly true now, if it wasn't before
|
|
346
347
|
this.expectSocketClose = true;
|
|
347
348
|
this.stream.end();
|
|
348
|
-
this
|
|
349
|
+
safeEmit(this, 'close', maybeErr);
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
_updateSecret(newSecret, reason, cb) {
|
|
@@ -363,7 +364,7 @@ class Connection extends EventEmitter {
|
|
|
363
364
|
const hb = new Heart(this.heartbeat, this.checkSend.bind(this), this.checkRecv.bind(this));
|
|
364
365
|
hb.on('timeout', () => {
|
|
365
366
|
const hberr = new Error('Heartbeat timeout');
|
|
366
|
-
this
|
|
367
|
+
safeEmit(this, 'error', hberr);
|
|
367
368
|
const s = stackCapture('Heartbeat timeout');
|
|
368
369
|
this.toClosed(s, hberr);
|
|
369
370
|
});
|
|
@@ -585,17 +586,17 @@ function channel0(connection) {
|
|
|
585
586
|
const e = new Error(emsg);
|
|
586
587
|
e.code = f.fields.replyCode;
|
|
587
588
|
if (isFatalError(e)) {
|
|
588
|
-
connection
|
|
589
|
+
safeEmit(connection, 'error', e);
|
|
589
590
|
}
|
|
590
591
|
connection.toClosed(s, e);
|
|
591
592
|
} else if (f.id === defs.ConnectionBlocked) {
|
|
592
593
|
connection.blocked = true;
|
|
593
|
-
connection
|
|
594
|
+
safeEmit(connection, 'blocked', f.fields.reason);
|
|
594
595
|
} else if (f.id === defs.ConnectionUnblocked) {
|
|
595
596
|
connection.blocked = false;
|
|
596
|
-
connection
|
|
597
|
+
safeEmit(connection, 'unblocked');
|
|
597
598
|
} else if (f.id === defs.ConnectionUpdateSecretOk) {
|
|
598
|
-
connection
|
|
599
|
+
safeEmit(connection, 'update-secret-ok');
|
|
599
600
|
} else {
|
|
600
601
|
connection.closeWithError(
|
|
601
602
|
fmt('Unexpected frame on channel 0'),
|