comq 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/discussions/amqp-1.0.md +127 -0
- package/features/heartbeat.feature +16 -0
- package/features/rpc.call.feature +23 -1
- package/features/rpc.call.recovery.feature +2 -23
- package/features/rpc.parked.feature +6 -6
- package/features/rpc.shards.retransmission.feature +46 -0
- package/features/shards.feature +1 -1
- package/features/silence.feature +3 -2
- package/features/steps/.types/context.d.ts +6 -1
- package/features/steps/brokers.js +1 -1
- package/features/steps/call.js +90 -30
- package/features/steps/connection.js +23 -0
- package/features/steps/context.js +1 -2
- package/features/steps/events.js +1 -1
- package/features/steps/hooks.js +0 -1
- package/features/steps/network.js +11 -2
- package/features/steps/networks.js +31 -2
- package/features/steps/parking.js +5 -5
- package/features/steps/retransmission.js +165 -0
- package/features/steps/rpc.js +0 -6
- package/package.json +20 -4
- package/readme.md +46 -46
- package/source/channel.js +3 -1
- package/source/connection.js +10 -90
- package/source/io.js +106 -25
- package/source/shards/channel.js +73 -12
- package/test/amqplib.mock.js +1 -1
- package/test/connection.mock.js +3 -2
- package/test/connection.test.js +16 -78
- package/test/{io.request.terms.test.js → io.call.terms.test.js} +31 -30
- package/test/io.call.test.js +5 -13
- package/test/io.reply.test.js +5 -3
- package/test/io.request.test.js +277 -2
- package/test/memory.test.js +1 -3
- package/test/shards/channel.test.js +168 -2
- package/types/amqp.d.ts +1 -1
- package/types/channel.d.ts +14 -5
- package/types/io.d.ts +18 -8
- package/features/rpc.timeout.feature +0 -19
- package/source/batch.js +0 -62
- package/test/batch.test.js +0 -94
|
@@ -21,7 +21,8 @@ let requests
|
|
|
21
21
|
/** @type {jest.MockedObject<comq.Channel>} */
|
|
22
22
|
let replies
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const exchange = generate()
|
|
25
|
+
const key = generate()
|
|
25
26
|
const payload = { [generate()]: generate() }
|
|
26
27
|
|
|
27
28
|
beforeEach(async () => {
|
|
@@ -34,12 +35,12 @@ beforeEach(async () => {
|
|
|
34
35
|
// endregion
|
|
35
36
|
|
|
36
37
|
describe('timeout', () => {
|
|
37
|
-
it('should expire the
|
|
38
|
-
io.
|
|
38
|
+
it('should expire the call when the caller stops waiting', async () => {
|
|
39
|
+
io.call(exchange, key, payload, { timeout: 10000 }).catch(noop)
|
|
39
40
|
|
|
40
41
|
await initialized()
|
|
41
42
|
|
|
42
|
-
const expiration = Number(requests.
|
|
43
|
+
const expiration = Number(requests.route.mock.calls[0][3].expiration)
|
|
43
44
|
|
|
44
45
|
expect(expiration).toBeGreaterThan(9000)
|
|
45
46
|
expect(expiration).toBeLessThanOrEqual(10000)
|
|
@@ -48,29 +49,29 @@ describe('timeout', () => {
|
|
|
48
49
|
it('should keep the encoding', async () => {
|
|
49
50
|
const encoding = 'application/octet-stream'
|
|
50
51
|
|
|
51
|
-
io.
|
|
52
|
+
io.call(exchange, key, randomBytes(8), { encoding, timeout: 10000 }).catch(noop)
|
|
52
53
|
|
|
53
54
|
await initialized()
|
|
54
55
|
|
|
55
|
-
expect(requests.
|
|
56
|
+
expect(requests.route.mock.calls[0][3].contentType).toStrictEqual(encoding)
|
|
56
57
|
})
|
|
57
58
|
|
|
58
59
|
it('should stop waiting while the channels are being created', async () => {
|
|
59
60
|
connection.createChannel.mockImplementation(() => new Promise(noop))
|
|
60
61
|
|
|
61
|
-
const promise = io.
|
|
62
|
+
const promise = io.call(exchange, key, payload, { timeout: 20 })
|
|
62
63
|
|
|
63
64
|
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
64
65
|
})
|
|
65
66
|
|
|
66
67
|
it('should reject once it has passed', async () => {
|
|
67
|
-
const promise = io.
|
|
68
|
+
const promise = io.call(exchange, key, payload, { timeout: 20 })
|
|
68
69
|
|
|
69
70
|
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
70
71
|
})
|
|
71
72
|
|
|
72
73
|
it('should resolve with a reply that arrives in time', async () => {
|
|
73
|
-
const promise = io.
|
|
74
|
+
const promise = io.call(exchange, key, payload, { timeout: 10000 })
|
|
74
75
|
const content = randomBytes(8)
|
|
75
76
|
|
|
76
77
|
await initialized()
|
|
@@ -80,7 +81,7 @@ describe('timeout', () => {
|
|
|
80
81
|
})
|
|
81
82
|
|
|
82
83
|
it('should re-send with the time that is left', async () => {
|
|
83
|
-
io.
|
|
84
|
+
io.call(exchange, key, payload, { timeout: 10000 }).catch(noop)
|
|
84
85
|
|
|
85
86
|
await initialized()
|
|
86
87
|
await timeout(20)
|
|
@@ -89,10 +90,10 @@ describe('timeout', () => {
|
|
|
89
90
|
|
|
90
91
|
await immediate()
|
|
91
92
|
|
|
92
|
-
expect(requests.
|
|
93
|
+
expect(requests.route).toHaveBeenCalledTimes(2)
|
|
93
94
|
|
|
94
|
-
const first = Number(requests.
|
|
95
|
-
const second = Number(requests.
|
|
95
|
+
const first = Number(requests.route.mock.calls[0][3].expiration)
|
|
96
|
+
const second = Number(requests.route.mock.calls[1][3].expiration)
|
|
96
97
|
|
|
97
98
|
expect(second).toBeLessThan(first)
|
|
98
99
|
})
|
|
@@ -102,7 +103,7 @@ describe('signal', () => {
|
|
|
102
103
|
it('should reject with the reason it was aborted for', async () => {
|
|
103
104
|
const controller = new AbortController()
|
|
104
105
|
const reason = new Error(generate())
|
|
105
|
-
const promise = io.
|
|
106
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal })
|
|
106
107
|
|
|
107
108
|
await initialized()
|
|
108
109
|
|
|
@@ -111,14 +112,14 @@ describe('signal', () => {
|
|
|
111
112
|
await expect(promise).rejects.toBe(reason)
|
|
112
113
|
})
|
|
113
114
|
|
|
114
|
-
it('should leave the
|
|
115
|
+
it('should leave the call in its queue', async () => {
|
|
115
116
|
const controller = new AbortController()
|
|
116
117
|
|
|
117
|
-
io.
|
|
118
|
+
io.call(exchange, key, payload, { signal: controller.signal }).catch(noop)
|
|
118
119
|
|
|
119
120
|
await initialized()
|
|
120
121
|
|
|
121
|
-
expect(requests.
|
|
122
|
+
expect(requests.route.mock.calls[0][3].expiration).toBeUndefined()
|
|
122
123
|
})
|
|
123
124
|
|
|
124
125
|
it('should reject at once when aborted before the channels exist', async () => {
|
|
@@ -129,27 +130,27 @@ describe('signal', () => {
|
|
|
129
130
|
|
|
130
131
|
controller.abort(reason)
|
|
131
132
|
|
|
132
|
-
await expect(io.
|
|
133
|
+
await expect(io.call(exchange, key, payload, { signal: controller.signal })).rejects.toBe(reason)
|
|
133
134
|
})
|
|
134
135
|
|
|
135
136
|
it('should send nothing once aborted', async () => {
|
|
136
137
|
const controller = new AbortController()
|
|
137
138
|
|
|
138
|
-
// the channels exist before the aborted
|
|
139
|
-
io.
|
|
139
|
+
// the channels exist before the aborted call is made
|
|
140
|
+
io.call(exchange, key, payload).catch(noop)
|
|
140
141
|
|
|
141
142
|
await initialized()
|
|
142
143
|
|
|
143
144
|
controller.abort()
|
|
144
145
|
|
|
145
|
-
await expect(io.
|
|
146
|
+
await expect(io.call(exchange, key, payload, { signal: controller.signal })).rejects.toBeDefined()
|
|
146
147
|
|
|
147
|
-
expect(requests.
|
|
148
|
+
expect(requests.route).toHaveBeenCalledTimes(1)
|
|
148
149
|
})
|
|
149
150
|
|
|
150
151
|
it('should not re-send what it abandoned', async () => {
|
|
151
152
|
const controller = new AbortController()
|
|
152
|
-
const promise = io.
|
|
153
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal })
|
|
153
154
|
|
|
154
155
|
await initialized()
|
|
155
156
|
|
|
@@ -161,19 +162,19 @@ describe('signal', () => {
|
|
|
161
162
|
|
|
162
163
|
await immediate()
|
|
163
164
|
|
|
164
|
-
expect(requests.
|
|
165
|
+
expect(requests.route).toHaveBeenCalledTimes(1)
|
|
165
166
|
})
|
|
166
167
|
|
|
167
168
|
it('should stop waiting for a publication', async () => {
|
|
168
|
-
io.
|
|
169
|
+
io.call(exchange, key, payload).catch(noop)
|
|
169
170
|
|
|
170
171
|
await initialized()
|
|
171
172
|
|
|
172
|
-
requests.
|
|
173
|
+
requests.route.mockImplementationOnce(() => new Promise(noop))
|
|
173
174
|
|
|
174
175
|
const controller = new AbortController()
|
|
175
176
|
const reason = new Error(generate())
|
|
176
|
-
const promise = io.
|
|
177
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal })
|
|
177
178
|
|
|
178
179
|
await immediate()
|
|
179
180
|
|
|
@@ -184,7 +185,7 @@ describe('signal', () => {
|
|
|
184
185
|
|
|
185
186
|
it('should discard a reply that arrives afterwards', async () => {
|
|
186
187
|
const controller = new AbortController()
|
|
187
|
-
const promise = io.
|
|
188
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal })
|
|
188
189
|
|
|
189
190
|
await initialized()
|
|
190
191
|
|
|
@@ -197,7 +198,7 @@ describe('signal', () => {
|
|
|
197
198
|
|
|
198
199
|
it('should end the wait within the timeout', async () => {
|
|
199
200
|
const controller = new AbortController()
|
|
200
|
-
const promise = io.
|
|
201
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal, timeout: 20 })
|
|
201
202
|
|
|
202
203
|
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
203
204
|
})
|
|
@@ -216,7 +217,7 @@ async function initialized () {
|
|
|
216
217
|
* @param {Buffer} [content]
|
|
217
218
|
*/
|
|
218
219
|
async function answer (index, content = randomBytes(8)) {
|
|
219
|
-
const { correlationId } = requests.
|
|
220
|
+
const { correlationId } = requests.route.mock.calls[index][3]
|
|
220
221
|
const deliver = replies.consume.mock.calls[0][1]
|
|
221
222
|
|
|
222
223
|
await deliver({ content, properties: { correlationId } })
|
package/test/io.call.test.js
CHANGED
|
@@ -52,7 +52,7 @@ describe('call', () => {
|
|
|
52
52
|
|
|
53
53
|
it('should publish to the exchange under the key', async () => {
|
|
54
54
|
expect(requests.route).toHaveBeenCalledWith(exchange, key, expect.any(Buffer),
|
|
55
|
-
expect.objectContaining({ mandatory: true }))
|
|
55
|
+
expect.objectContaining({ mandatory: true }), expect.any(Function))
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
it('should declare no queue to publish to', async () => {
|
|
@@ -98,16 +98,6 @@ describe('call', () => {
|
|
|
98
98
|
})
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
-
describe('timeout', () => {
|
|
102
|
-
it('should stop waiting while the channels are being created', async () => {
|
|
103
|
-
connection.createChannel.mockImplementation(() => new Promise(() => undefined))
|
|
104
|
-
|
|
105
|
-
const promise = io.call(exchange, key, payload, { timeout: 20 })
|
|
106
|
-
|
|
107
|
-
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
101
|
describe('back', () => {
|
|
112
102
|
const producer = jest.fn(() => generate())
|
|
113
103
|
|
|
@@ -126,14 +116,16 @@ describe('back', () => {
|
|
|
126
116
|
const contentType = 'application/json'
|
|
127
117
|
const properties = { contentType, correlationId: generate(), replyTo: generate() }
|
|
128
118
|
|
|
129
|
-
|
|
119
|
+
const request = { content: encode(payload, contentType), properties }
|
|
120
|
+
|
|
121
|
+
await consumer(request)
|
|
130
122
|
|
|
131
123
|
replies = await findChannel('reply')
|
|
132
124
|
|
|
133
125
|
expect(producer).toHaveBeenCalledWith(payload)
|
|
134
126
|
|
|
135
127
|
expect(replies.fire).toHaveBeenCalledWith(properties.replyTo, expect.any(Buffer),
|
|
136
|
-
expect.objectContaining({ correlationId: properties.correlationId }))
|
|
128
|
+
expect.objectContaining({ correlationId: properties.correlationId }), request)
|
|
137
129
|
})
|
|
138
130
|
})
|
|
139
131
|
|
package/test/io.reply.test.js
CHANGED
|
@@ -98,7 +98,8 @@ describe('reply', () => {
|
|
|
98
98
|
const reply = await produce.mock.results[0].value
|
|
99
99
|
const buffer = encode(reply, properties.contentType)
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// the request goes along, so that the reply goes back the way it came
|
|
102
|
+
expect(replies.fire).toHaveBeenCalledWith(properties.replyTo, buffer, expect.anything(), message)
|
|
102
103
|
})
|
|
103
104
|
|
|
104
105
|
it('should throw if producer returned undefined', async () => {
|
|
@@ -190,7 +191,7 @@ describe('encoding', () => {
|
|
|
190
191
|
mandatory: true
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
expect(replies.fire).toHaveBeenCalledWith(properties.replyTo, reply, props)
|
|
194
|
+
expect(replies.fire).toHaveBeenCalledWith(properties.replyTo, reply, props, message)
|
|
194
195
|
})
|
|
195
196
|
|
|
196
197
|
it('should set octet-stream for Buffer reply', async () => {
|
|
@@ -215,7 +216,8 @@ describe('encoding', () => {
|
|
|
215
216
|
.toHaveBeenCalledWith(
|
|
216
217
|
expect.any(String),
|
|
217
218
|
buffer,
|
|
218
|
-
expect.objectContaining({ contentType: 'application/octet-stream' })
|
|
219
|
+
expect.objectContaining({ contentType: 'application/octet-stream' }),
|
|
220
|
+
message
|
|
219
221
|
)
|
|
220
222
|
})
|
|
221
223
|
|
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
|
|
|
@@ -115,6 +116,19 @@ describe('send', () => {
|
|
|
115
116
|
expect(call[2]).toMatchObject({ contentType })
|
|
116
117
|
})
|
|
117
118
|
|
|
119
|
+
it('should not expire', async () => {
|
|
120
|
+
expect(call[2].expiration).toBeUndefined()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should refuse options other than the encoding', async () => {
|
|
124
|
+
requests.send.mockClear()
|
|
125
|
+
|
|
126
|
+
await expect(io.request(queue, payload, { timeout: 1000 })).rejects.toBeInstanceOf(TypeError)
|
|
127
|
+
await expect(io.request(queue, payload, { signal: new AbortController().signal })).rejects.toBeInstanceOf(TypeError)
|
|
128
|
+
|
|
129
|
+
expect(requests.send).not.toHaveBeenCalled()
|
|
130
|
+
})
|
|
131
|
+
|
|
118
132
|
it('should throw if encoding is not supported', async () => {
|
|
119
133
|
const encoding = /** @type {comq.Encoding} */ 'wtf/' + generate()
|
|
120
134
|
|
|
@@ -314,15 +328,276 @@ describe('send', () => {
|
|
|
314
328
|
})
|
|
315
329
|
})
|
|
316
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
|
+
|
|
317
591
|
/**
|
|
318
592
|
* Fires the listeners the IO has attached to the shard loss diagnostic.
|
|
319
593
|
*
|
|
594
|
+
* @param {number} [index]
|
|
320
595
|
* @return {Promise<void>}
|
|
321
596
|
*/
|
|
322
|
-
async function lose () {
|
|
597
|
+
async function lose (index = 0) {
|
|
323
598
|
const calls = requests.diagnose.mock.calls.filter((call) => call[0] === 'lost')
|
|
324
599
|
|
|
325
|
-
for (const [, listener] of calls) listener(
|
|
600
|
+
for (const [, listener] of calls) listener(index)
|
|
326
601
|
|
|
327
602
|
await immediate()
|
|
328
603
|
await immediate()
|
package/test/memory.test.js
CHANGED
|
@@ -12,7 +12,7 @@ const { timeout } = require('./helpers')
|
|
|
12
12
|
const mock = require('./amqplib.mock')
|
|
13
13
|
const { amqplib, connect } = mock
|
|
14
14
|
|
|
15
|
-
jest.mock('amqplib', () => mock.amqplib)
|
|
15
|
+
jest.mock('@toa.io/amqplib', () => mock.amqplib)
|
|
16
16
|
|
|
17
17
|
const { connect: open, assert } = require('../source')
|
|
18
18
|
const { SingletonConnection } = require('../source/singleton')
|
|
@@ -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(() => {
|