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/source/shards/channel.js
CHANGED
|
@@ -11,12 +11,40 @@ const emitter = require('../emitter')
|
|
|
11
11
|
class Channel {
|
|
12
12
|
sharded = true
|
|
13
13
|
|
|
14
|
-
/** @type {comq.Connection
|
|
14
|
+
/** @type {Map<number, comq.Connection>} the connection of each shard, by its number */
|
|
15
15
|
#connections
|
|
16
16
|
|
|
17
|
+
/** @type {Map<number, comq.Channel>} the channel of each shard, once it has one */
|
|
18
|
+
#shards = new Map()
|
|
19
|
+
|
|
17
20
|
/** @type {Set<comq.Channel>} */
|
|
18
21
|
#channels = new Set()
|
|
19
22
|
|
|
23
|
+
/** @type {Set<comq.Channel>} the channels of the shards that are joining */
|
|
24
|
+
#joining = new Set()
|
|
25
|
+
|
|
26
|
+
/** @type {WeakSet<comq.Channel>} the channels of the shards that were let go */
|
|
27
|
+
#gone = new WeakSet()
|
|
28
|
+
|
|
29
|
+
/** @type {Set<number>} the shards nothing new is published to */
|
|
30
|
+
#retiring = new Set()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* What has been consumed, to be consumed from a shard that joins later as well.
|
|
34
|
+
*
|
|
35
|
+
* @type {{ apply: (channel: comq.Channel) => Promise<any>, awaited: boolean }[]}
|
|
36
|
+
*/
|
|
37
|
+
#consumptions = []
|
|
38
|
+
|
|
39
|
+
/** @type {Map<number, [comq.Connection, Function, Function]>} */
|
|
40
|
+
#watchers = new Map()
|
|
41
|
+
|
|
42
|
+
/** @type {((index: number) => boolean) | undefined} */
|
|
43
|
+
#occupied
|
|
44
|
+
|
|
45
|
+
/** @type {boolean} */
|
|
46
|
+
#shut = false
|
|
47
|
+
|
|
20
48
|
/** @type {comq.Channel[]} */
|
|
21
49
|
#pool
|
|
22
50
|
|
|
@@ -43,34 +71,179 @@ class Channel {
|
|
|
43
71
|
#diagnostics = emitter.create()
|
|
44
72
|
|
|
45
73
|
/**
|
|
46
|
-
* @param {comq.Connection[]} connections
|
|
74
|
+
* @param {comq.Connection[] | Map<number, comq.Connection>} connections of the shards, which
|
|
75
|
+
* are numbered by their position unless they are given by number
|
|
47
76
|
* @param {comq.topology.type} type
|
|
48
77
|
*/
|
|
49
78
|
constructor (connections, type) {
|
|
50
|
-
this.#connections = connections
|
|
79
|
+
this.#connections = new Map(Array.isArray(connections) ? connections.entries() : connections)
|
|
51
80
|
this.#type = type
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
async create () {
|
|
55
|
-
const promises =
|
|
84
|
+
const promises = Array.from(this.#connections,
|
|
85
|
+
([index, connection]) => this.#create(connection, index))
|
|
56
86
|
|
|
57
87
|
await Promise.any(promises)
|
|
58
88
|
}
|
|
59
89
|
|
|
90
|
+
/**
|
|
91
|
+
* @param {number} index
|
|
92
|
+
* @return {boolean} whether the shard is one of this channel's
|
|
93
|
+
*/
|
|
94
|
+
has (index) {
|
|
95
|
+
return this.#connections.has(index)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Takes a shard in. Whatever has been consumed is consumed from it as well, and only then is
|
|
100
|
+
* anything published through it: a Reply comes back through the shard its Request went, where
|
|
101
|
+
* somebody must be waiting for it by then.
|
|
102
|
+
*
|
|
103
|
+
* @param {comq.Connection} connection
|
|
104
|
+
* @param {number} index
|
|
105
|
+
* @return {Promise<void>}
|
|
106
|
+
*/
|
|
107
|
+
async join (connection, index) {
|
|
108
|
+
if (this.#shut || this.#connections.has(index)) return
|
|
109
|
+
|
|
110
|
+
this.#connections.set(index, connection)
|
|
111
|
+
this.#watch(connection, index)
|
|
112
|
+
|
|
113
|
+
/** @type {comq.Channel} */
|
|
114
|
+
let channel
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
channel = await connection.createChannel(this.#type, index)
|
|
118
|
+
} catch (exception) {
|
|
119
|
+
// not its shard, so that it may be joined again
|
|
120
|
+
if (!this.#abandoned(connection, index)) await this.leave(index)
|
|
121
|
+
|
|
122
|
+
throw exception
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (this.#abandoned(connection, index)) return await channel.close()
|
|
126
|
+
|
|
127
|
+
this.#joining.add(channel)
|
|
128
|
+
this.#attach(channel, index)
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// what is consumed while this is under way is consumed here too, as the list grows
|
|
132
|
+
for (let i = 0; i < this.#consumptions.length; i++) {
|
|
133
|
+
const { apply, awaited } = this.#consumptions[i]
|
|
134
|
+
const applied = apply(channel)
|
|
135
|
+
|
|
136
|
+
if (awaited) await applied
|
|
137
|
+
else applied.catch(noop)
|
|
138
|
+
}
|
|
139
|
+
} catch (exception) {
|
|
140
|
+
// a shard that cannot be consumed from is not one to publish through: whoever took the
|
|
141
|
+
// shard for joined would let go of the one that is consumed from
|
|
142
|
+
if (!this.#abandoned(connection, index)) await this.leave(index)
|
|
143
|
+
|
|
144
|
+
throw exception
|
|
145
|
+
} finally {
|
|
146
|
+
this.#joining.delete(channel)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// sealed or closed with the rest while it was joining, or let go
|
|
150
|
+
if (this.#abandoned(connection, index)) return await channel.close()
|
|
151
|
+
|
|
152
|
+
this.#add(channel)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Stops publishing through a shard, which goes on being consumed from. A Reply still goes
|
|
157
|
+
* back through it: see `fire`.
|
|
158
|
+
*
|
|
159
|
+
* @param {number} index
|
|
160
|
+
*/
|
|
161
|
+
retire (index) {
|
|
162
|
+
this.#retiring.add(index)
|
|
163
|
+
this.#update()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {number} index
|
|
168
|
+
*/
|
|
169
|
+
restore (index) {
|
|
170
|
+
this.#retiring.delete(index)
|
|
171
|
+
this.#update()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {number} index
|
|
176
|
+
* @return {Promise<number>} since when the shard has had nothing to do, which is now while
|
|
177
|
+
* it has
|
|
178
|
+
*/
|
|
179
|
+
async quiet (index) {
|
|
180
|
+
if (this.#occupied?.(index) === true) return Date.now()
|
|
181
|
+
|
|
182
|
+
const channel = this.#shards.get(index)
|
|
183
|
+
|
|
184
|
+
return channel === undefined ? 0 : await channel.quiet()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Whoever publishes through this channel may be waiting for something a shard owes it, which
|
|
189
|
+
* the channel itself cannot tell.
|
|
190
|
+
*
|
|
191
|
+
* @param {(index: number) => boolean} occupied
|
|
192
|
+
*/
|
|
193
|
+
occupy (occupied) {
|
|
194
|
+
this.#occupied = occupied
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Lets a shard go, with the channel it had.
|
|
199
|
+
*
|
|
200
|
+
* @param {number} index
|
|
201
|
+
* @return {Promise<void>}
|
|
202
|
+
*/
|
|
203
|
+
async leave (index) {
|
|
204
|
+
const channel = this.#shards.get(index)
|
|
205
|
+
|
|
206
|
+
// what it has delivered is done with while it is still the way back for the answers
|
|
207
|
+
if (channel !== undefined) {
|
|
208
|
+
await channel.seal().catch(noop)
|
|
209
|
+
await channel.settled()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.#connections.delete(index)
|
|
213
|
+
this.#shards.delete(index)
|
|
214
|
+
this.#retiring.delete(index)
|
|
215
|
+
this.#unwatch(index)
|
|
216
|
+
|
|
217
|
+
if (channel === undefined) return
|
|
218
|
+
|
|
219
|
+
this.#gone.add(channel)
|
|
220
|
+
this.#channels.delete(channel)
|
|
221
|
+
this.#bench.delete(channel)
|
|
222
|
+
this.#update()
|
|
223
|
+
|
|
224
|
+
await channel.close()
|
|
225
|
+
}
|
|
226
|
+
|
|
60
227
|
/**
|
|
61
228
|
* A message is told which shard it arrived on, so that whatever answers it can go back the same
|
|
62
229
|
* way. See `fire`.
|
|
63
230
|
*/
|
|
64
231
|
async consume (queue, consumer) {
|
|
65
|
-
return await this.#
|
|
232
|
+
return await this.#consumption(
|
|
233
|
+
(channel) => channel.consume(queue, arrival(consumer, channel)),
|
|
234
|
+
(apply) => this.#every(apply))
|
|
66
235
|
}
|
|
67
236
|
|
|
68
237
|
async subscribe (queue, group, consumer) {
|
|
69
|
-
await this.#
|
|
238
|
+
await this.#consumption(
|
|
239
|
+
(channel) => channel.subscribe(queue, group, consumer),
|
|
240
|
+
(apply) => this.#every(apply))
|
|
70
241
|
}
|
|
71
242
|
|
|
72
243
|
async bound (exchange, queue, key, consumer) {
|
|
73
|
-
await this.#
|
|
244
|
+
await this.#consumption(
|
|
245
|
+
(channel) => channel.bound(exchange, queue, key, consumer),
|
|
246
|
+
(apply) => this.#every(apply))
|
|
74
247
|
}
|
|
75
248
|
|
|
76
249
|
/**
|
|
@@ -78,7 +251,10 @@ class Channel {
|
|
|
78
251
|
* claiming it, and holds it as well once it is let go.
|
|
79
252
|
*/
|
|
80
253
|
async held (exchange, queue, key, consumer) {
|
|
81
|
-
|
|
254
|
+
// a shard that joins is not waited for either: the key may be held there by another
|
|
255
|
+
await this.#consumption(
|
|
256
|
+
(channel) => channel.held(exchange, queue, key, arrival(consumer, channel)),
|
|
257
|
+
(apply) => Promise.any(this.#apply(apply)), false)
|
|
82
258
|
}
|
|
83
259
|
|
|
84
260
|
/**
|
|
@@ -126,7 +302,11 @@ class Channel {
|
|
|
126
302
|
}
|
|
127
303
|
|
|
128
304
|
async close () {
|
|
305
|
+
this.#shut = true
|
|
306
|
+
|
|
129
307
|
await this.#all((channel) => channel.close())
|
|
308
|
+
|
|
309
|
+
for (const index of this.#watchers.keys()) this.#unwatch(index)
|
|
130
310
|
}
|
|
131
311
|
|
|
132
312
|
get closed () {
|
|
@@ -134,6 +314,9 @@ class Channel {
|
|
|
134
314
|
}
|
|
135
315
|
|
|
136
316
|
async seal () {
|
|
317
|
+
// a sealed channel is not going to consume again, from a shard that joins either
|
|
318
|
+
this.#consumptions = []
|
|
319
|
+
|
|
137
320
|
await this.#all((channel) => channel.seal())
|
|
138
321
|
}
|
|
139
322
|
|
|
@@ -156,7 +339,18 @@ class Channel {
|
|
|
156
339
|
const pending = connection.createChannel(this.#type, index)
|
|
157
340
|
const channel = await this.#pend(pending, index)
|
|
158
341
|
|
|
342
|
+
if (this.#abandoned(connection, index)) return await channel.close()
|
|
343
|
+
|
|
344
|
+
this.#attach(channel, index)
|
|
159
345
|
this.#add(channel)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {comq.Channel} channel
|
|
350
|
+
* @param {number} index
|
|
351
|
+
*/
|
|
352
|
+
#attach (channel, index) {
|
|
353
|
+
this.#shards.set(index, channel)
|
|
160
354
|
this.#pipe(channel)
|
|
161
355
|
|
|
162
356
|
channel.diagnose('flow', () => this.#remove((channel)))
|
|
@@ -164,6 +358,42 @@ class Channel {
|
|
|
164
358
|
channel.diagnose('recover', () => this.#recover(channel))
|
|
165
359
|
}
|
|
166
360
|
|
|
361
|
+
/**
|
|
362
|
+
* @param {comq.Connection} connection
|
|
363
|
+
* @param {number} index
|
|
364
|
+
* @return {boolean} whether the shard was let go, or the channel closed, while a channel was
|
|
365
|
+
* being made for it
|
|
366
|
+
*/
|
|
367
|
+
#abandoned (connection, index) {
|
|
368
|
+
return this.#shut || this.#connections.get(index) !== connection
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Consumes from the shards there are, and from the ones that join later. What no shard could
|
|
373
|
+
* consume is not consumed from those either: it has failed for whoever asked for it.
|
|
374
|
+
*
|
|
375
|
+
* @param {(channel: comq.Channel) => Promise<any>} apply
|
|
376
|
+
* @param {(apply: (channel: comq.Channel) => Promise<any>) => Promise<any>} consume
|
|
377
|
+
* @param {boolean} [awaited] whether a shard that joins waits for it before it is published to
|
|
378
|
+
* @return {Promise<any>}
|
|
379
|
+
*/
|
|
380
|
+
async #consumption (apply, consume, awaited = true) {
|
|
381
|
+
const consumption = { apply, awaited }
|
|
382
|
+
|
|
383
|
+
// before it is applied, for a shard that joins meanwhile
|
|
384
|
+
this.#consumptions.push(consumption)
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
return await consume(apply)
|
|
388
|
+
} catch (exception) {
|
|
389
|
+
const index = this.#consumptions.indexOf(consumption)
|
|
390
|
+
|
|
391
|
+
if (index !== -1) this.#consumptions.splice(index, 1)
|
|
392
|
+
|
|
393
|
+
throw exception
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
167
397
|
/**
|
|
168
398
|
* Tracks whether a shard is reachable, so that subscribing does not wait for
|
|
169
399
|
* one that is not. The pool itself is left alone: a channel is only benched
|
|
@@ -181,7 +411,7 @@ class Channel {
|
|
|
181
411
|
|
|
182
412
|
if (connection.connected === false) this.#down[index].resolve()
|
|
183
413
|
|
|
184
|
-
|
|
414
|
+
const closed = (error) => {
|
|
185
415
|
this.#alive[index] = false
|
|
186
416
|
this.#down[index].resolve()
|
|
187
417
|
|
|
@@ -191,14 +421,41 @@ class Channel {
|
|
|
191
421
|
if (error !== undefined && connection.closed !== true) {
|
|
192
422
|
this.#diagnostics.emit(LOST, index)
|
|
193
423
|
}
|
|
194
|
-
}
|
|
424
|
+
}
|
|
195
425
|
|
|
196
|
-
|
|
426
|
+
const opened = () => {
|
|
197
427
|
this.#alive[index] = true
|
|
198
428
|
this.#down[index] = new Promex()
|
|
199
429
|
|
|
200
430
|
this.#update()
|
|
201
|
-
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
connection.diagnose('close', closed)
|
|
434
|
+
connection.diagnose('open', opened)
|
|
435
|
+
|
|
436
|
+
this.#watchers.set(index, [connection, closed, opened])
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* @param {number} index
|
|
441
|
+
*/
|
|
442
|
+
#unwatch (index) {
|
|
443
|
+
const watcher = this.#watchers.get(index)
|
|
444
|
+
|
|
445
|
+
if (watcher === undefined) return
|
|
446
|
+
|
|
447
|
+
const [connection, closed, opened] = watcher
|
|
448
|
+
|
|
449
|
+
connection.forget('close', closed)
|
|
450
|
+
connection.forget('open', opened)
|
|
451
|
+
|
|
452
|
+
this.#watchers.delete(index)
|
|
453
|
+
|
|
454
|
+
// whatever waits for the shard is not to wait for it any longer
|
|
455
|
+
this.#down[index].resolve()
|
|
456
|
+
|
|
457
|
+
delete this.#down[index]
|
|
458
|
+
delete this.#alive[index]
|
|
202
459
|
}
|
|
203
460
|
|
|
204
461
|
/**
|
|
@@ -248,7 +505,7 @@ class Channel {
|
|
|
248
505
|
// a failed message waits on an exchange of comq's own, a fanout that is declared as one
|
|
249
506
|
// where it is consumed, and declaring it on another shard as anything else is refused
|
|
250
507
|
const exhausted = exchange.startsWith(RETRY_PREFIX) ||
|
|
251
|
-
attempt >= this.#connections.
|
|
508
|
+
attempt >= this.#connections.size ||
|
|
252
509
|
rest.length === 0
|
|
253
510
|
|
|
254
511
|
if (exhausted) return report()
|
|
@@ -313,6 +570,10 @@ class Channel {
|
|
|
313
570
|
* @param {comq.Channel} channel
|
|
314
571
|
*/
|
|
315
572
|
#recover (channel) {
|
|
573
|
+
// one that is joining is added once it consumes what the rest do, and one that has been
|
|
574
|
+
// let go is not added at all
|
|
575
|
+
if (this.#joining.has(channel) || this.#gone.has(channel)) return
|
|
576
|
+
|
|
316
577
|
if (this.#bench.has(channel)) this.#comeback(channel)
|
|
317
578
|
|
|
318
579
|
this.#add(channel)
|
|
@@ -379,6 +640,9 @@ class Channel {
|
|
|
379
640
|
async #all (fn) {
|
|
380
641
|
const promises = this.#apply(fn)
|
|
381
642
|
|
|
643
|
+
// a shard that is joining is sealed and closed with the rest
|
|
644
|
+
for (const channel of this.#joining) promises.push(fn(channel))
|
|
645
|
+
|
|
382
646
|
await Promise.all(promises)
|
|
383
647
|
}
|
|
384
648
|
|
|
@@ -397,14 +661,15 @@ class Channel {
|
|
|
397
661
|
}
|
|
398
662
|
|
|
399
663
|
/**
|
|
400
|
-
* The channels of the shards that are known to be connected.
|
|
664
|
+
* The channels of the shards that are known to be connected, and are not retiring.
|
|
401
665
|
* An empty result means every shard is down or benched — wait rather than
|
|
402
666
|
* publish: a destroyed socket accepts a write without complaining.
|
|
403
667
|
*
|
|
404
668
|
* @return {comq.Channel[]}
|
|
405
669
|
*/
|
|
406
670
|
#reachable () {
|
|
407
|
-
return this.#pool.filter((channel) =>
|
|
671
|
+
return this.#pool.filter((channel) =>
|
|
672
|
+
this.#alive[channel.index] !== false && !this.#retiring.has(channel.index))
|
|
408
673
|
}
|
|
409
674
|
|
|
410
675
|
/**
|
|
@@ -419,14 +684,19 @@ class Channel {
|
|
|
419
684
|
const waiting = this.#recovery
|
|
420
685
|
const pool = this.#reachable()
|
|
421
686
|
|
|
422
|
-
|
|
687
|
+
// a shard that is retiring is still the way back for what arrived on it
|
|
688
|
+
const preferred = route.prefer === undefined
|
|
689
|
+
? undefined
|
|
690
|
+
: this.#pool.find((channel) =>
|
|
691
|
+
channel.index === route.prefer && this.#alive[channel.index] !== false)
|
|
692
|
+
|
|
693
|
+
if (preferred === undefined && pool.length === 0) {
|
|
423
694
|
await waiting
|
|
424
695
|
|
|
425
696
|
return this.#one(fn, route)
|
|
426
697
|
}
|
|
427
698
|
|
|
428
|
-
const channel = pool.
|
|
429
|
-
pool[Math.floor(Math.random() * pool.length)]
|
|
699
|
+
const channel = preferred ?? pool[Math.floor(Math.random() * pool.length)]
|
|
430
700
|
|
|
431
701
|
// told before the publish is awaited: a shard lost while it is under way has it on board
|
|
432
702
|
route.via?.(channel.index)
|
|
@@ -455,7 +725,7 @@ function arrival (consumer, channel) {
|
|
|
455
725
|
}
|
|
456
726
|
|
|
457
727
|
/**
|
|
458
|
-
* @param {comq.Connection[]} connections
|
|
728
|
+
* @param {comq.Connection[] | Map<number, comq.Connection>} connections
|
|
459
729
|
* @param {comq.topology.type} type
|
|
460
730
|
* @return {comq.Channel}
|
|
461
731
|
*/
|