comq 0.25.1 → 0.26.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/docs/headers.md +16 -1
- package/features/properties.feature +39 -0
- package/features/shards.virtual.feature +89 -0
- package/features/steps/context.js +29 -2
- package/features/steps/hooks.js +1 -0
- package/features/steps/names.js +245 -0
- package/features/steps/networks.js +23 -5
- package/features/steps/rpc.js +14 -2
- package/package.json +1 -1
- package/readme.md +102 -4
- package/source/channel.js +147 -2
- package/source/connect.js +18 -15
- package/source/connection.js +8 -5
- package/source/events.js +7 -0
- package/source/io.js +31 -9
- package/source/shards/channel.js +289 -19
- package/source/shards/connection.js +404 -18
- package/source/shards/expand.js +57 -0
- package/source/shards/index.js +2 -0
- package/source/shards/resolve.js +134 -0
- package/source/shards/watch.js +257 -0
- package/source/singleton.js +3 -2
- package/test/amqplib.mock.js +1 -0
- package/test/assert.test.js +7 -2
- package/test/channel.quiet.test.js +301 -0
- package/test/connect.test.js +49 -8
- package/test/connection.mock.js +3 -0
- package/test/connection.test.js +15 -0
- package/test/io.call.test.js +1 -1
- package/test/io.diagnostics.test.js +1 -1
- package/test/io.reply.test.js +12 -1
- package/test/io.request.test.js +18 -0
- package/test/io.shards.test.js +85 -0
- package/test/shards/channel.test.js +411 -0
- package/test/shards/connection.test.js +372 -17
- package/test/shards/expand.test.js +58 -0
- package/test/shards/resolve.test.js +73 -0
- package/test/shards/watch.test.js +264 -0
- package/test/singleton.test.js +1 -1
- package/types/channel.d.ts +23 -0
- package/types/connection.d.ts +13 -2
- package/types/diagnostic.d.ts +2 -1
- package/types/io.d.ts +17 -1
- package/types/shards.d.ts +58 -0
package/readme.md
CHANGED
|
@@ -18,6 +18,7 @@ for distributed, eventually consistent systems running on Node.js.
|
|
|
18
18
|
- [Poison message handling](#messages)
|
|
19
19
|
- [Connection tolerance](#connection-tolerance) and broker restart resilience
|
|
20
20
|
- [Sharded connection](#sharded-connection) :rocket:
|
|
21
|
+
- [Virtual shards](#virtual-shards) that follow DNS
|
|
21
22
|
- [Singleton connection](#singleton-connection)
|
|
22
23
|
- [Graceful shutdown](#graceful-shutdown)
|
|
23
24
|
|
|
@@ -81,10 +82,10 @@ Event published to a fanout exchange carries none.
|
|
|
81
82
|
|
|
82
83
|
`async IO.reply(queue: string, producer): void`
|
|
83
84
|
|
|
84
|
-
`producer` function's signature is `async? (message: any): any`
|
|
85
|
+
`producer` function's signature is `async? (message: any, properties?): any`
|
|
85
86
|
|
|
86
87
|
Assert a `queue` and start consuming Requests. Received messages are decoded and the resulting
|
|
87
|
-
content is passed to the `producer
|
|
88
|
+
content is passed to the `producer`, followed by the [properties](docs/headers.md) of the Request. The result returned by the `producer` is then encoded and sent
|
|
88
89
|
back to the queue specified in the `replyTo` property of the Request, along with a `correlationId`
|
|
89
90
|
that has the same value as in the Request.
|
|
90
91
|
|
|
@@ -109,8 +110,11 @@ await io.reply('add_numbers', ({ a, b }) => (a + b))
|
|
|
109
110
|
|
|
110
111
|
`async IO.request(queue: string, payload: any, encoding?: string): any`
|
|
111
112
|
|
|
113
|
+
`async IO.request(queue: string, payload: any, properties?): any`
|
|
114
|
+
|
|
112
115
|
Send encoded Request message with `replyTo` and `correlationId` properties set and
|
|
113
|
-
return decoded Reply content.
|
|
116
|
+
return decoded Reply content. A Request may carry [properties](docs/headers.md) of its own, such
|
|
117
|
+
as `headers`. The promise stays pending until the Reply arrives, however long
|
|
114
118
|
that takes: a Request has no timeout and cannot be withdrawn. One no Producer can answer is
|
|
115
119
|
[parked](#parked-messages), and can still be answered while its caller waits.
|
|
116
120
|
|
|
@@ -508,6 +512,89 @@ const io = await connect(shard0, shard1)
|
|
|
508
512
|
await io.close()
|
|
509
513
|
```
|
|
510
514
|
|
|
515
|
+
### Virtual shards
|
|
516
|
+
|
|
517
|
+
The URLs of a sharded connection are *names*, and several of them may stand for one broker. A
|
|
518
|
+
connection is made to each broker rather than to each name: thirty-two names resolving to two
|
|
519
|
+
addresses are two connections.
|
|
520
|
+
|
|
521
|
+
```javascript
|
|
522
|
+
const io = await connect('amqp://rmq0.example.com', 'amqp://rmq1.example.com', /* ... */)
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
A range in a host is as many URLs. Its end is exclusive, so a range that starts at zero names as
|
|
526
|
+
many hosts as its end says, and a start written with leading zeros pads every number to its
|
|
527
|
+
width:
|
|
528
|
+
|
|
529
|
+
```javascript
|
|
530
|
+
await connect('amqp://developer:secret@rmq[0..32].example.com') // rmq0 … rmq31
|
|
531
|
+
await connect('amqp://developer:secret@rmq[00..32].example.com') // rmq00 … rmq31
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
This is what lets the number of brokers change without anyone being reconfigured: the names stay,
|
|
535
|
+
and what they resolve to moves.
|
|
536
|
+
|
|
537
|
+
A broker is told by its address, port, virtual host and credentials. A name with several
|
|
538
|
+
addresses is still one broker, connected through one of them: two connections to one broker
|
|
539
|
+
would compete for the queues that are exclusive to a connection. Different addresses are taken
|
|
540
|
+
for different brokers.
|
|
541
|
+
|
|
542
|
+
#### Following the names
|
|
543
|
+
|
|
544
|
+
The names are resolved again every `interval`. What they resolve to is seldom seen changing
|
|
545
|
+
whole: records are updated one after another, and until their TTL has run out caches answer
|
|
546
|
+
with the old address and the new one in turn. So a change is acted upon only once the answer has
|
|
547
|
+
stayed the same for the `settle` time, and anything else observed meanwhile starts that time
|
|
548
|
+
anew. A name that fails to resolve keeps its address, which is reported as `error`.
|
|
549
|
+
|
|
550
|
+
Then the brokers are brought in line with the names, making before breaking:
|
|
551
|
+
|
|
552
|
+
1. **Join.** A connection is made to each broker that is newly named. Whatever is consumed is
|
|
553
|
+
consumed from it as well, and then it joins the pool. Until every named broker has joined,
|
|
554
|
+
nothing below happens: a name pointing at a broker that is not there yet takes nothing away.
|
|
555
|
+
2. **Retire.** A broker no name stands for is taken out of the pool: nothing new is published to
|
|
556
|
+
it. It goes on being consumed from, and a Reply still goes back through it.
|
|
557
|
+
3. **Leave.** A retired broker is disconnected once, for the whole `linger` time, it has had
|
|
558
|
+
nothing to do: the queues consumed on it are empty, no message is with its consumer or
|
|
559
|
+
waiting to be [retried](#retries), no publication awaits its confirmation, and no Request
|
|
560
|
+
sent through it awaits its Reply.
|
|
561
|
+
|
|
562
|
+
Everyone connected follows the names on their own, and not at the same moment. The `linger` time
|
|
563
|
+
is what covers the difference: what is published to a retired broker by those who have not seen
|
|
564
|
+
the names move yet is consumed by those who have. There is no deadline, since leaving a broker
|
|
565
|
+
that is still published to would strand the messages. A retired broker that is named again is
|
|
566
|
+
back in the pool, and one that stays out of reach for the `linger` time is given up on.
|
|
567
|
+
|
|
568
|
+
A broker that is out of reach is not waited for: what its names resolve to next is taken at
|
|
569
|
+
once, as it is when a single connection is restored by its name.
|
|
570
|
+
|
|
571
|
+
The [guarantees](#what-is-guaranteed) are the ones a sharded connection has. A publication is
|
|
572
|
+
confirmed by whichever broker took it, a consumer does not leave a queue that holds messages,
|
|
573
|
+
and a Request is either answered through the broker it was sent to or re-sent as it is when a
|
|
574
|
+
shard is lost.
|
|
575
|
+
|
|
576
|
+
#### Timing
|
|
577
|
+
|
|
578
|
+
The trailing argument of `connect` takes it next to the [topology settings](#settings), in
|
|
579
|
+
milliseconds:
|
|
580
|
+
|
|
581
|
+
```javascript
|
|
582
|
+
await connect(...urls, { resolution: { interval: 10_000, settle: 60_000, linger: 60_000 } })
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
- `interval`: how often the names are resolved, 10 seconds by default.
|
|
586
|
+
- `settle`: how long an answer is to hold before it is acted upon.
|
|
587
|
+
- `linger`: how long a retired broker is to stay idle before it is left.
|
|
588
|
+
|
|
589
|
+
Unless given, `settle` and `linger` are the longest TTL the names have been answered with, and
|
|
590
|
+
no less than 30 seconds: a TTL bounds how long a cache may answer with what is no longer true,
|
|
591
|
+
and says nothing of records being updated one by one. A name that is not found in DNS itself (one
|
|
592
|
+
from the hosts file, or one completed by the resolver's search list) has no TTL, and makes them
|
|
593
|
+
60 seconds.
|
|
594
|
+
|
|
595
|
+
A single URL is one connection made by its name, as it always was, and nothing here applies to
|
|
596
|
+
it.
|
|
597
|
+
|
|
511
598
|
## Singleton connection
|
|
512
599
|
|
|
513
600
|
`async assert(url: string): IO`
|
|
@@ -821,6 +908,12 @@ Subscribe to one of the diagnostic events:
|
|
|
821
908
|
reported only once every shard has rejected it.
|
|
822
909
|
- `taken`: a Key [`back`](#addressed-requests) claims is held by another connection on this broker, and
|
|
823
910
|
is claimed again. Channel type and the queue name are passed.
|
|
911
|
+
- `join`: a broker the names of the [virtual shards](#virtual-shards) stand for has joined the
|
|
912
|
+
pool, or a retired one is back in it. The shard number and the address are passed.
|
|
913
|
+
- `retire`: no name stands for a broker any longer, and nothing new is published to it. The
|
|
914
|
+
shard number and the address are passed.
|
|
915
|
+
- `leave`: a retired broker has had nothing more to give, and is disconnected. The shard number
|
|
916
|
+
and the address are passed.
|
|
824
917
|
- `pause`: channel is paused. Channel type is passed.
|
|
825
918
|
In the case of a [sharded connection](#sharded-connection), it means that there is no shard left
|
|
826
919
|
to publish to, be it because every one of them has rejected a publish or lost its connection.
|
|
@@ -830,7 +923,9 @@ In the case of a [sharded connection](#sharded-connection), an additional argume
|
|
|
830
923
|
shard number will be passed to listeners.
|
|
831
924
|
This is applicable except for the `pause` and `resume` events,
|
|
832
925
|
which are emitted when the associated channels are paused or resumed across all shards.
|
|
833
|
-
|
|
926
|
+
A shard is a broker, and is numbered in the order the brokers are first named: with a URL per
|
|
927
|
+
broker that is the position of the argument used in the `connect` function call. A broker that
|
|
928
|
+
[joins](#virtual-shards) later takes the next number, and a number is not given twice.
|
|
834
929
|
|
|
835
930
|
[^3]: As the [`connect`](#connect) function returns an instance of `IO` *after* the connection has been
|
|
836
931
|
established, there is no way to capture the initial `open` event.
|
|
@@ -847,6 +942,9 @@ io.diagnose('close', (error, shard) => console.log('AMQP connection closed', { m
|
|
|
847
942
|
io.diagnose('error', (error, shard) => console.log('AMQP connection failed', { message: error.message, shard }))
|
|
848
943
|
io.diagnose('lost', (type, shard) => console.log('AMQP shard lost', { type, shard }))
|
|
849
944
|
io.diagnose('recover', (type, shard) => console.log('AMQP channel recovered', { type, shard }))
|
|
945
|
+
io.diagnose('join', (shard, address) => console.log('AMQP broker joined', { shard, address }))
|
|
946
|
+
io.diagnose('retire', (shard, address) => console.log('AMQP broker retired', { shard, address }))
|
|
947
|
+
io.diagnose('leave', (shard, address) => console.log('AMQP broker left', { shard, address }))
|
|
850
948
|
```
|
|
851
949
|
|
|
852
950
|
# Gratitude
|
package/source/channel.js
CHANGED
|
@@ -52,6 +52,21 @@ class Channel {
|
|
|
52
52
|
/** @type {Set<Promise<void>>} failed messages on their way to a retry or the parking queue */
|
|
53
53
|
#settling = new Set()
|
|
54
54
|
|
|
55
|
+
/** @type {Set<string>} the queues being consumed */
|
|
56
|
+
#queues = new Set()
|
|
57
|
+
|
|
58
|
+
/** @type {number} how many deliveries are with their consumers */
|
|
59
|
+
#handling = 0
|
|
60
|
+
|
|
61
|
+
/** @type {Promex | null} resolved once no delivery is with its consumer */
|
|
62
|
+
#rest = null
|
|
63
|
+
|
|
64
|
+
/** @type {number} when a message was last published, or a delivery was last done with */
|
|
65
|
+
#active = 0
|
|
66
|
+
|
|
67
|
+
/** @type {number} when the last of the messages waiting to be retried is delivered again */
|
|
68
|
+
#awaited = 0
|
|
69
|
+
|
|
55
70
|
#diagnostics = emitter.create()
|
|
56
71
|
|
|
57
72
|
#closed = false
|
|
@@ -87,6 +102,7 @@ class Channel {
|
|
|
87
102
|
|
|
88
103
|
// a fresh channel has bound nothing
|
|
89
104
|
this.#held.clear()
|
|
105
|
+
this.#queues.clear()
|
|
90
106
|
|
|
91
107
|
await this.#channel.prefetch(this.#topology.prefetch)
|
|
92
108
|
|
|
@@ -267,6 +283,44 @@ class Channel {
|
|
|
267
283
|
return this.#closed
|
|
268
284
|
}
|
|
269
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Resolves once the deliveries that are with their consumers are done with, and so is what
|
|
288
|
+
* was published. It is waited for between sealing and closing by whoever means to lose none
|
|
289
|
+
* of them: a channel that is closed takes back what it has delivered.
|
|
290
|
+
*
|
|
291
|
+
* @return {Promise<void>}
|
|
292
|
+
*/
|
|
293
|
+
async settled () {
|
|
294
|
+
while (this.#handling > 0) await (this.#rest ??= new Promex())
|
|
295
|
+
|
|
296
|
+
await Promise.allSettled([...this.#confirmations, ...this.#settling])
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Since when the channel has had nothing to do, which is now while it has: a delivery with
|
|
301
|
+
* its consumer, a publication awaiting its confirmation, a failed message that is yet to come
|
|
302
|
+
* back, or a message in a queue it consumes. It is what a broker that is being left is
|
|
303
|
+
* asked, since whatever is left behind on it is lost to this connection.
|
|
304
|
+
*
|
|
305
|
+
* @return {Promise<number>}
|
|
306
|
+
*/
|
|
307
|
+
async quiet () {
|
|
308
|
+
const now = Date.now()
|
|
309
|
+
|
|
310
|
+
const busy = this.#handling > 0 || this.#confirmations.size > 0 ||
|
|
311
|
+
this.#settling.size > 0 || this.#paused !== null || this.#awaited > now
|
|
312
|
+
|
|
313
|
+
if (busy) return now
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
if (await this.#depth() > 0) return now
|
|
317
|
+
} catch {
|
|
318
|
+
return now // what cannot be told is not taken for nothing
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return Math.max(this.#active, this.#awaited)
|
|
322
|
+
}
|
|
323
|
+
|
|
270
324
|
async recover (connection) {
|
|
271
325
|
this.#connection = connection
|
|
272
326
|
|
|
@@ -372,6 +426,45 @@ class Channel {
|
|
|
372
426
|
return true
|
|
373
427
|
}
|
|
374
428
|
|
|
429
|
+
/**
|
|
430
|
+
* @return {Promise<number>} how many messages wait in the queues being consumed
|
|
431
|
+
*/
|
|
432
|
+
async #depth () {
|
|
433
|
+
let depth = 0
|
|
434
|
+
|
|
435
|
+
for (const queue of this.#queues) depth += await this.#count(queue)
|
|
436
|
+
|
|
437
|
+
return depth
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Asked on a channel of its own, for the reason `#declare` has one: a queue that is gone is
|
|
442
|
+
* answered by closing the channel that asked.
|
|
443
|
+
*
|
|
444
|
+
* @param {string} queue
|
|
445
|
+
* @return {Promise<number>}
|
|
446
|
+
*/
|
|
447
|
+
async #count (queue) {
|
|
448
|
+
const probe = await this.#connection.createChannel()
|
|
449
|
+
|
|
450
|
+
/** @type {Error & { code?: number } | undefined} */
|
|
451
|
+
let refusal
|
|
452
|
+
|
|
453
|
+
probe.on('error', (error) => { refusal = error })
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const { messageCount } = await probe.checkQueue(queue)
|
|
457
|
+
|
|
458
|
+
await probe.close().catch(noop)
|
|
459
|
+
|
|
460
|
+
return messageCount
|
|
461
|
+
} catch (exception) {
|
|
462
|
+
if (refusal?.code === NOT_FOUND) return 0
|
|
463
|
+
|
|
464
|
+
throw refusal ?? exception
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
375
468
|
// region initializers
|
|
376
469
|
|
|
377
470
|
/**
|
|
@@ -532,6 +625,8 @@ class Channel {
|
|
|
532
625
|
|
|
533
626
|
options = Object.assign({ persistent: this.#topology.persistent }, options)
|
|
534
627
|
|
|
628
|
+
this.#active = Date.now()
|
|
629
|
+
|
|
535
630
|
const confirmation = this.#topology.confirms ? this.#confirmation() : undefined
|
|
536
631
|
const resume = this.#channel.publish(exchange, queue, buffer, options, confirmation?.callback)
|
|
537
632
|
|
|
@@ -570,16 +665,58 @@ class Channel {
|
|
|
570
665
|
/** @type {comq.amqp.options.Consume} */
|
|
571
666
|
const options = {}
|
|
572
667
|
|
|
573
|
-
if (this.#topology.acknowledgments)
|
|
574
|
-
|
|
668
|
+
if (this.#topology.acknowledgments) {
|
|
669
|
+
consumer = this.#getAcknowledgingConsumer(queue, consumer)
|
|
670
|
+
} else {
|
|
671
|
+
consumer = this.#attended(consumer)
|
|
672
|
+
options.noAck = true
|
|
673
|
+
}
|
|
575
674
|
|
|
576
675
|
const response = await this.#channel.consume(queue, consumer, options)
|
|
577
676
|
|
|
578
677
|
this.#tags.push(response.consumerTag)
|
|
678
|
+
this.#queues.add(queue)
|
|
579
679
|
|
|
580
680
|
return response.consumerTag
|
|
581
681
|
})
|
|
582
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Counts the deliveries that are with a consumer nothing is acknowledged for, without a
|
|
685
|
+
* promise of its own for the ones that are done with as they arrive.
|
|
686
|
+
*
|
|
687
|
+
* @param {comq.channels.Consumer} consumer
|
|
688
|
+
* @returns {comq.channels.Consumer}
|
|
689
|
+
*/
|
|
690
|
+
#attended = (consumer) =>
|
|
691
|
+
(message) => {
|
|
692
|
+
this.#handling++
|
|
693
|
+
|
|
694
|
+
let result
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
result = consumer(message)
|
|
698
|
+
} catch (exception) {
|
|
699
|
+
this.#attendedTo()
|
|
700
|
+
|
|
701
|
+
throw exception
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (typeof result?.then === 'function') result.then(this.#attendedTo, this.#attendedTo)
|
|
705
|
+
else this.#attendedTo()
|
|
706
|
+
|
|
707
|
+
return result
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
#attendedTo = () => {
|
|
711
|
+
this.#handling--
|
|
712
|
+
this.#active = Date.now()
|
|
713
|
+
|
|
714
|
+
if (this.#handling === 0 && this.#rest !== null) {
|
|
715
|
+
this.#rest.resolve()
|
|
716
|
+
this.#rest = null
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
583
720
|
/**
|
|
584
721
|
* @param {string} queue the queue being consumed
|
|
585
722
|
* @param {comq.channels.Consumer} consumer
|
|
@@ -587,6 +724,8 @@ class Channel {
|
|
|
587
724
|
*/
|
|
588
725
|
#getAcknowledgingConsumer = (queue, consumer) =>
|
|
589
726
|
async (message) => {
|
|
727
|
+
this.#handling++
|
|
728
|
+
|
|
590
729
|
try {
|
|
591
730
|
await consumer(message)
|
|
592
731
|
|
|
@@ -601,6 +740,8 @@ class Channel {
|
|
|
601
740
|
await settling
|
|
602
741
|
|
|
603
742
|
this.#settling.delete(settling)
|
|
743
|
+
} finally {
|
|
744
|
+
this.#attendedTo()
|
|
604
745
|
}
|
|
605
746
|
}
|
|
606
747
|
|
|
@@ -647,6 +788,7 @@ class Channel {
|
|
|
647
788
|
await this.#publish(this.#retryQueueOf(attempt), queue, message.content, properties)
|
|
648
789
|
|
|
649
790
|
this.#channel.ack(message)
|
|
791
|
+
this.#awaited = Math.max(this.#awaited, Date.now() + this.#delays[attempt - 1])
|
|
650
792
|
|
|
651
793
|
// the attempt that just failed, rather than the one it is about to get
|
|
652
794
|
this.#diagnostics.emit('retry', message, exception, attempt)
|
|
@@ -781,6 +923,9 @@ const INTERRUPTION = /** @type {Error} */ Symbol('internal interruption')
|
|
|
781
923
|
// the AMQP reply code a broker closes a channel with when another connection holds the queue
|
|
782
924
|
const RESOURCE_LOCKED = 405
|
|
783
925
|
|
|
926
|
+
// and the one it closes a channel with when asked about a queue it does not have
|
|
927
|
+
const NOT_FOUND = 404
|
|
928
|
+
|
|
784
929
|
const RETRY_PREFIX = 'comq.retry.'
|
|
785
930
|
|
|
786
931
|
/** Where every message that has run out of attempts is kept, whatever it was consumed from. */
|
package/source/connect.js
CHANGED
|
@@ -16,29 +16,29 @@ const assert = async (...args) => {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* The urls are variadic, so the
|
|
20
|
-
* that is not a url.
|
|
19
|
+
* The urls are variadic, so the options are the trailing argument when there is one
|
|
20
|
+
* that is not a url: the topology overrides, and how the names are followed.
|
|
21
21
|
*
|
|
22
|
-
* @param {(string | comq.
|
|
23
|
-
* @return {[string[], comq.topology.Overrides]}
|
|
22
|
+
* @param {(string | comq.Options)[]} args
|
|
23
|
+
* @return {[string[], comq.topology.Overrides, comq.shards.Timing | undefined]}
|
|
24
24
|
*/
|
|
25
25
|
const split = (args) => {
|
|
26
26
|
const last = args[args.length - 1]
|
|
27
27
|
|
|
28
28
|
const urls = /** @type {string[]} */ (typeof last === 'object' ? args.slice(0, -1) : args)
|
|
29
|
-
const overrides = /** @type {comq.
|
|
29
|
+
const { resolution, ...overrides } = /** @type {comq.Options} */ (typeof last === 'object' ? last : {})
|
|
30
30
|
|
|
31
|
-
return [urls, overrides]
|
|
31
|
+
return [urls.flatMap(shards.expand), overrides, resolution]
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* @param {(string | comq.
|
|
36
|
-
* @param {new (url: string, overrides: comq.topology.Overrides) => comq.Connection} ConnectionClass
|
|
35
|
+
* @param {(string | comq.Options)[]} args
|
|
36
|
+
* @param {new (url: string, overrides: comq.topology.Overrides, servername?: string) => comq.Connection} ConnectionClass
|
|
37
37
|
* @return {Promise<IO>}
|
|
38
38
|
*/
|
|
39
39
|
const create = async (args, ConnectionClass) => {
|
|
40
|
-
const [urls, overrides] = split(args)
|
|
41
|
-
const connection = connectionOf(urls, overrides, ConnectionClass)
|
|
40
|
+
const [urls, overrides, resolution] = split(args)
|
|
41
|
+
const connection = connectionOf(urls, overrides, resolution, ConnectionClass)
|
|
42
42
|
|
|
43
43
|
await connection.open()
|
|
44
44
|
|
|
@@ -46,17 +46,20 @@ const create = async (args, ConnectionClass) => {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
+
* Several URLs are the names of the shards, which may be fewer than the names: a connection
|
|
50
|
+
* is made to each broker they stand for. A single URL is connected by its name, as it is.
|
|
51
|
+
*
|
|
49
52
|
* @param {string[]} urls
|
|
50
53
|
* @param {comq.topology.Overrides} overrides
|
|
54
|
+
* @param {comq.shards.Timing | undefined} resolution
|
|
51
55
|
* @param {Function} ConnectionClass
|
|
52
56
|
* @return {comq.Connection}
|
|
53
57
|
*/
|
|
54
|
-
const connectionOf = (urls, overrides, ConnectionClass) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (connections.length === 1) return connections[0]
|
|
58
|
+
const connectionOf = (urls, overrides, resolution, ConnectionClass) => {
|
|
59
|
+
if (urls.length === 1) return new ConnectionClass(urls[0], overrides)
|
|
58
60
|
|
|
59
|
-
return new shards.Connection(
|
|
61
|
+
return new shards.Connection(urls,
|
|
62
|
+
(url, servername) => new ConnectionClass(url, overrides, servername), resolution)
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
exports.connect = connect
|
package/source/connection.js
CHANGED
|
@@ -38,18 +38,21 @@ class Connection {
|
|
|
38
38
|
/** @type {comq.topology.Overrides} */
|
|
39
39
|
#overrides
|
|
40
40
|
|
|
41
|
+
/** @type {object} */
|
|
42
|
+
#options
|
|
43
|
+
|
|
41
44
|
#diagnostics = emitter.create()
|
|
42
45
|
|
|
43
|
-
/**
|
|
44
|
-
* @param {string} url
|
|
45
|
-
*/
|
|
46
46
|
/**
|
|
47
47
|
* @param {string} url
|
|
48
48
|
* @param {comq.topology.Overrides} [overrides] per channel type, merged over the presets
|
|
49
|
+
* @param {string} [servername] the name TLS verifies the broker by, when the URL has its
|
|
50
|
+
* address in place of the name
|
|
49
51
|
*/
|
|
50
|
-
constructor (url, overrides = {}) {
|
|
52
|
+
constructor (url, overrides = {}, servername = undefined) {
|
|
51
53
|
this.#url = heartbeaten(url)
|
|
52
54
|
this.#overrides = overrides
|
|
55
|
+
this.#options = servername === undefined ? SOCKET_OPTIONS : { ...SOCKET_OPTIONS, servername }
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
get connected () {
|
|
@@ -255,7 +258,7 @@ class Connection {
|
|
|
255
258
|
* @return {Promise<comq.amqp.Connection>}
|
|
256
259
|
*/
|
|
257
260
|
async #connect () {
|
|
258
|
-
const connecting = amqp.connect(this.#url,
|
|
261
|
+
const connecting = amqp.connect(this.#url, this.#options)
|
|
259
262
|
|
|
260
263
|
let timer
|
|
261
264
|
|
package/source/events.js
CHANGED
|
@@ -3,5 +3,12 @@
|
|
|
3
3
|
/** @type {comq.diagnostics.Event[]} */
|
|
4
4
|
exports.connection = ['open', 'close', 'error', 'reconnect', 'exhausted']
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* What a sharded connection tells of the brokers its names stand for.
|
|
8
|
+
*
|
|
9
|
+
* @type {comq.diagnostics.Event[]}
|
|
10
|
+
*/
|
|
11
|
+
exports.shards = ['join', 'retire', 'leave']
|
|
12
|
+
|
|
6
13
|
/** @type {comq.diagnostics.Event[]} */
|
|
7
14
|
exports.channel = ['flow', 'drain', 'recover', 'discard', 'retry', 'pause', 'resume', 'return', 'lost', 'remove', 'taken']
|
package/source/io.js
CHANGED
|
@@ -63,7 +63,7 @@ class IO {
|
|
|
63
63
|
constructor (connection) {
|
|
64
64
|
this.#connection = connection
|
|
65
65
|
|
|
66
|
-
for (const event of events.connection) {
|
|
66
|
+
for (const event of [...events.connection, ...events.shards]) {
|
|
67
67
|
const forwarder = (...args) => this.#diagnostics.emit(event, ...args)
|
|
68
68
|
|
|
69
69
|
this.#connection.diagnose(event, forwarder)
|
|
@@ -89,16 +89,22 @@ class IO {
|
|
|
89
89
|
*
|
|
90
90
|
* @param {string} queue
|
|
91
91
|
* @param {any | Readable} payload
|
|
92
|
-
* @param {comq.Encoding} [encoding]
|
|
92
|
+
* @param {comq.Encoding | comq.amqp.Properties} [encoding]
|
|
93
93
|
* @returns {Promise<any | Readable>}
|
|
94
94
|
*/
|
|
95
95
|
async request (queue, payload, encoding) {
|
|
96
|
+
if (typeof encoding !== 'object' || encoding === null) {
|
|
97
|
+
return await this.#request(queue, payload, { encoding })
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
// a caller of the version that took a timeout here would otherwise wait on in silence
|
|
97
|
-
if (
|
|
98
|
-
throw new TypeError('A Request takes an encoding and waits for its Reply')
|
|
101
|
+
if ('timeout' in encoding || 'signal' in encoding) {
|
|
102
|
+
throw new TypeError('A Request takes an encoding or properties and waits for its Reply')
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
|
|
105
|
+
const { contentType, ...properties } = encoding
|
|
106
|
+
|
|
107
|
+
return await this.#request(queue, payload, { encoding: contentType, properties })
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
/**
|
|
@@ -294,6 +300,9 @@ class IO {
|
|
|
294
300
|
|
|
295
301
|
this.#setupRetransmission()
|
|
296
302
|
|
|
303
|
+
// a shard that is being retired is not left while it owes a Reply
|
|
304
|
+
this.#requests.occupy?.((index) => this.#awaiting(index))
|
|
305
|
+
|
|
297
306
|
// on a sharded connection, only once every shard has returned it
|
|
298
307
|
this.#requests.diagnose('return', this.#returned)
|
|
299
308
|
}
|
|
@@ -393,7 +402,7 @@ class IO {
|
|
|
393
402
|
*/
|
|
394
403
|
async (request) => {
|
|
395
404
|
const payload = decode(request)
|
|
396
|
-
const reply = await produce(producer, payload)
|
|
405
|
+
const reply = await produce(producer, payload, request.properties)
|
|
397
406
|
|
|
398
407
|
if (request.properties.replyTo === undefined) return
|
|
399
408
|
|
|
@@ -455,7 +464,9 @@ class IO {
|
|
|
455
464
|
|
|
456
465
|
const [buffer, contentType] = this.#encode(payload, terms.encoding)
|
|
457
466
|
const request = this.#createRequest(contentType, signal)
|
|
458
|
-
|
|
467
|
+
|
|
468
|
+
// what makes it a Request is comq's to set, whatever it was sent with
|
|
469
|
+
const properties = { ...terms.properties, ...request.properties }
|
|
459
470
|
|
|
460
471
|
if (expires !== undefined) {
|
|
461
472
|
const left = Math.ceil(expires - Date.now())
|
|
@@ -745,6 +756,16 @@ class IO {
|
|
|
745
756
|
}
|
|
746
757
|
}
|
|
747
758
|
|
|
759
|
+
/**
|
|
760
|
+
* @param {number} index
|
|
761
|
+
* @return {boolean} whether a Request sent through a shard is yet to be answered
|
|
762
|
+
*/
|
|
763
|
+
#awaiting (index) {
|
|
764
|
+
for (const request of this.#pendingReplies.values()) if (request.shard === index) return true
|
|
765
|
+
|
|
766
|
+
return false
|
|
767
|
+
}
|
|
768
|
+
|
|
748
769
|
/**
|
|
749
770
|
* @param {comq.Request} request
|
|
750
771
|
* @param {Promex} reply
|
|
@@ -856,10 +877,11 @@ async function abortable (promise, signal) {
|
|
|
856
877
|
*
|
|
857
878
|
* @param {comq.Producer} producer
|
|
858
879
|
* @param {any} payload
|
|
880
|
+
* @param {comq.amqp.Properties} properties
|
|
859
881
|
*/
|
|
860
|
-
async function produce (producer, payload) {
|
|
882
|
+
async function produce (producer, payload, properties) {
|
|
861
883
|
try {
|
|
862
|
-
return await producer(payload)
|
|
884
|
+
return await producer(payload, properties)
|
|
863
885
|
} catch (exception) {
|
|
864
886
|
if (verdictOf(exception) !== PARK) throw exception
|
|
865
887
|
|