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
|
@@ -15,32 +15,79 @@ it('should be', async () => {
|
|
|
15
15
|
expect(Connection).toBeDefined()
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
-
/** @type {jest.MockedObject<comq.Connection>[]} */
|
|
18
|
+
/** @type {jest.MockedObject<comq.Connection>[]} the connections made, in the order they were */
|
|
19
19
|
let connections
|
|
20
20
|
|
|
21
|
+
/** @type {jest.MockedFunction<(url: string, servername?: string) => comq.Connection>} */
|
|
22
|
+
let make
|
|
23
|
+
|
|
21
24
|
/** @type {comq.Connection} */
|
|
22
25
|
let connection
|
|
23
26
|
|
|
27
|
+
/** @type {jest.MockedObject<comq.Channel>[]} */
|
|
28
|
+
let channels
|
|
29
|
+
|
|
30
|
+
const urls = ['amqp://10.0.0.1', 'amqp://10.0.0.2']
|
|
31
|
+
|
|
32
|
+
const sharded = () => {
|
|
33
|
+
const channel = {
|
|
34
|
+
has: jest.fn(() => false),
|
|
35
|
+
join: jest.fn(async () => undefined),
|
|
36
|
+
retire: jest.fn(() => undefined),
|
|
37
|
+
restore: jest.fn(() => undefined),
|
|
38
|
+
quiet: jest.fn(async () => 0),
|
|
39
|
+
leave: jest.fn(async () => undefined)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
channels.push(channel)
|
|
43
|
+
|
|
44
|
+
return channel
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
beforeEach(() => {
|
|
25
48
|
jest.clearAllMocks()
|
|
26
49
|
|
|
27
|
-
connections = [
|
|
28
|
-
|
|
50
|
+
connections = []
|
|
51
|
+
channels = []
|
|
52
|
+
|
|
53
|
+
make = jest.fn(() => {
|
|
54
|
+
const connection = mock.connection()
|
|
55
|
+
|
|
56
|
+
connections.push(connection)
|
|
57
|
+
|
|
58
|
+
return connection
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
create.mockImplementation(async () => sharded())
|
|
62
|
+
|
|
63
|
+
connection = new Connection(urls, make)
|
|
29
64
|
})
|
|
30
65
|
|
|
31
66
|
describe('open', () => {
|
|
67
|
+
it('should make a connection to each of the urls', async () => {
|
|
68
|
+
await connection.open()
|
|
69
|
+
|
|
70
|
+
expect(make).toHaveBeenCalledTimes(2)
|
|
71
|
+
urls.forEach((url) => expect(make).toHaveBeenCalledWith(url, undefined))
|
|
72
|
+
connections.forEach((conn) => expect(conn.open).toHaveBeenCalled())
|
|
73
|
+
})
|
|
74
|
+
|
|
32
75
|
it('should resolve when all of the connections are established', async () => {
|
|
33
76
|
expect.assertions(2)
|
|
34
77
|
|
|
35
78
|
/** @type {Promex[]} */
|
|
36
79
|
const promises = []
|
|
37
80
|
|
|
38
|
-
|
|
81
|
+
make.mockImplementation(() => {
|
|
82
|
+
const conn = mock.connection()
|
|
39
83
|
const promise = new Promex()
|
|
40
84
|
|
|
41
85
|
conn.open.mockImplementation(() => promise)
|
|
42
86
|
promises.push(promise)
|
|
43
|
-
|
|
87
|
+
connections.push(conn)
|
|
88
|
+
|
|
89
|
+
return conn
|
|
90
|
+
})
|
|
44
91
|
|
|
45
92
|
let resolved = false
|
|
46
93
|
|
|
@@ -64,21 +111,29 @@ describe('open', () => {
|
|
|
64
111
|
})
|
|
65
112
|
|
|
66
113
|
it('should close opened connection if one fails', async () => {
|
|
67
|
-
connections.push(mock.connection())
|
|
68
|
-
|
|
69
|
-
const [one, bad, two] = connections
|
|
70
114
|
const exception = new Error(generate())
|
|
71
115
|
|
|
72
|
-
|
|
73
|
-
two.open.mockImplementation(() => Promise.resolve())
|
|
116
|
+
connection = new Connection([...urls, 'amqp://10.0.0.3'], make)
|
|
74
117
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
118
|
+
make.mockImplementation(() => {
|
|
119
|
+
const conn = mock.connection()
|
|
120
|
+
|
|
121
|
+
if (connections.length === 1) {
|
|
122
|
+
conn.open.mockImplementation(async () => {
|
|
123
|
+
await immediate()
|
|
124
|
+
throw exception
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
connections.push(conn)
|
|
129
|
+
|
|
130
|
+
return conn
|
|
78
131
|
})
|
|
79
132
|
|
|
80
133
|
await expect(connection.open()).rejects.toThrow(exception)
|
|
81
134
|
|
|
135
|
+
const [one, , two] = connections
|
|
136
|
+
|
|
82
137
|
expect(one.close).toHaveBeenCalled()
|
|
83
138
|
expect(two.close).toHaveBeenCalled()
|
|
84
139
|
})
|
|
@@ -86,6 +141,7 @@ describe('open', () => {
|
|
|
86
141
|
|
|
87
142
|
describe('close', () => {
|
|
88
143
|
it('should close all connections', async () => {
|
|
144
|
+
await connection.open()
|
|
89
145
|
await connection.close()
|
|
90
146
|
|
|
91
147
|
for (const conn of connections) {
|
|
@@ -97,17 +153,20 @@ describe('close', () => {
|
|
|
97
153
|
describe('createChannel', () => {
|
|
98
154
|
const type = generate()
|
|
99
155
|
|
|
100
|
-
beforeEach(() => {
|
|
101
|
-
connection.
|
|
156
|
+
beforeEach(async () => {
|
|
157
|
+
await connection.open()
|
|
158
|
+
await connection.createChannel(type)
|
|
102
159
|
})
|
|
103
160
|
|
|
104
|
-
it('should create channel', async () => {
|
|
105
|
-
expect(create).toHaveBeenCalledWith(connections, type)
|
|
161
|
+
it('should create channel over the connections, by the number of each', async () => {
|
|
162
|
+
expect(create).toHaveBeenCalledWith(new Map(connections.entries()), type)
|
|
106
163
|
})
|
|
107
164
|
})
|
|
108
165
|
|
|
109
166
|
describe.each(/** @type {comq.diagnostics.Event[]} */ ['open', 'close'])('diagnose %s event',
|
|
110
167
|
(event) => {
|
|
168
|
+
beforeEach(() => connection.open())
|
|
169
|
+
|
|
111
170
|
it('should re-emit event', async () => {
|
|
112
171
|
const index = random(connections.length)
|
|
113
172
|
|
|
@@ -134,6 +193,8 @@ describe.each(/** @type {comq.diagnostics.Event[]} */ ['open', 'close'])('diagno
|
|
|
134
193
|
|
|
135
194
|
describe('forget', () => {
|
|
136
195
|
it('should stop re-emitting to the listener', async () => {
|
|
196
|
+
await connection.open()
|
|
197
|
+
|
|
137
198
|
const listener = /** @type {Function} */ jest.fn()
|
|
138
199
|
|
|
139
200
|
connection.diagnose('open', listener)
|
|
@@ -146,3 +207,297 @@ describe('forget', () => {
|
|
|
146
207
|
expect(listener).not.toHaveBeenCalled()
|
|
147
208
|
})
|
|
148
209
|
})
|
|
210
|
+
|
|
211
|
+
describe('names', () => {
|
|
212
|
+
const INTERVAL = 1000
|
|
213
|
+
const SETTLE = 3000
|
|
214
|
+
const LINGER = 5000
|
|
215
|
+
|
|
216
|
+
const names = ['amqp://one', 'amqp://two', 'amqp://three', 'amqp://four']
|
|
217
|
+
|
|
218
|
+
/** @type {Record<string, string>} */
|
|
219
|
+
let addresses
|
|
220
|
+
|
|
221
|
+
/** @type {Record<string, jest.MockedFunction<Function>>} */
|
|
222
|
+
let heard
|
|
223
|
+
|
|
224
|
+
const tick = (ms = INTERVAL) => jest.advanceTimersByTimeAsync(ms)
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {jest.MockedObject<comq.Connection>} conn
|
|
228
|
+
* @param {comq.diagnostics.Event} event
|
|
229
|
+
* @param {...any} args
|
|
230
|
+
*/
|
|
231
|
+
const tell = (conn, event, ...args) => {
|
|
232
|
+
for (const [name, listener] of conn.diagnose.mock.calls) if (name === event) listener(...args)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
beforeEach(async () => {
|
|
236
|
+
jest.useFakeTimers()
|
|
237
|
+
|
|
238
|
+
addresses = { one: '10.0.0.1', two: '10.0.0.1', three: '10.0.0.2', four: '10.0.0.2' }
|
|
239
|
+
|
|
240
|
+
global.COMQ_TESTING_LOOKUP = async (host) => ({ addresses: [addresses[host]], ttl: 1 })
|
|
241
|
+
|
|
242
|
+
connection = new Connection(names, make, { interval: INTERVAL, settle: SETTLE, linger: LINGER })
|
|
243
|
+
heard = { join: jest.fn(), retire: jest.fn(), leave: jest.fn() }
|
|
244
|
+
|
|
245
|
+
for (const [event, listener] of Object.entries(heard)) connection.diagnose(event, listener)
|
|
246
|
+
|
|
247
|
+
await connection.open()
|
|
248
|
+
await connection.createChannel(generate())
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
afterEach(async () => {
|
|
252
|
+
await connection.close()
|
|
253
|
+
|
|
254
|
+
jest.useRealTimers()
|
|
255
|
+
|
|
256
|
+
delete global.COMQ_TESTING_LOOKUP
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('should make a connection to each broker rather than to each name', async () => {
|
|
260
|
+
expect(make).toHaveBeenCalledTimes(2)
|
|
261
|
+
expect(make).toHaveBeenCalledWith('amqp://10.0.0.1', 'one')
|
|
262
|
+
expect(make).toHaveBeenCalledWith('amqp://10.0.0.2', 'three')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should join a broker a name has moved to, once that has settled', async () => {
|
|
266
|
+
addresses.four = '10.0.0.3'
|
|
267
|
+
|
|
268
|
+
await tick(SETTLE)
|
|
269
|
+
|
|
270
|
+
expect(make).toHaveBeenCalledTimes(2)
|
|
271
|
+
|
|
272
|
+
await tick()
|
|
273
|
+
|
|
274
|
+
expect(make).toHaveBeenCalledWith('amqp://10.0.0.3', 'four')
|
|
275
|
+
expect(connections[2].open).toHaveBeenCalled()
|
|
276
|
+
expect(channels[0].join).toHaveBeenCalledWith(connections[2], 2)
|
|
277
|
+
expect(heard.join).toHaveBeenCalledWith(2, '10.0.0.3')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should not retire a broker that is still named', async () => {
|
|
281
|
+
addresses.four = '10.0.0.3'
|
|
282
|
+
|
|
283
|
+
await tick(SETTLE + LINGER * 2)
|
|
284
|
+
|
|
285
|
+
expect(channels[0].retire).not.toHaveBeenCalled()
|
|
286
|
+
expect(heard.retire).not.toHaveBeenCalled()
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('should retire a broker no name points at', async () => {
|
|
290
|
+
addresses.three = '10.0.0.3'
|
|
291
|
+
addresses.four = '10.0.0.3'
|
|
292
|
+
|
|
293
|
+
await tick(SETTLE + INTERVAL)
|
|
294
|
+
|
|
295
|
+
expect(channels[0].retire).toHaveBeenCalledWith(1)
|
|
296
|
+
expect(heard.retire).toHaveBeenCalledWith(1, '10.0.0.2')
|
|
297
|
+
expect(connections[1].close).not.toHaveBeenCalled()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should not retire a broker before the one named instead has joined', async () => {
|
|
301
|
+
const opening = new Promex()
|
|
302
|
+
|
|
303
|
+
make.mockImplementation(() => {
|
|
304
|
+
const conn = mock.connection()
|
|
305
|
+
|
|
306
|
+
conn.open.mockImplementation(() => opening)
|
|
307
|
+
connections.push(conn)
|
|
308
|
+
|
|
309
|
+
return conn
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
addresses.three = '10.0.0.3'
|
|
313
|
+
addresses.four = '10.0.0.3'
|
|
314
|
+
|
|
315
|
+
await tick(SETTLE + LINGER * 2)
|
|
316
|
+
|
|
317
|
+
expect(heard.retire).not.toHaveBeenCalled()
|
|
318
|
+
|
|
319
|
+
opening.resolve()
|
|
320
|
+
|
|
321
|
+
await tick(0)
|
|
322
|
+
|
|
323
|
+
expect(heard.retire).toHaveBeenCalledWith(1, '10.0.0.2')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('should take a shard that has joined into a channel made later', async () => {
|
|
327
|
+
addresses.four = '10.0.0.3'
|
|
328
|
+
|
|
329
|
+
await tick(SETTLE + INTERVAL)
|
|
330
|
+
await connection.createChannel(generate())
|
|
331
|
+
|
|
332
|
+
expect(create).toHaveBeenLastCalledWith(new Map(connections.entries()), expect.any(String))
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('retired', () => {
|
|
336
|
+
beforeEach(async () => {
|
|
337
|
+
addresses.three = '10.0.0.3'
|
|
338
|
+
addresses.four = '10.0.0.3'
|
|
339
|
+
|
|
340
|
+
await tick(SETTLE + INTERVAL)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('should leave a broker that has been idle for the linger time', async () => {
|
|
344
|
+
await tick(LINGER - INTERVAL)
|
|
345
|
+
|
|
346
|
+
expect(heard.leave).not.toHaveBeenCalled()
|
|
347
|
+
|
|
348
|
+
await tick(INTERVAL * 2)
|
|
349
|
+
|
|
350
|
+
expect(channels[0].leave).toHaveBeenCalledWith(1)
|
|
351
|
+
expect(connections[1].close).toHaveBeenCalled()
|
|
352
|
+
expect(heard.leave).toHaveBeenCalledWith(1, '10.0.0.2')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('should not leave a broker that has something to do', async () => {
|
|
356
|
+
channels[0].quiet.mockImplementation(async () => Date.now())
|
|
357
|
+
|
|
358
|
+
await tick(LINGER * 3)
|
|
359
|
+
|
|
360
|
+
expect(heard.leave).not.toHaveBeenCalled()
|
|
361
|
+
expect(connections[1].close).not.toHaveBeenCalled()
|
|
362
|
+
|
|
363
|
+
channels[0].quiet.mockImplementation(async () => 0)
|
|
364
|
+
|
|
365
|
+
await tick()
|
|
366
|
+
|
|
367
|
+
expect(heard.leave).toHaveBeenCalledWith(1, '10.0.0.2')
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('should count the linger time from when the broker was last busy', async () => {
|
|
371
|
+
const busy = Date.now() + INTERVAL * 2
|
|
372
|
+
|
|
373
|
+
channels[0].quiet.mockImplementation(async () => busy)
|
|
374
|
+
|
|
375
|
+
await tick(LINGER + INTERVAL)
|
|
376
|
+
|
|
377
|
+
expect(heard.leave).not.toHaveBeenCalled()
|
|
378
|
+
|
|
379
|
+
await tick(INTERVAL * 2)
|
|
380
|
+
|
|
381
|
+
expect(heard.leave).toHaveBeenCalled()
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('should give up on a broker that is out of reach for the linger time', async () => {
|
|
385
|
+
connections[1].connected = false
|
|
386
|
+
channels[0].quiet.mockImplementation(async () => Date.now())
|
|
387
|
+
|
|
388
|
+
await tick(LINGER + INTERVAL * 2)
|
|
389
|
+
|
|
390
|
+
expect(heard.leave).toHaveBeenCalledWith(1, '10.0.0.2')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('should put a broker that is named again back', async () => {
|
|
394
|
+
addresses.four = '10.0.0.2'
|
|
395
|
+
|
|
396
|
+
await tick(SETTLE + INTERVAL)
|
|
397
|
+
|
|
398
|
+
expect(channels[0].restore).toHaveBeenCalledWith(1)
|
|
399
|
+
expect(heard.join).toHaveBeenCalledWith(1, '10.0.0.2')
|
|
400
|
+
|
|
401
|
+
await tick(LINGER * 2)
|
|
402
|
+
|
|
403
|
+
expect(heard.leave).not.toHaveBeenCalled()
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('should retire in a channel made later', async () => {
|
|
407
|
+
const channel = await connection.createChannel(generate())
|
|
408
|
+
|
|
409
|
+
expect(channel.retire).toHaveBeenCalledWith(1)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('should join anew a broker that was left', async () => {
|
|
413
|
+
await tick(LINGER + INTERVAL)
|
|
414
|
+
|
|
415
|
+
addresses.four = '10.0.0.2'
|
|
416
|
+
|
|
417
|
+
await tick(SETTLE + INTERVAL)
|
|
418
|
+
|
|
419
|
+
expect(make).toHaveBeenCalledTimes(4)
|
|
420
|
+
expect(heard.join).toHaveBeenCalledWith(3, '10.0.0.2')
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('should follow at once the names of a broker that is out of reach', async () => {
|
|
425
|
+
addresses.three = '10.0.0.3'
|
|
426
|
+
addresses.four = '10.0.0.3'
|
|
427
|
+
|
|
428
|
+
connections[1].connected = false
|
|
429
|
+
|
|
430
|
+
tell(connections[1], 'close', new Error('lost'))
|
|
431
|
+
|
|
432
|
+
await tick(0)
|
|
433
|
+
|
|
434
|
+
expect(make).toHaveBeenCalledWith('amqp://10.0.0.3', 'three')
|
|
435
|
+
expect(heard.join).toHaveBeenCalledWith(2, '10.0.0.3')
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('should report a name that fails to resolve', async () => {
|
|
439
|
+
const listener = jest.fn()
|
|
440
|
+
const exception = new Error('ENOTFOUND')
|
|
441
|
+
|
|
442
|
+
connection.diagnose('error', listener)
|
|
443
|
+
|
|
444
|
+
global.COMQ_TESTING_LOOKUP = async () => { throw exception }
|
|
445
|
+
|
|
446
|
+
await tick()
|
|
447
|
+
|
|
448
|
+
expect(listener).toHaveBeenCalledWith(exception)
|
|
449
|
+
expect(heard.retire).not.toHaveBeenCalled()
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('should not take a broker that cannot be consumed from for one that has joined', async () => {
|
|
453
|
+
const exception = new Error('PRECONDITION_FAILED')
|
|
454
|
+
const listener = jest.fn()
|
|
455
|
+
|
|
456
|
+
connection.diagnose('error', listener)
|
|
457
|
+
channels[0].join.mockImplementationOnce(async () => { throw exception })
|
|
458
|
+
|
|
459
|
+
addresses.three = '10.0.0.3'
|
|
460
|
+
addresses.four = '10.0.0.3'
|
|
461
|
+
|
|
462
|
+
await tick(SETTLE + INTERVAL)
|
|
463
|
+
|
|
464
|
+
expect(listener).toHaveBeenCalledWith(exception, 2)
|
|
465
|
+
expect(heard.join).not.toHaveBeenCalled()
|
|
466
|
+
expect(heard.retire).not.toHaveBeenCalled()
|
|
467
|
+
expect(channels[0].leave).toHaveBeenCalledWith(2)
|
|
468
|
+
expect(connections[2].close).toHaveBeenCalled()
|
|
469
|
+
|
|
470
|
+
// and joins it anew later
|
|
471
|
+
await tick()
|
|
472
|
+
|
|
473
|
+
expect(heard.join).toHaveBeenCalledWith(3, '10.0.0.3')
|
|
474
|
+
expect(heard.retire).toHaveBeenCalledWith(1, '10.0.0.2')
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('should ask again a broker that refused', async () => {
|
|
478
|
+
const exception = new Error('ACCESS-REFUSED')
|
|
479
|
+
const listener = jest.fn()
|
|
480
|
+
|
|
481
|
+
connection.diagnose('error', listener)
|
|
482
|
+
|
|
483
|
+
make.mockImplementationOnce(() => {
|
|
484
|
+
const conn = mock.connection()
|
|
485
|
+
|
|
486
|
+
conn.open.mockImplementation(async () => { throw exception })
|
|
487
|
+
connections.push(conn)
|
|
488
|
+
|
|
489
|
+
return conn
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
addresses.four = '10.0.0.3'
|
|
493
|
+
|
|
494
|
+
await tick(SETTLE + INTERVAL)
|
|
495
|
+
|
|
496
|
+
expect(listener).toHaveBeenCalledWith(exception, 2)
|
|
497
|
+
expect(heard.join).not.toHaveBeenCalled()
|
|
498
|
+
|
|
499
|
+
await tick()
|
|
500
|
+
|
|
501
|
+
expect(heard.join).toHaveBeenCalledWith(3, '10.0.0.3')
|
|
502
|
+
})
|
|
503
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { expand } = require('../../source/shards/expand')
|
|
4
|
+
|
|
5
|
+
it('should leave a url without a range as it is', async () => {
|
|
6
|
+
expect(expand('amqp://user:secret@rmq.example.com:5672/vhost?heartbeat=5'))
|
|
7
|
+
.toStrictEqual(['amqp://user:secret@rmq.example.com:5672/vhost?heartbeat=5'])
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should expand a range, its end being exclusive', async () => {
|
|
11
|
+
expect(expand('amqp://rmq[0..3].example.com')).toStrictEqual([
|
|
12
|
+
'amqp://rmq0.example.com',
|
|
13
|
+
'amqp://rmq1.example.com',
|
|
14
|
+
'amqp://rmq2.example.com'
|
|
15
|
+
])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should name as many hosts as the end of a range that starts at zero', async () => {
|
|
19
|
+
expect(expand('amqp://rmq[0..32].example.com')).toHaveLength(32)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should keep what surrounds the host', async () => {
|
|
23
|
+
expect(expand('amqps://user:secret@rmq-[1..3].example.com:5671/vhost?heartbeat=5')).toStrictEqual([
|
|
24
|
+
'amqps://user:secret@rmq-1.example.com:5671/vhost?heartbeat=5',
|
|
25
|
+
'amqps://user:secret@rmq-2.example.com:5671/vhost?heartbeat=5'
|
|
26
|
+
])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should pad to the width the start is written with', async () => {
|
|
30
|
+
expect(expand('amqp://rmq[08..11]')).toStrictEqual(['amqp://rmq08', 'amqp://rmq09', 'amqp://rmq10'])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should expand every range', async () => {
|
|
34
|
+
expect(expand('amqp://rmq[0..2].zone[0..2]')).toStrictEqual([
|
|
35
|
+
'amqp://rmq0.zone0',
|
|
36
|
+
'amqp://rmq0.zone1',
|
|
37
|
+
'amqp://rmq1.zone0',
|
|
38
|
+
'amqp://rmq1.zone1'
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should leave an IPv6 literal alone', async () => {
|
|
43
|
+
expect(expand('amqp://[::1]:5672')).toStrictEqual(['amqp://[::1]:5672'])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should look at the host only', async () => {
|
|
47
|
+
const url = 'amqp://user:[0..2]@rmq.example.com/[0..2]?name=[0..2]'
|
|
48
|
+
|
|
49
|
+
expect(expand(url)).toStrictEqual([url])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should leave what is not a url as it is', async () => {
|
|
53
|
+
expect(expand('whatever')).toStrictEqual(['whatever'])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it.each(['[3..3]', '[3..1]'])('should refuse a range that names no hosts: %s', async (range) => {
|
|
57
|
+
expect(() => expand(`amqp://rmq${range}`)).toThrow(RangeError)
|
|
58
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { choose, hostOf, locate } = require('../../source/shards/resolve')
|
|
4
|
+
|
|
5
|
+
describe('choose', () => {
|
|
6
|
+
it('should choose the same address whichever order they came in', async () => {
|
|
7
|
+
expect(choose(['10.0.0.2', '10.0.0.1'])).toStrictEqual('10.0.0.1')
|
|
8
|
+
expect(choose(['10.0.0.1', '10.0.0.2'])).toStrictEqual('10.0.0.1')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should prefer IPv4', async () => {
|
|
12
|
+
expect(choose(['::1', '127.0.0.1'])).toStrictEqual('127.0.0.1')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should keep the address in use while it is listed', async () => {
|
|
16
|
+
expect(choose(['10.0.0.1', '10.0.0.2'], '10.0.0.2')).toStrictEqual('10.0.0.2')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should let go of an address that is no longer listed', async () => {
|
|
20
|
+
expect(choose(['10.0.0.1', '10.0.0.3'], '10.0.0.2')).toStrictEqual('10.0.0.1')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('hostOf', () => {
|
|
25
|
+
it('should tell the name', async () => {
|
|
26
|
+
expect(hostOf('amqp://user:secret@rmq.example.com:5672/vhost')).toStrictEqual('rmq.example.com')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it.each(['amqp://10.0.0.1:5672', 'amqp://[::1]', 'whatever'])('should tell nothing of %s',
|
|
30
|
+
async (url) => {
|
|
31
|
+
expect(hostOf(url)).toBeUndefined()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('locate', () => {
|
|
36
|
+
it('should put the address in place of the name, which the broker is verified by', async () => {
|
|
37
|
+
expect(locate('amqps://user:secret@rmq.example.com:5671/vhost?heartbeat=5', '10.0.0.1')).toStrictEqual({
|
|
38
|
+
url: 'amqps://user:secret@10.0.0.1:5671/vhost?heartbeat=5',
|
|
39
|
+
address: '10.0.0.1',
|
|
40
|
+
servername: 'rmq.example.com'
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should bracket an IPv6 address', async () => {
|
|
45
|
+
expect(locate('amqp://rmq.example.com:5672', '::1').url).toStrictEqual('amqp://[::1]:5672')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should locate the names of one broker alike', async () => {
|
|
49
|
+
const one = locate('amqp://rmq0.example.com', '10.0.0.1')
|
|
50
|
+
const another = locate('amqp://rmq1.example.com', '10.0.0.1')
|
|
51
|
+
|
|
52
|
+
expect(one.url).toStrictEqual(another.url)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should tell brokers apart by port, credentials and virtual host', async () => {
|
|
56
|
+
const urls = [
|
|
57
|
+
'amqp://rmq.example.com',
|
|
58
|
+
'amqp://rmq.example.com:5673',
|
|
59
|
+
'amqp://user:secret@rmq.example.com',
|
|
60
|
+
'amqp://rmq.example.com/vhost'
|
|
61
|
+
].map((url) => locate(url, '10.0.0.1').url)
|
|
62
|
+
|
|
63
|
+
expect(new Set(urls).size).toStrictEqual(urls.length)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should leave an address as it is', async () => {
|
|
67
|
+
expect(locate('amqp://10.0.0.1:5672')).toStrictEqual({ url: 'amqp://10.0.0.1:5672', address: '10.0.0.1' })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should leave what is not a url as it is', async () => {
|
|
71
|
+
expect(locate('whatever')).toStrictEqual({ url: 'whatever', address: 'whatever' })
|
|
72
|
+
})
|
|
73
|
+
})
|