comq 0.24.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/features/heartbeat.feature +16 -0
- package/features/rpc.call.recovery.feature +2 -2
- 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/connection.js +23 -0
- package/features/steps/context.js +1 -2
- package/features/steps/hooks.js +0 -1
- package/features/steps/network.js +11 -2
- package/features/steps/networks.js +31 -2
- package/features/steps/retransmission.js +165 -0
- package/features/steps/rpc.js +0 -6
- package/package.json +15 -1
- package/readme.md +9 -4
- package/source/channel.js +3 -1
- package/source/connection.js +9 -86
- package/source/io.js +90 -15
- package/source/shards/channel.js +73 -12
- package/test/connection.mock.js +3 -2
- package/test/connection.test.js +15 -77
- package/test/io.call.test.js +5 -3
- package/test/io.reply.test.js +5 -3
- package/test/io.request.test.js +264 -2
- package/test/memory.test.js +0 -2
- package/test/shards/channel.test.js +168 -2
- package/types/channel.d.ts +13 -4
- package/types/io.d.ts +9 -0
package/test/io.request.test.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const stream = require('node:stream')
|
|
6
6
|
const { randomBytes } = require('node:crypto')
|
|
7
7
|
const { generate } = require('randomstring')
|
|
8
|
+
const { Promex } = require('promex')
|
|
8
9
|
const { immediate } = require('./helpers')
|
|
9
10
|
const { encode } = require('../source/encode')
|
|
10
11
|
|
|
@@ -327,15 +328,276 @@ describe('send', () => {
|
|
|
327
328
|
})
|
|
328
329
|
})
|
|
329
330
|
|
|
331
|
+
describe('shards', () => {
|
|
332
|
+
beforeEach(async () => {
|
|
333
|
+
jest.clearAllMocks()
|
|
334
|
+
|
|
335
|
+
connection = mock.connection(true)
|
|
336
|
+
io = new IO(connection)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Sends a Request through a shard.
|
|
341
|
+
*
|
|
342
|
+
* @param {number} index
|
|
343
|
+
* @param {string} target
|
|
344
|
+
* @return {Promise<void>}
|
|
345
|
+
*/
|
|
346
|
+
async function ask (index, target) {
|
|
347
|
+
requests = await requestChannel()
|
|
348
|
+
|
|
349
|
+
requests.send.mockImplementationOnce(async (_queue, _buffer, _options, via) => via(index))
|
|
350
|
+
|
|
351
|
+
io.request(target, payload)
|
|
352
|
+
|
|
353
|
+
await immediate()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @return {Promise<jest.MockedObject<comq.Channel>>}
|
|
358
|
+
*/
|
|
359
|
+
async function requestChannel () {
|
|
360
|
+
if (connection.createChannel.mock.calls.length === 0) {
|
|
361
|
+
// the channels are created with the first Request
|
|
362
|
+
io.request(generate(), payload)
|
|
363
|
+
|
|
364
|
+
await immediate()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return findChannel('request')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @param {jest.MockedObject<comq.Channel>} channel
|
|
372
|
+
* @return {string[]} the queues each publish went to
|
|
373
|
+
*/
|
|
374
|
+
const targets = (channel) => channel.send.mock.calls.map(([queue]) => queue)
|
|
375
|
+
|
|
376
|
+
it('should re-send only the requests the lost shard holds', async () => {
|
|
377
|
+
const a = generate()
|
|
378
|
+
const b = generate()
|
|
379
|
+
|
|
380
|
+
await ask(0, a)
|
|
381
|
+
await ask(1, b)
|
|
382
|
+
|
|
383
|
+
requests.send.mockClear()
|
|
384
|
+
|
|
385
|
+
await lose(1)
|
|
386
|
+
|
|
387
|
+
expect(targets(requests)).toStrictEqual([b])
|
|
388
|
+
|
|
389
|
+
requests.send.mockClear()
|
|
390
|
+
|
|
391
|
+
// b has been re-sent through the shard that stays, and is held by it now
|
|
392
|
+
await lose(0)
|
|
393
|
+
|
|
394
|
+
expect(targets(requests)).toContain(a)
|
|
395
|
+
expect(targets(requests)).toContain(b)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('should re-send only the requests of a shard whose reply channel recovers', async () => {
|
|
399
|
+
const a = generate()
|
|
400
|
+
const b = generate()
|
|
401
|
+
|
|
402
|
+
await ask(0, a)
|
|
403
|
+
await ask(1, b)
|
|
404
|
+
|
|
405
|
+
replies = await findChannel('reply')
|
|
406
|
+
requests.send.mockClear()
|
|
407
|
+
|
|
408
|
+
const calls = replies.diagnose.mock.calls.filter((call) => call[0] === 'recover')
|
|
409
|
+
|
|
410
|
+
for (const [, listener] of calls) listener(1)
|
|
411
|
+
|
|
412
|
+
await immediate()
|
|
413
|
+
|
|
414
|
+
expect(targets(requests)).toStrictEqual([b])
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('should re-send only the requests of a shard removed from the pool', async () => {
|
|
418
|
+
const a = generate()
|
|
419
|
+
const b = generate()
|
|
420
|
+
|
|
421
|
+
await ask(0, a)
|
|
422
|
+
await ask(1, b)
|
|
423
|
+
|
|
424
|
+
requests.send.mockClear()
|
|
425
|
+
|
|
426
|
+
const calls = requests.diagnose.mock.calls.filter((call) => call[0] === 'remove')
|
|
427
|
+
|
|
428
|
+
for (const [, listener] of calls) listener(0)
|
|
429
|
+
|
|
430
|
+
await immediate()
|
|
431
|
+
|
|
432
|
+
expect(targets(requests).includes(a)).toStrictEqual(true)
|
|
433
|
+
expect(targets(requests).includes(b)).toStrictEqual(false)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('should re-send a request the lost shard held while it was being published', async () => {
|
|
437
|
+
requests = await requestChannel()
|
|
438
|
+
|
|
439
|
+
const published = new Promex()
|
|
440
|
+
|
|
441
|
+
requests.send.mockImplementationOnce(async (_queue, _buffer, _options, via) => {
|
|
442
|
+
via(1)
|
|
443
|
+
|
|
444
|
+
await published
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
const target = generate()
|
|
448
|
+
|
|
449
|
+
io.request(target, payload)
|
|
450
|
+
|
|
451
|
+
await immediate()
|
|
452
|
+
await lose(1)
|
|
453
|
+
|
|
454
|
+
// not before it is published
|
|
455
|
+
expect(targets(requests).filter((queue) => queue === target)).toHaveLength(1)
|
|
456
|
+
|
|
457
|
+
published.resolve()
|
|
458
|
+
|
|
459
|
+
await immediate()
|
|
460
|
+
await immediate()
|
|
461
|
+
|
|
462
|
+
expect(targets(requests).filter((queue) => queue === target)).toHaveLength(2)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('should not re-send a request that failed over to another shard', async () => {
|
|
466
|
+
requests = await requestChannel()
|
|
467
|
+
|
|
468
|
+
const published = new Promex()
|
|
469
|
+
|
|
470
|
+
requests.send.mockImplementationOnce(async (_queue, _buffer, _options, via) => {
|
|
471
|
+
via(1)
|
|
472
|
+
|
|
473
|
+
await published
|
|
474
|
+
|
|
475
|
+
// the pool fails over from the lost shard
|
|
476
|
+
via(0)
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
const target = generate()
|
|
480
|
+
|
|
481
|
+
io.request(target, payload)
|
|
482
|
+
|
|
483
|
+
await immediate()
|
|
484
|
+
await lose(1)
|
|
485
|
+
|
|
486
|
+
published.resolve()
|
|
487
|
+
|
|
488
|
+
await immediate()
|
|
489
|
+
await immediate()
|
|
490
|
+
|
|
491
|
+
expect(targets(requests).filter((queue) => queue === target)).toHaveLength(1)
|
|
492
|
+
|
|
493
|
+
// it is held by the shard it went through
|
|
494
|
+
await lose(0)
|
|
495
|
+
|
|
496
|
+
expect(targets(requests).filter((queue) => queue === target)).toHaveLength(2)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('should not re-send a request that went through another shard than the one lost as it was published', async () => {
|
|
500
|
+
requests = await requestChannel()
|
|
501
|
+
|
|
502
|
+
const published = new Promex()
|
|
503
|
+
|
|
504
|
+
requests.send.mockImplementationOnce(async (_queue, _buffer, _options, via) => {
|
|
505
|
+
via(0)
|
|
506
|
+
|
|
507
|
+
await published
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
const target = generate()
|
|
511
|
+
|
|
512
|
+
io.request(target, payload)
|
|
513
|
+
|
|
514
|
+
await immediate()
|
|
515
|
+
await lose(1)
|
|
516
|
+
|
|
517
|
+
published.resolve()
|
|
518
|
+
|
|
519
|
+
await immediate()
|
|
520
|
+
await immediate()
|
|
521
|
+
|
|
522
|
+
expect(targets(requests).filter((queue) => queue === target)).toHaveLength(1)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it('should re-send a returned call from the shard it was published on next', async () => {
|
|
526
|
+
requests = await requestChannel()
|
|
527
|
+
|
|
528
|
+
const key = generate()
|
|
529
|
+
|
|
530
|
+
requests.route.mockImplementationOnce(async (_exchange, _key, _buffer, _options, via) => via(0))
|
|
531
|
+
|
|
532
|
+
io.call(generate(), key, payload)
|
|
533
|
+
|
|
534
|
+
await immediate()
|
|
535
|
+
|
|
536
|
+
const [,,, properties] = requests.route.mock.calls[0]
|
|
537
|
+
const message = { fields: { exchange: generate(), routingKey: key }, properties }
|
|
538
|
+
const calls = requests.diagnose.mock.calls.filter((call) => call[0] === 'reroute')
|
|
539
|
+
|
|
540
|
+
expect(calls.length).toBeGreaterThan(0)
|
|
541
|
+
|
|
542
|
+
for (const [, listener] of calls) listener(message, 1)
|
|
543
|
+
|
|
544
|
+
await lose(0)
|
|
545
|
+
|
|
546
|
+
expect(requests.route).toHaveBeenCalledTimes(1)
|
|
547
|
+
|
|
548
|
+
await lose(1)
|
|
549
|
+
|
|
550
|
+
expect(requests.route).toHaveBeenCalledTimes(2)
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('should re-send a call the lost shard holds', async () => {
|
|
554
|
+
requests = await requestChannel()
|
|
555
|
+
|
|
556
|
+
requests.route.mockImplementationOnce(async (_exchange, _key, _buffer, _options, via) => via(1))
|
|
557
|
+
|
|
558
|
+
io.call(generate(), generate(), payload)
|
|
559
|
+
|
|
560
|
+
await immediate()
|
|
561
|
+
await lose(0)
|
|
562
|
+
|
|
563
|
+
expect(requests.route).toHaveBeenCalledTimes(1)
|
|
564
|
+
|
|
565
|
+
await lose(1)
|
|
566
|
+
|
|
567
|
+
expect(requests.route).toHaveBeenCalledTimes(2)
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
describe('recovery', () => {
|
|
572
|
+
it('should re-send every unanswered Request of a connection that is not sharded', async () => {
|
|
573
|
+
const other = generate()
|
|
574
|
+
|
|
575
|
+
io.request(other, payload)
|
|
576
|
+
|
|
577
|
+
await immediate()
|
|
578
|
+
|
|
579
|
+
requests.send.mockClear()
|
|
580
|
+
|
|
581
|
+
const calls = requests.diagnose.mock.calls.filter((call) => call[0] === 'recover')
|
|
582
|
+
|
|
583
|
+
for (const [, listener] of calls) listener()
|
|
584
|
+
|
|
585
|
+
await immediate()
|
|
586
|
+
|
|
587
|
+
expect(requests.send.mock.calls.map(([queue]) => queue).sort()).toStrictEqual([queue, other].sort())
|
|
588
|
+
})
|
|
589
|
+
})
|
|
590
|
+
|
|
330
591
|
/**
|
|
331
592
|
* Fires the listeners the IO has attached to the shard loss diagnostic.
|
|
332
593
|
*
|
|
594
|
+
* @param {number} [index]
|
|
333
595
|
* @return {Promise<void>}
|
|
334
596
|
*/
|
|
335
|
-
async function lose () {
|
|
597
|
+
async function lose (index = 0) {
|
|
336
598
|
const calls = requests.diagnose.mock.calls.filter((call) => call[0] === 'lost')
|
|
337
599
|
|
|
338
|
-
for (const [, listener] of calls) listener(
|
|
600
|
+
for (const [, listener] of calls) listener(index)
|
|
339
601
|
|
|
340
602
|
await immediate()
|
|
341
603
|
await immediate()
|
package/test/memory.test.js
CHANGED
|
@@ -29,14 +29,12 @@ beforeEach(() => {
|
|
|
29
29
|
amqplib.connect.mockImplementation(async (url) => wired(await connect(url)))
|
|
30
30
|
|
|
31
31
|
global.COMQ_TESTING_SHUTDOWN_TIMEOUT = 1
|
|
32
|
-
global.COMQ_TESTING_WATCHDOG_INTERVAL = 60_000
|
|
33
32
|
})
|
|
34
33
|
|
|
35
34
|
afterEach(() => {
|
|
36
35
|
SingletonConnection.__lets_pretend_this_method_doesnt_exist()
|
|
37
36
|
|
|
38
37
|
delete global.COMQ_TESTING_SHUTDOWN_TIMEOUT
|
|
39
|
-
delete global.COMQ_TESTING_WATCHDOG_INTERVAL
|
|
40
38
|
})
|
|
41
39
|
|
|
42
40
|
afterAll(() => {
|
|
@@ -230,7 +230,17 @@ describe.each(['consume', 'subscribe'])('%s', (method) => {
|
|
|
230
230
|
|
|
231
231
|
await channel[method](...args)
|
|
232
232
|
|
|
233
|
-
for (const chan of channels)
|
|
233
|
+
for (const chan of channels) {
|
|
234
|
+
const call = chan[method].mock.calls[0]
|
|
235
|
+
const message = /** @type {comq.amqp.Message} */ {}
|
|
236
|
+
|
|
237
|
+
expect(call.slice(0, -1)).toStrictEqual(args.slice(0, -1))
|
|
238
|
+
|
|
239
|
+
// a consumed message may be told which shard it arrived on on its way
|
|
240
|
+
await call.at(-1)(message)
|
|
241
|
+
|
|
242
|
+
expect(processor).toHaveBeenCalledWith(message)
|
|
243
|
+
}
|
|
234
244
|
})
|
|
235
245
|
|
|
236
246
|
it(`should not resolve ${method} until pending channels are created`, async () => {
|
|
@@ -593,6 +603,23 @@ describe('diagnose', () => {
|
|
|
593
603
|
expect(listener).not.toHaveBeenCalled()
|
|
594
604
|
})
|
|
595
605
|
|
|
606
|
+
it('should tell where a returned message is published next', async () => {
|
|
607
|
+
const listener = /** @type {jest.MockedFunction} */ jest.fn()
|
|
608
|
+
const index = random(channels.length)
|
|
609
|
+
const chan = channels[index]
|
|
610
|
+
const message = returned()
|
|
611
|
+
|
|
612
|
+
message.fields.exchange = generate()
|
|
613
|
+
channel.diagnose('reroute', listener)
|
|
614
|
+
|
|
615
|
+
emitReturn(chan, message)
|
|
616
|
+
|
|
617
|
+
const next = channels.find((one) => one.route.mock.calls.length > 0)
|
|
618
|
+
|
|
619
|
+
expect(next).not.toBe(chan)
|
|
620
|
+
expect(listener).toHaveBeenCalledWith(message, next.index)
|
|
621
|
+
})
|
|
622
|
+
|
|
596
623
|
it('should report a returned retry at once', async () => {
|
|
597
624
|
const listener = /** @type {jest.MockedFunction} */ jest.fn()
|
|
598
625
|
const index = random(channels.length)
|
|
@@ -620,7 +647,15 @@ describe('diagnose', () => {
|
|
|
620
647
|
|
|
621
648
|
await channel.held(exchange, queue, key, consumer)
|
|
622
649
|
|
|
623
|
-
for (const one of channels)
|
|
650
|
+
for (const one of channels) {
|
|
651
|
+
expect(one.held).toHaveBeenCalledWith(exchange, queue, key, expect.any(Function))
|
|
652
|
+
|
|
653
|
+
const message = /** @type {comq.amqp.Message} */ {}
|
|
654
|
+
|
|
655
|
+
await one.held.mock.calls[0][3](message)
|
|
656
|
+
|
|
657
|
+
expect(consumer).toHaveBeenCalledWith(message)
|
|
658
|
+
}
|
|
624
659
|
})
|
|
625
660
|
|
|
626
661
|
it('should hold a queue once any shard holds it', async () => {
|
|
@@ -819,3 +854,134 @@ describe('routed', () => {
|
|
|
819
854
|
expect(used[0].route).toHaveBeenCalledWith(exchange, key, buffer, undefined)
|
|
820
855
|
})
|
|
821
856
|
})
|
|
857
|
+
|
|
858
|
+
describe('shards of messages', () => {
|
|
859
|
+
const queue = generate()
|
|
860
|
+
const buffer = randomBytes(8)
|
|
861
|
+
|
|
862
|
+
/** @type {jest.MockedObject<comq.Channel>[]} */
|
|
863
|
+
let channels
|
|
864
|
+
|
|
865
|
+
beforeEach(async () => {
|
|
866
|
+
channel = await create(connections, type)
|
|
867
|
+
channels = await getCreatedChannels()
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
it.each(['send', 'route'])('should tell `%s` which shard a message goes through', async (method) => {
|
|
871
|
+
const via = /** @type {jest.MockedFunction} */ jest.fn()
|
|
872
|
+
const args = method === 'send' ? [queue, buffer, {}] : [generate(), queue, buffer, {}]
|
|
873
|
+
|
|
874
|
+
await channel[method](...args, via)
|
|
875
|
+
|
|
876
|
+
const used = channels.find((chan) => chan[method].mock.calls.length > 0)
|
|
877
|
+
|
|
878
|
+
expect(via).toHaveBeenCalledTimes(1)
|
|
879
|
+
expect(via).toHaveBeenCalledWith(used.index)
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
it('should tell which shard a message goes through before it is published', async () => {
|
|
883
|
+
const via = /** @type {jest.MockedFunction} */ jest.fn()
|
|
884
|
+
const publishing = new Promex()
|
|
885
|
+
|
|
886
|
+
for (const chan of channels) chan.send.mockImplementationOnce(() => publishing)
|
|
887
|
+
|
|
888
|
+
const sending = channel.send(queue, buffer, {}, via)
|
|
889
|
+
|
|
890
|
+
await immediate()
|
|
891
|
+
|
|
892
|
+
// a shard lost from now on has it on board
|
|
893
|
+
expect(via).toHaveBeenCalledTimes(1)
|
|
894
|
+
|
|
895
|
+
publishing.resolve()
|
|
896
|
+
|
|
897
|
+
await sending
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
it('should tell the shard a message fails over to', async () => {
|
|
901
|
+
const via = /** @type {jest.MockedFunction} */ jest.fn()
|
|
902
|
+
|
|
903
|
+
for (const chan of channels) chan.send.mockImplementationOnce(async () => { throw new Error() })
|
|
904
|
+
|
|
905
|
+
// both shards fail once, and the one recovered first takes the message
|
|
906
|
+
setImmediate(() => emitRecover(channels[1]))
|
|
907
|
+
|
|
908
|
+
await channel.send(queue, buffer, {}, via)
|
|
909
|
+
|
|
910
|
+
const [first, second] = via.mock.calls.map(([index]) => index)
|
|
911
|
+
|
|
912
|
+
expect(via).toHaveBeenCalledTimes(3)
|
|
913
|
+
expect(second).not.toStrictEqual(first)
|
|
914
|
+
expect(via).toHaveBeenLastCalledWith(1)
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
it.each([0, 1])('should fire a reply through the shard its request arrived on (%i)', async (index) => {
|
|
918
|
+
const consumer = /** @type {jest.MockedFunction} */ jest.fn()
|
|
919
|
+
|
|
920
|
+
await channel.consume(queue, consumer)
|
|
921
|
+
|
|
922
|
+
const request = /** @type {comq.amqp.Message} */ {}
|
|
923
|
+
|
|
924
|
+
await channels[index].consume.mock.calls[0][1](request)
|
|
925
|
+
|
|
926
|
+
for (let i = 0; i < 20; i++) await channel.fire(queue, buffer, {}, request)
|
|
927
|
+
|
|
928
|
+
expect(channels[index].fire).toHaveBeenCalledTimes(20)
|
|
929
|
+
expect(channels[1 - index].fire).not.toHaveBeenCalled()
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
it('should fire a reply through the shard a held request arrived on', async () => {
|
|
933
|
+
await channel.held(generate(), generate(), generate(), jest.fn())
|
|
934
|
+
|
|
935
|
+
const request = /** @type {comq.amqp.Message} */ {}
|
|
936
|
+
|
|
937
|
+
await channels[1].held.mock.calls[0][3](request)
|
|
938
|
+
|
|
939
|
+
for (let i = 0; i < 20; i++) await channel.fire(queue, buffer, {}, request)
|
|
940
|
+
|
|
941
|
+
expect(channels[1].fire).toHaveBeenCalledTimes(20)
|
|
942
|
+
expect(channels[0].fire).not.toHaveBeenCalled()
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
it('should fire a reply through another shard when its own is lost', async () => {
|
|
946
|
+
await channel.consume(queue, jest.fn())
|
|
947
|
+
|
|
948
|
+
const request = /** @type {comq.amqp.Message} */ {}
|
|
949
|
+
|
|
950
|
+
await channels[1].consume.mock.calls[0][1](request)
|
|
951
|
+
|
|
952
|
+
lose(connections[1])
|
|
953
|
+
|
|
954
|
+
await channel.fire(queue, buffer, {}, request)
|
|
955
|
+
|
|
956
|
+
expect(channels[0].fire).toHaveBeenCalledTimes(1)
|
|
957
|
+
expect(channels[1].fire).not.toHaveBeenCalled()
|
|
958
|
+
|
|
959
|
+
restore(connections[1])
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
it('should fire a reply through another shard when its own fails', async () => {
|
|
963
|
+
await channel.consume(queue, jest.fn())
|
|
964
|
+
|
|
965
|
+
const request = /** @type {comq.amqp.Message} */ {}
|
|
966
|
+
|
|
967
|
+
await channels[1].consume.mock.calls[0][1](request)
|
|
968
|
+
|
|
969
|
+
channels[1].fire.mockImplementationOnce(async () => { throw new Error() })
|
|
970
|
+
|
|
971
|
+
await channel.fire(queue, buffer, {}, request)
|
|
972
|
+
|
|
973
|
+
expect(channels[1].fire).toHaveBeenCalledTimes(1)
|
|
974
|
+
expect(channels[0].fire).toHaveBeenCalledTimes(1)
|
|
975
|
+
|
|
976
|
+
emitRecover(channels[1])
|
|
977
|
+
})
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* @param {jest.MockedObject<comq.Channel>} chan
|
|
982
|
+
*/
|
|
983
|
+
function emitRecover (chan) {
|
|
984
|
+
const calls = chan.diagnose.mock.calls.filter((call) => call[0] === 'recover')
|
|
985
|
+
|
|
986
|
+
for (const call of calls) call[1]()
|
|
987
|
+
}
|
package/types/channel.d.ts
CHANGED
|
@@ -25,14 +25,22 @@ declare namespace comq {
|
|
|
25
25
|
/** consumes `queue`, exclusive to this connection and bound to a routed `exchange` under `key` */
|
|
26
26
|
held (exchange: string, queue: string, key: string, consumer: channels.Consumer): Promise<void>
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Over a sharded connection, `via` is told the index of the shard the message is published
|
|
30
|
+
* through, as soon as it is chosen and again whenever the publish fails over to another.
|
|
31
|
+
*/
|
|
32
|
+
send (queue: string, buffer: Buffer, options?: Options.Publish, via?: (index: number) => void): Promise<void>
|
|
29
33
|
|
|
30
34
|
publish (exchange: string, buffer: Buffer, options?: Options.Publish): Promise<void>
|
|
31
35
|
|
|
32
36
|
/** publishes to a routed exchange under `key`, where `publish` fans out */
|
|
33
|
-
route (exchange: string, key: string, buffer: Buffer, options?: Options.Publish): Promise<void>
|
|
37
|
+
route (exchange: string, key: string, buffer: Buffer, options?: Options.Publish, via?: (index: number) => void): Promise<void>
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Over a sharded connection, the message goes back through the shard `origin`, the message it
|
|
41
|
+
* answers, arrived on, while that shard is reachable.
|
|
42
|
+
*/
|
|
43
|
+
fire (queue: string, buffer: Buffer, options?: Options.Publish, origin?: Message): Promise<boolean>
|
|
36
44
|
|
|
37
45
|
seal (): Promise<void>
|
|
38
46
|
|
|
@@ -40,7 +48,8 @@ declare namespace comq {
|
|
|
40
48
|
|
|
41
49
|
readonly closed: boolean
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
/** `reroute` is a sharded channel's own: a returned message is published on another shard */
|
|
52
|
+
diagnose (event: _diagnostics.Event | 'reroute', listener: Function): void
|
|
44
53
|
|
|
45
54
|
forget (event: _diagnostics.Event, listener: Function): void
|
|
46
55
|
|
package/types/io.d.ts
CHANGED
|
@@ -35,6 +35,15 @@ declare namespace comq {
|
|
|
35
35
|
emitter: ReplyEmitter
|
|
36
36
|
properties: _amqp.Properties
|
|
37
37
|
signal?: AbortSignal
|
|
38
|
+
|
|
39
|
+
/** The shard the Request is published through, over a sharded connection. */
|
|
40
|
+
shard?: number
|
|
41
|
+
|
|
42
|
+
/** Whether the Request is being published. */
|
|
43
|
+
publishing?: boolean
|
|
44
|
+
|
|
45
|
+
/** Whether its shard was lost while it was being published, which re-sends it once it is. */
|
|
46
|
+
lost?: boolean
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
/** How long a caller waits for the Reply to a call. */
|