comq 0.23.0 → 0.24.1
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/discussions/amqp-1.0.md +127 -0
- package/features/heartbeat.feature +16 -0
- package/features/rpc.call.feature +23 -1
- package/features/rpc.call.recovery.feature +2 -23
- package/features/rpc.parked.feature +6 -6
- package/features/rpc.shards.retransmission.feature +46 -0
- package/features/shards.feature +1 -1
- package/features/silence.feature +3 -2
- package/features/steps/.types/context.d.ts +6 -1
- package/features/steps/brokers.js +1 -1
- package/features/steps/call.js +90 -30
- package/features/steps/connection.js +23 -0
- package/features/steps/context.js +1 -2
- package/features/steps/events.js +1 -1
- package/features/steps/hooks.js +0 -1
- package/features/steps/network.js +11 -2
- package/features/steps/networks.js +31 -2
- package/features/steps/parking.js +5 -5
- package/features/steps/retransmission.js +165 -0
- package/features/steps/rpc.js +0 -6
- package/package.json +20 -4
- package/readme.md +46 -46
- package/source/channel.js +3 -1
- package/source/connection.js +10 -90
- package/source/io.js +106 -25
- package/source/shards/channel.js +73 -12
- package/test/amqplib.mock.js +1 -1
- package/test/connection.mock.js +3 -2
- package/test/connection.test.js +16 -78
- package/test/{io.request.terms.test.js → io.call.terms.test.js} +31 -30
- package/test/io.call.test.js +5 -13
- package/test/io.reply.test.js +5 -3
- package/test/io.request.test.js +277 -2
- package/test/memory.test.js +1 -3
- package/test/shards/channel.test.js +168 -2
- package/types/amqp.d.ts +1 -1
- package/types/channel.d.ts +14 -5
- package/types/io.d.ts +18 -8
- package/features/rpc.timeout.feature +0 -19
- package/source/batch.js +0 -62
- package/test/batch.test.js +0 -94
package/source/io.js
CHANGED
|
@@ -84,16 +84,21 @@ class IO {
|
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
+
* Waits for the Reply for as long as it takes: a Request is answered eventually, and one no
|
|
88
|
+
* Producer can answer is parked rather than abandoned.
|
|
89
|
+
*
|
|
87
90
|
* @param {string} queue
|
|
88
91
|
* @param {any | Readable} payload
|
|
89
|
-
* @param {comq.Encoding
|
|
92
|
+
* @param {comq.Encoding} [encoding]
|
|
90
93
|
* @returns {Promise<any | Readable>}
|
|
91
94
|
*/
|
|
92
|
-
request (queue, payload,
|
|
93
|
-
|
|
95
|
+
async request (queue, payload, encoding) {
|
|
96
|
+
// a caller of the version that took a timeout here would otherwise wait on in silence
|
|
97
|
+
if (typeof encoding === 'object' && encoding !== null) {
|
|
98
|
+
throw new TypeError('A Request takes an encoding and waits for its Reply')
|
|
99
|
+
}
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
return abortable(this.#request(queue, payload, settled), settled.signal)
|
|
101
|
+
return await this.#request(queue, payload, { encoding })
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
/**
|
|
@@ -103,12 +108,13 @@ class IO {
|
|
|
103
108
|
* @param {string} exchange
|
|
104
109
|
* @param {string} key
|
|
105
110
|
* @param {any} payload
|
|
106
|
-
* @param {comq.Encoding | comq.
|
|
111
|
+
* @param {comq.Encoding | comq.CallOptions} [options]
|
|
107
112
|
* @returns {Promise<any | Readable>}
|
|
108
113
|
*/
|
|
109
114
|
call (exchange, key, payload, options) {
|
|
110
115
|
const settled = terms(options)
|
|
111
116
|
|
|
117
|
+
// the channels a first call declares are waited for within the terms too
|
|
112
118
|
return abortable(this.#call(exchange, key, payload, settled), settled.signal)
|
|
113
119
|
}
|
|
114
120
|
|
|
@@ -129,8 +135,7 @@ class IO {
|
|
|
129
135
|
await this.#requests.held(exchange, exchange + '.' + key, key, consumer)
|
|
130
136
|
})
|
|
131
137
|
|
|
132
|
-
// failsafe is aimed to retransmit unanswered messages
|
|
133
|
-
// re-sent Request keeps the deadline of the first
|
|
138
|
+
// failsafe is aimed to retransmit unanswered messages
|
|
134
139
|
#request = lazy(this, [this.#createRequestReplyChannels, this.#consumeReplies],
|
|
135
140
|
failsafe(this, this.#recover,
|
|
136
141
|
/**
|
|
@@ -149,9 +154,10 @@ class IO {
|
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
return await this.#ask(queue, payload, terms,
|
|
152
|
-
(buffer, properties) => this.#requests.send(queue, buffer, properties))
|
|
157
|
+
(buffer, properties, via) => this.#requests.send(queue, buffer, properties, via))
|
|
153
158
|
}))
|
|
154
159
|
|
|
160
|
+
// the terms are taken once, so a re-sent call keeps the deadline of the first
|
|
155
161
|
#call = lazy(this, [this.#createRequestReplyChannels, this.#consumeReplies],
|
|
156
162
|
failsafe(this, this.#recover,
|
|
157
163
|
/**
|
|
@@ -164,7 +170,8 @@ class IO {
|
|
|
164
170
|
async (exchange, key, payload, terms) => {
|
|
165
171
|
// mandatory: a Request nobody holds the key of is returned rather than dropped
|
|
166
172
|
return await this.#ask(exchange, payload, terms,
|
|
167
|
-
(buffer, properties) =>
|
|
173
|
+
(buffer, properties, via) =>
|
|
174
|
+
this.#requests.route(exchange, key, buffer, { ...properties, mandatory: true }, via))
|
|
168
175
|
}))
|
|
169
176
|
|
|
170
177
|
consume = lazy(this, this.#createEventChannel,
|
|
@@ -351,12 +358,20 @@ class IO {
|
|
|
351
358
|
return channel
|
|
352
359
|
}
|
|
353
360
|
|
|
361
|
+
/**
|
|
362
|
+
* On a sharded connection, what is re-sent is what the shard in question holds: the index of
|
|
363
|
+
* the shard comes with each of the events, and a Request that went through another shard is
|
|
364
|
+
* answered through that one.
|
|
365
|
+
*/
|
|
354
366
|
#setupRetransmission () {
|
|
355
367
|
if (this.#requests.sharded === true) {
|
|
356
368
|
// a shard leaves the pool when it rejects a publish, and is lost when its
|
|
357
369
|
// connection drops, which leaves an already sent request unanswered
|
|
358
370
|
this.#requests.diagnose('remove', this.#retransmit)
|
|
359
371
|
this.#requests.diagnose('lost', this.#retransmit)
|
|
372
|
+
|
|
373
|
+
// a returned call is published on another shard, and is held by that one from then on
|
|
374
|
+
this.#requests.diagnose('reroute', this.#rerouted)
|
|
360
375
|
} else {
|
|
361
376
|
this.#requests.diagnose('recover', this.#retransmit)
|
|
362
377
|
}
|
|
@@ -456,11 +471,33 @@ class IO {
|
|
|
456
471
|
|
|
457
472
|
if (signal !== undefined) this.#abandon(request, reply, signal)
|
|
458
473
|
|
|
459
|
-
|
|
474
|
+
request.publishing = true
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
await abortable(publish(buffer, properties, (index) => this.#board(request, index)), signal)
|
|
478
|
+
} finally {
|
|
479
|
+
request.publishing = false
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// the shard it went through was lost while it was being published, and it has not failed
|
|
483
|
+
// over to another since
|
|
484
|
+
if (request.lost === true) this.#resend(request, reply)
|
|
460
485
|
|
|
461
486
|
return reply
|
|
462
487
|
}
|
|
463
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Records the shard a Request is published through. It is recorded as the shard is chosen,
|
|
491
|
+
* rather than once the publish is done, so that a shard lost in between is known to hold it.
|
|
492
|
+
*
|
|
493
|
+
* @param {comq.Request} request
|
|
494
|
+
* @param {number} index
|
|
495
|
+
*/
|
|
496
|
+
#board (request, index) {
|
|
497
|
+
request.shard = index
|
|
498
|
+
request.lost = false
|
|
499
|
+
}
|
|
500
|
+
|
|
464
501
|
/**
|
|
465
502
|
* Stops waiting for a Reply once the signal aborts. The Request is forgotten at once, so a
|
|
466
503
|
* retransmission leaves it be, and a Reply arriving later finds nobody waiting for it.
|
|
@@ -488,17 +525,41 @@ class IO {
|
|
|
488
525
|
* @param {comq.amqp.Message} message
|
|
489
526
|
*/
|
|
490
527
|
#returned = (message) => {
|
|
528
|
+
const pending = this.#pendingOf(message)
|
|
529
|
+
|
|
530
|
+
if (pending === undefined) return
|
|
531
|
+
|
|
532
|
+
const [reply, request] = pending
|
|
533
|
+
|
|
534
|
+
request.emitter.off(request.properties.correlationId)
|
|
535
|
+
this.#pendingReplies.delete(reply)
|
|
536
|
+
reply.reject(new Unroutable(message.fields.exchange, message.fields.routingKey))
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* A Request one shard returned is published on another.
|
|
541
|
+
*
|
|
542
|
+
* @param {comq.amqp.Message} message
|
|
543
|
+
* @param {number} index
|
|
544
|
+
*/
|
|
545
|
+
#rerouted = (message, index) => {
|
|
546
|
+
const pending = this.#pendingOf(message)
|
|
547
|
+
|
|
548
|
+
if (pending !== undefined) this.#board(pending[1], index)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @param {comq.amqp.Message} message
|
|
553
|
+
* @return {[Promex, comq.Request] | undefined}
|
|
554
|
+
*/
|
|
555
|
+
#pendingOf (message) {
|
|
491
556
|
const { correlationId, replyTo } = message.properties
|
|
492
557
|
|
|
493
558
|
for (const [reply, request] of this.#pendingReplies) {
|
|
494
559
|
if (request.properties.correlationId !== correlationId) continue
|
|
495
560
|
if (request.emitter.queue !== replyTo) continue
|
|
496
561
|
|
|
497
|
-
request
|
|
498
|
-
this.#pendingReplies.delete(reply)
|
|
499
|
-
reply.reject(new Unroutable(message.fields.exchange, message.fields.routingKey))
|
|
500
|
-
|
|
501
|
-
return
|
|
562
|
+
return [reply, request]
|
|
502
563
|
}
|
|
503
564
|
}
|
|
504
565
|
|
|
@@ -659,24 +720,44 @@ class IO {
|
|
|
659
720
|
// an unroutable reply must be returned by the broker rather than dropped
|
|
660
721
|
properties.mandatory = true
|
|
661
722
|
|
|
662
|
-
|
|
723
|
+
// back the way the request came: see `fire` of a sharded channel
|
|
724
|
+
return await this.#replies.fire(replyTo, buffer, properties, request)
|
|
663
725
|
}
|
|
664
726
|
|
|
665
727
|
#recover (exception) {
|
|
666
728
|
if (exception !== RETRANSMISSION) return false
|
|
667
729
|
}
|
|
668
730
|
|
|
669
|
-
|
|
731
|
+
/**
|
|
732
|
+
* Re-sends the Requests awaiting their replies on a shard, or on the connection when no shard
|
|
733
|
+
* is given. A Request that went through another shard is not re-sent: it is answered through
|
|
734
|
+
* that shard, and it is not for comq to assume that it is idempotent.
|
|
735
|
+
*
|
|
736
|
+
* @param {number} [index]
|
|
737
|
+
*/
|
|
738
|
+
#retransmit = (index) => {
|
|
670
739
|
for (const [reply, request] of this.#pendingReplies) {
|
|
671
|
-
|
|
672
|
-
// that are still flowing over the other shards in place
|
|
673
|
-
request.emitter.off(request.properties.correlationId)
|
|
740
|
+
if (index !== undefined && request.shard !== index) continue
|
|
674
741
|
|
|
675
|
-
//
|
|
676
|
-
|
|
742
|
+
// one still being published is re-sent once it is, unless it fails over to another shard
|
|
743
|
+
if (request.publishing === true) request.lost = true
|
|
744
|
+
else this.#resend(request, reply)
|
|
677
745
|
}
|
|
678
746
|
}
|
|
679
747
|
|
|
748
|
+
/**
|
|
749
|
+
* @param {comq.Request} request
|
|
750
|
+
* @param {Promex} reply
|
|
751
|
+
*/
|
|
752
|
+
#resend (request, reply) {
|
|
753
|
+
// detaching this attempt alone leaves the listeners of the reply streams
|
|
754
|
+
// that are still flowing over the other shards in place
|
|
755
|
+
request.emitter.off(request.properties.correlationId)
|
|
756
|
+
|
|
757
|
+
// trigger failsafe attribute
|
|
758
|
+
reply.reject(RETRANSMISSION)
|
|
759
|
+
}
|
|
760
|
+
|
|
680
761
|
/**
|
|
681
762
|
* @param {any} payload
|
|
682
763
|
* @param {comq.Encoding} [contentType]
|
|
@@ -708,10 +789,10 @@ const REPLY = 'comq.reply'
|
|
|
708
789
|
const CONTROL = 'control'
|
|
709
790
|
|
|
710
791
|
/**
|
|
711
|
-
* What a caller passed, settled once: a timeout becomes the moment the
|
|
792
|
+
* What a caller passed, settled once: a timeout becomes the moment the call expires, so that
|
|
712
793
|
* every attempt of it shares one deadline.
|
|
713
794
|
*
|
|
714
|
-
* @param {comq.Encoding | comq.
|
|
795
|
+
* @param {comq.Encoding | comq.CallOptions} [options]
|
|
715
796
|
* @returns {comq.Terms}
|
|
716
797
|
*/
|
|
717
798
|
function terms (options) {
|
package/source/shards/channel.js
CHANGED
|
@@ -57,8 +57,12 @@ class Channel {
|
|
|
57
57
|
await Promise.any(promises)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* A message is told which shard it arrived on, so that whatever answers it can go back the same
|
|
62
|
+
* way. See `fire`.
|
|
63
|
+
*/
|
|
60
64
|
async consume (queue, consumer) {
|
|
61
|
-
return await this.#every((channel) => channel.consume(queue, consumer))
|
|
65
|
+
return await this.#every((channel) => channel.consume(queue, arrival(consumer, channel)))
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
async subscribe (queue, group, consumer) {
|
|
@@ -74,24 +78,51 @@ class Channel {
|
|
|
74
78
|
* claiming it, and holds it as well once it is let go.
|
|
75
79
|
*/
|
|
76
80
|
async held (exchange, queue, key, consumer) {
|
|
77
|
-
await Promise.any(this.#apply((channel) => channel.held(exchange, queue, key, consumer)))
|
|
81
|
+
await Promise.any(this.#apply((channel) => channel.held(exchange, queue, key, arrival(consumer, channel))))
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} queue
|
|
86
|
+
* @param {Buffer} buffer
|
|
87
|
+
* @param {comq.amqp.options.Publish} [options]
|
|
88
|
+
* @param {(index: number) => void} [via] told which shard the message is published through,
|
|
89
|
+
* as soon as one is chosen and again whenever the publish fails over to another
|
|
90
|
+
*/
|
|
91
|
+
async send (queue, buffer, options, via) {
|
|
92
|
+
await this.#one((channel) => channel.send(queue, buffer, options), { via })
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
async publish (exchange, buffer, options) {
|
|
85
96
|
await this.#one((channel) => channel.publish(exchange, buffer, options))
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} exchange
|
|
101
|
+
* @param {string} key
|
|
102
|
+
* @param {Buffer} buffer
|
|
103
|
+
* @param {comq.amqp.options.Publish} [options]
|
|
104
|
+
* @param {(index: number) => void} [via] as `send` has it
|
|
105
|
+
*/
|
|
106
|
+
async route (exchange, key, buffer, options, via) {
|
|
107
|
+
await this.#one((channel) => channel.route(exchange, key, buffer, options), { via })
|
|
90
108
|
}
|
|
91
109
|
|
|
92
|
-
|
|
110
|
+
/**
|
|
111
|
+
* A reply goes back through the shard the message it answers arrived on. Whoever sent that
|
|
112
|
+
* message re-sends what it sent through a shard once the shard is lost, and nothing else, so a
|
|
113
|
+
* reply taking another way could be lost with a shard that nothing is re-sent for. It takes
|
|
114
|
+
* another way when that shard is not reachable, which leaves its message unacknowledged there,
|
|
115
|
+
* to be delivered and answered again.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} queue
|
|
118
|
+
* @param {Buffer} buffer
|
|
119
|
+
* @param {comq.amqp.options.Publish} [options]
|
|
120
|
+
* @param {comq.amqp.Message} [origin] the message this one answers
|
|
121
|
+
* @return {Promise<boolean>}
|
|
122
|
+
*/
|
|
123
|
+
async fire (queue, buffer, options, origin) {
|
|
93
124
|
// noinspection JSValidateTypes
|
|
94
|
-
return await this.#one((channel) => channel.fire(queue, buffer, options))
|
|
125
|
+
return await this.#one((channel) => channel.fire(queue, buffer, options), { prefer: origin?.[ARRIVAL] })
|
|
95
126
|
}
|
|
96
127
|
|
|
97
128
|
async close () {
|
|
@@ -230,6 +261,9 @@ class Channel {
|
|
|
230
261
|
|
|
231
262
|
const next = rest[Math.floor(Math.random() * rest.length)]
|
|
232
263
|
|
|
264
|
+
// the message is on that shard now, and is lost with it rather than with this one
|
|
265
|
+
this.#diagnostics.emit(REROUTE, message, next.index)
|
|
266
|
+
|
|
233
267
|
// a Request under a key goes through its routed exchange, which `route` declares on that
|
|
234
268
|
// shard before publishing: publishing to an exchange a broker lacks closes the connection
|
|
235
269
|
const retried = exchange === DEFAULT
|
|
@@ -374,9 +408,13 @@ class Channel {
|
|
|
374
408
|
}
|
|
375
409
|
|
|
376
410
|
/**
|
|
411
|
+
* Publishes through a shard chosen at random, or through the one preferred while it is
|
|
412
|
+
* reachable, and fails over to another when that one fails.
|
|
413
|
+
*
|
|
377
414
|
* @param {(channel: comq.Channel) => void} fn
|
|
415
|
+
* @param {{ via?: (index: number) => void, prefer?: number }} [route]
|
|
378
416
|
*/
|
|
379
|
-
async #one (fn) {
|
|
417
|
+
async #one (fn, route = {}) {
|
|
380
418
|
// capture before the check: recover may replace `#recovery` in between
|
|
381
419
|
const waiting = this.#recovery
|
|
382
420
|
const pool = this.#reachable()
|
|
@@ -384,21 +422,38 @@ class Channel {
|
|
|
384
422
|
if (pool.length === 0) {
|
|
385
423
|
await waiting
|
|
386
424
|
|
|
387
|
-
return this.#one(fn)
|
|
425
|
+
return this.#one(fn, route)
|
|
388
426
|
}
|
|
389
427
|
|
|
390
|
-
const channel = pool
|
|
428
|
+
const channel = pool.find((channel) => channel.index === route.prefer) ??
|
|
429
|
+
pool[Math.floor(Math.random() * pool.length)]
|
|
430
|
+
|
|
431
|
+
// told before the publish is awaited: a shard lost while it is under way has it on board
|
|
432
|
+
route.via?.(channel.index)
|
|
391
433
|
|
|
392
434
|
try {
|
|
393
435
|
return await fn(channel)
|
|
394
436
|
} catch {
|
|
395
437
|
this.#remove(channel)
|
|
396
438
|
|
|
397
|
-
return this.#one(fn)
|
|
439
|
+
return this.#one(fn, route)
|
|
398
440
|
}
|
|
399
441
|
}
|
|
400
442
|
}
|
|
401
443
|
|
|
444
|
+
/**
|
|
445
|
+
* @param {comq.channels.Consumer} consumer
|
|
446
|
+
* @param {comq.Channel} channel
|
|
447
|
+
* @return {comq.channels.Consumer}
|
|
448
|
+
*/
|
|
449
|
+
function arrival (consumer, channel) {
|
|
450
|
+
return (message) => {
|
|
451
|
+
message[ARRIVAL] = channel.index
|
|
452
|
+
|
|
453
|
+
return consumer(message)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
402
457
|
/**
|
|
403
458
|
* @param {comq.Connection[]} connections
|
|
404
459
|
* @param {comq.topology.type} type
|
|
@@ -418,6 +473,12 @@ const REMOVE = 'remove'
|
|
|
418
473
|
const RETURN_HEADER = 'x-return'
|
|
419
474
|
const LOST = 'lost'
|
|
420
475
|
|
|
476
|
+
/** A returned message published on another shard, which is where it can be lost from now on. */
|
|
477
|
+
const REROUTE = 'reroute'
|
|
478
|
+
|
|
479
|
+
/** Which shard a consumed message arrived on. */
|
|
480
|
+
const ARRIVAL = Symbol('arrival')
|
|
481
|
+
|
|
421
482
|
function noop () {}
|
|
422
483
|
|
|
423
484
|
exports.create = create
|
package/test/amqplib.mock.js
CHANGED
|
@@ -58,7 +58,7 @@ class Connection extends EventEmitter {
|
|
|
58
58
|
|
|
59
59
|
const connect = async (url) => new Connection(url)
|
|
60
60
|
|
|
61
|
-
/** @type {jest.MockedObject<import('amqplib')>} */
|
|
61
|
+
/** @type {jest.MockedObject<import('@toa.io/amqplib')>} */
|
|
62
62
|
const amqplib = {
|
|
63
63
|
connect: jest.fn(connect)
|
|
64
64
|
}
|
package/test/connection.mock.js
CHANGED
|
@@ -6,13 +6,14 @@ const channel = /** @type {jest.MockedFunction<(sharded?: boolean, index?: numbe
|
|
|
6
6
|
sharded,
|
|
7
7
|
consume: jest.fn(async () => undefined),
|
|
8
8
|
deliver: jest.fn(async () => undefined),
|
|
9
|
-
|
|
9
|
+
// a sharded channel tells which shard a message goes through, the first one unless told otherwise
|
|
10
|
+
send: jest.fn(async (_queue, _buffer, _options, via) => { if (sharded) via?.(0) }),
|
|
10
11
|
fire: jest.fn(async () => undefined),
|
|
11
12
|
subscribe: jest.fn(async () => undefined),
|
|
12
13
|
bound: jest.fn(async () => undefined),
|
|
13
14
|
held: jest.fn(async () => undefined),
|
|
14
15
|
publish: jest.fn(async () => undefined),
|
|
15
|
-
route: jest.fn(async () =>
|
|
16
|
+
route: jest.fn(async (_exchange, _key, _buffer, _options, via) => { if (sharded) via?.(0) }),
|
|
16
17
|
diagnose: jest.fn(async () => undefined),
|
|
17
18
|
forget: jest.fn(() => undefined),
|
|
18
19
|
seal: jest.fn(async () => undefined),
|
package/test/connection.test.js
CHANGED
|
@@ -10,7 +10,7 @@ const { amqplib, connect } = require('./amqplib.mock')
|
|
|
10
10
|
const { channel: create } = require('./connection.mock')
|
|
11
11
|
const mock = { amqplib, channel: { create } }
|
|
12
12
|
|
|
13
|
-
jest.mock('amqplib', () => mock.amqplib)
|
|
13
|
+
jest.mock('@toa.io/amqplib', () => mock.amqplib)
|
|
14
14
|
jest.mock('../source/channel', () => mock.channel)
|
|
15
15
|
|
|
16
16
|
const presets = require('../source/topology')
|
|
@@ -543,109 +543,47 @@ describe('channel exhaustion', () => {
|
|
|
543
543
|
})
|
|
544
544
|
})
|
|
545
545
|
|
|
546
|
-
describe('
|
|
546
|
+
describe('silence', () => {
|
|
547
547
|
afterEach(() => {
|
|
548
548
|
jest.useRealTimers()
|
|
549
549
|
})
|
|
550
550
|
|
|
551
|
-
|
|
551
|
+
// amqplib reads the socket through a callback rather than 'data' events, and tells a
|
|
552
|
+
// connection gone silent by its heartbeat
|
|
553
|
+
it('should keep an idle connection', async () => {
|
|
552
554
|
jest.useFakeTimers()
|
|
553
555
|
|
|
554
556
|
await connection.open()
|
|
555
557
|
|
|
556
558
|
const conn = await amqplib.connect.mock.results[0].value
|
|
557
559
|
|
|
558
|
-
await jest.advanceTimersByTimeAsync(
|
|
559
|
-
|
|
560
|
-
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
it('should reconnect after the watchdog destroys a silent connection', async () => {
|
|
564
|
-
jest.useFakeTimers()
|
|
565
|
-
|
|
566
|
-
await connection.open()
|
|
567
|
-
|
|
568
|
-
await jest.advanceTimersByTimeAsync(46_000)
|
|
569
|
-
|
|
570
|
-
// a socket destroyed without an error is a socket amqplib never reports,
|
|
571
|
-
// which leaves the connection silently dead instead of recovering
|
|
572
|
-
expect(amqplib.connect).toHaveBeenCalledTimes(2)
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
it('should not let a replaced socket disarm the watchdog', async () => {
|
|
576
|
-
jest.useFakeTimers()
|
|
577
|
-
|
|
578
|
-
await connection.open()
|
|
579
|
-
|
|
580
|
-
const stale = await amqplib.connect.mock.results[0].value
|
|
581
|
-
|
|
582
|
-
stale.emit('close', new Error('lost'))
|
|
583
|
-
|
|
584
|
-
await jest.advanceTimersByTimeAsync(1)
|
|
585
|
-
|
|
586
|
-
const live = await amqplib.connect.mock.results[1].value
|
|
587
|
-
|
|
588
|
-
await jest.advanceTimersByTimeAsync(30_000)
|
|
589
|
-
|
|
590
|
-
stale.connection.stream.emit('data', Buffer.alloc(0))
|
|
591
|
-
|
|
592
|
-
await jest.advanceTimersByTimeAsync(30_000)
|
|
593
|
-
|
|
594
|
-
expect(live.connection.stream.destroy).toHaveBeenCalled()
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
it('should reset watchdog on socket data', async () => {
|
|
598
|
-
jest.useFakeTimers()
|
|
599
|
-
|
|
600
|
-
await connection.open()
|
|
601
|
-
|
|
602
|
-
const conn = await amqplib.connect.mock.results[0].value
|
|
603
|
-
|
|
604
|
-
await jest.advanceTimersByTimeAsync(40_000)
|
|
605
|
-
|
|
606
|
-
conn.connection.stream.emit('data', Buffer.alloc(0))
|
|
607
|
-
|
|
608
|
-
await jest.advanceTimersByTimeAsync(40_000)
|
|
560
|
+
await jest.advanceTimersByTimeAsync(600_000)
|
|
609
561
|
|
|
610
562
|
expect(conn.connection.stream.destroy).not.toHaveBeenCalled()
|
|
611
|
-
|
|
612
|
-
await jest.advanceTimersByTimeAsync(10_000)
|
|
613
|
-
|
|
614
|
-
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
563
|
+
expect(amqplib.connect).toHaveBeenCalledTimes(1)
|
|
615
564
|
})
|
|
616
565
|
|
|
617
|
-
it('should
|
|
618
|
-
jest.useFakeTimers()
|
|
619
|
-
|
|
620
|
-
const connection = new Connection('amqp://localhost?heartbeat=30')
|
|
621
|
-
|
|
566
|
+
it('should destroy the socket of a connection lost with an error', async () => {
|
|
622
567
|
await connection.open()
|
|
623
568
|
|
|
624
569
|
const conn = await amqplib.connect.mock.results[0].value
|
|
570
|
+
const stream = conn.connection.stream
|
|
625
571
|
|
|
626
|
-
|
|
627
|
-
await jest.advanceTimersByTimeAsync(60_000)
|
|
628
|
-
|
|
629
|
-
expect(conn.connection.stream.destroy).not.toHaveBeenCalled()
|
|
630
|
-
|
|
631
|
-
await jest.advanceTimersByTimeAsync(31_000)
|
|
572
|
+
conn.emit('close', new Error('Heartbeat timeout'))
|
|
632
573
|
|
|
633
|
-
expect(
|
|
574
|
+
expect(stream.destroy).toHaveBeenCalled()
|
|
634
575
|
})
|
|
635
576
|
|
|
636
|
-
it('should
|
|
637
|
-
jest.useFakeTimers()
|
|
638
|
-
|
|
639
|
-
const connection = new Connection('amqp://localhost?heartbeat=0')
|
|
640
|
-
|
|
577
|
+
it('should reconnect once a connection is lost with an error', async () => {
|
|
641
578
|
await connection.open()
|
|
642
579
|
|
|
643
580
|
const conn = await amqplib.connect.mock.results[0].value
|
|
644
581
|
|
|
645
|
-
|
|
646
|
-
await jest.advanceTimersByTimeAsync(600_000)
|
|
582
|
+
conn.emit('close', new Error('Heartbeat timeout'))
|
|
647
583
|
|
|
648
|
-
|
|
584
|
+
await timeout(0)
|
|
585
|
+
|
|
586
|
+
expect(amqplib.connect).toHaveBeenCalledTimes(2)
|
|
649
587
|
})
|
|
650
588
|
})
|
|
651
589
|
|