comq 0.4.1-dev.3 → 0.5.0-dev.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/.github/labels.yaml +15 -0
- package/.github/workflows/labels.yaml +18 -0
- package/.github/workflows/release.yaml +20 -8
- package/docker-compose.yaml +20 -2
- package/docs/notes.md +14 -8
- package/examples/const.js +1 -3
- package/examples/events/consumer.js +5 -6
- package/examples/events/producer.js +3 -3
- package/examples/package.json +3 -0
- package/examples/rpc/consumer.js +3 -3
- package/examples/rpc/producer.js +3 -3
- package/features/connection.feature +3 -0
- package/features/events.feature +1 -1
- package/features/shards.feature +61 -0
- package/features/steps/.test/backpressure.test.js +2 -2
- package/features/steps/.test/broker.test.js +32 -6
- package/features/steps/.test/connection.test.js +105 -1
- package/features/steps/.test/context.mock.js +2 -2
- package/features/steps/.test/context.test.js +13 -5
- package/features/steps/.test/events.test.js +66 -3
- package/features/steps/.test/rpc.test.js +99 -1
- package/features/steps/.types/context.d.ts +11 -1
- package/features/steps/backpressure.js +1 -1
- package/features/steps/broker.js +26 -9
- package/features/steps/connection.js +69 -2
- package/features/steps/context.js +28 -12
- package/features/steps/events.js +41 -2
- package/features/steps/rpc.js +58 -1
- package/lerna.json +1 -1
- package/package.json +5 -5
- package/readme.md +75 -25
- package/source/channel.js +56 -32
- package/source/connect.js +28 -2
- package/source/connection.js +24 -17
- package/source/decode.js +1 -1
- package/source/events.js +7 -0
- package/source/io.js +21 -14
- package/source/shards/channel.js +219 -0
- package/source/shards/connection.js +56 -0
- package/source/shards/index.js +5 -0
- package/test/channel.test.js +240 -145
- package/test/connect.test.js +39 -11
- package/test/connection.mock.js +17 -13
- package/test/connection.test.js +65 -12
- package/test/decode.test.js +5 -5
- package/test/io.close.test.js +2 -2
- package/test/io.consume.test.js +1 -1
- package/test/io.reply.test.js +7 -7
- package/test/io.request.test.js +27 -1
- package/test/presets.test.js +1 -1
- package/test/shards/channel.test.js +367 -0
- package/test/shards/connection.test.js +106 -0
- package/types/amqp.d.ts +3 -1
- package/types/channel.d.ts +8 -5
- package/types/connection.d.ts +3 -1
- package/types/diagnostic.d.ts +1 -1
- package/types/topology.d.ts +4 -0
- /package/source/{presets → topology}/event.json +0 -0
- /package/source/{presets → topology}/index.js +0 -0
- /package/source/{presets → topology}/reply.json +0 -0
- /package/source/{presets → topology}/request.json +0 -0
package/test/connection.test.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const { generate } = require('randomstring')
|
|
6
6
|
|
|
7
|
-
const { timeout } = require('@toa.io/generic')
|
|
7
|
+
const { timeout, promex, random } = require('@toa.io/generic')
|
|
8
8
|
const { amqplib } = require('./amqplib.mock')
|
|
9
9
|
const { channel: create } = require('./connection.mock')
|
|
10
10
|
const mock = { amqplib, channel: { create } }
|
|
@@ -12,7 +12,7 @@ const mock = { amqplib, channel: { create } }
|
|
|
12
12
|
jest.mock('amqplib', () => mock.amqplib)
|
|
13
13
|
jest.mock('../source/channel', () => mock.channel)
|
|
14
14
|
|
|
15
|
-
const presets = require('../source/
|
|
15
|
+
const presets = require('../source/topology')
|
|
16
16
|
const { Connection } = require('../source/connection')
|
|
17
17
|
|
|
18
18
|
it('should be', async () => {
|
|
@@ -64,7 +64,7 @@ describe('initial connection', () => {
|
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
describe('reconnection', () => {
|
|
67
|
-
/** @type {jest.MockedObject<
|
|
67
|
+
/** @type {jest.MockedObject<comq.amqp.Connection>} */
|
|
68
68
|
let conn
|
|
69
69
|
|
|
70
70
|
beforeEach(async () => {
|
|
@@ -115,7 +115,7 @@ describe('reconnection', () => {
|
|
|
115
115
|
})
|
|
116
116
|
|
|
117
117
|
describe('create channel', () => {
|
|
118
|
-
/** @type {jest.MockedObject<
|
|
118
|
+
/** @type {jest.MockedObject<comq.amqp.Connection>} */
|
|
119
119
|
let conn
|
|
120
120
|
|
|
121
121
|
beforeEach(async () => {
|
|
@@ -126,7 +126,7 @@ describe('create channel', () => {
|
|
|
126
126
|
|
|
127
127
|
it.each(
|
|
128
128
|
/** @type {comq.topology.type[]} */
|
|
129
|
-
['request', 'reply', 'event'])('should create channel of %s type',
|
|
129
|
+
['request', 'reply', 'event'])('should create failsafe channel of %s type',
|
|
130
130
|
async (type) => {
|
|
131
131
|
// noinspection JSCheckFunctionSignatures
|
|
132
132
|
create.mockImplementationOnce(async () => generate())
|
|
@@ -134,10 +134,19 @@ describe('create channel', () => {
|
|
|
134
134
|
const preset = presets[type]
|
|
135
135
|
const channel = await connection.createChannel(type)
|
|
136
136
|
|
|
137
|
-
expect(create).toHaveBeenCalledWith(conn, preset)
|
|
137
|
+
expect(create).toHaveBeenCalledWith(conn, preset, undefined)
|
|
138
138
|
expect(channel).toStrictEqual(await create.mock.results[0].value)
|
|
139
139
|
})
|
|
140
140
|
|
|
141
|
+
it('should create failfast channel', async () => {
|
|
142
|
+
const type = 'request'
|
|
143
|
+
const index = random()
|
|
144
|
+
|
|
145
|
+
await connection.createChannel(type, index)
|
|
146
|
+
|
|
147
|
+
expect(create).toHaveBeenCalledWith(expect.anything(), expect.anything(), index)
|
|
148
|
+
})
|
|
149
|
+
|
|
141
150
|
it('should create channel after exception', async () => {
|
|
142
151
|
create.mockImplementation(async () => { throw new Error() })
|
|
143
152
|
|
|
@@ -152,6 +161,49 @@ describe('create channel', () => {
|
|
|
152
161
|
|
|
153
162
|
expect(channel).toStrictEqual(await create.mock.results[1].value)
|
|
154
163
|
})
|
|
164
|
+
|
|
165
|
+
it('should wait for initial connection', async () => {
|
|
166
|
+
jest.clearAllMocks()
|
|
167
|
+
expect.assertions(2)
|
|
168
|
+
|
|
169
|
+
connection = new Connection(url)
|
|
170
|
+
|
|
171
|
+
setImmediate(async () => {
|
|
172
|
+
expect(create).not.toHaveBeenCalled()
|
|
173
|
+
|
|
174
|
+
await connection.open()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
await connection.createChannel('request')
|
|
178
|
+
|
|
179
|
+
expect(create).toHaveBeenCalled()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should wait for reconnection', async () => {
|
|
183
|
+
expect.assertions(3)
|
|
184
|
+
|
|
185
|
+
jest.clearAllMocks()
|
|
186
|
+
|
|
187
|
+
const promise = promex()
|
|
188
|
+
|
|
189
|
+
amqplib.connect.mockImplementationOnce(() => promise)
|
|
190
|
+
|
|
191
|
+
conn.emit('close', new Error())
|
|
192
|
+
|
|
193
|
+
expect(amqplib.connect).toHaveBeenCalled()
|
|
194
|
+
|
|
195
|
+
setImmediate(async () => {
|
|
196
|
+
expect(create).not.toHaveBeenCalled()
|
|
197
|
+
|
|
198
|
+
const conn = await amqplib.connect()
|
|
199
|
+
|
|
200
|
+
promise.resolve(conn)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await connection.createChannel('request')
|
|
204
|
+
|
|
205
|
+
expect(create).toHaveBeenCalled()
|
|
206
|
+
})
|
|
155
207
|
})
|
|
156
208
|
|
|
157
209
|
describe('close', () => {
|
|
@@ -164,15 +216,16 @@ describe('close', () => {
|
|
|
164
216
|
expect(amqp.close).toHaveBeenCalled()
|
|
165
217
|
})
|
|
166
218
|
|
|
167
|
-
it('should
|
|
168
|
-
|
|
219
|
+
it('should close after connection is (re)established', async () => {
|
|
220
|
+
// don't wait for completion
|
|
221
|
+
connection.open().then()
|
|
169
222
|
|
|
170
|
-
|
|
171
|
-
const amqp = await amqplib.connect.mock.results[0].value
|
|
223
|
+
await connection.close()
|
|
172
224
|
|
|
173
|
-
|
|
225
|
+
/** @type {jest.MockedObject<comq.amqp.Connection>} */
|
|
226
|
+
const conn = await amqplib.connect.mock.results[0].value
|
|
174
227
|
|
|
175
|
-
|
|
228
|
+
expect(conn.close).toHaveBeenCalled()
|
|
176
229
|
})
|
|
177
230
|
})
|
|
178
231
|
|
package/test/decode.test.js
CHANGED
|
@@ -16,7 +16,7 @@ it('should decode application/json', async () => {
|
|
|
16
16
|
const content = Buffer.from(json)
|
|
17
17
|
const contentType = 'application/json'
|
|
18
18
|
const properties = { contentType }
|
|
19
|
-
const message = /** @type {
|
|
19
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
20
20
|
|
|
21
21
|
const decoded = decode(message)
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ it('should decode application/msgpack', async () => {
|
|
|
28
28
|
const content = pack(object)
|
|
29
29
|
const contentType = 'application/msgpack'
|
|
30
30
|
const properties = { contentType }
|
|
31
|
-
const message = /** @type {
|
|
31
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
32
32
|
|
|
33
33
|
const decoded = decode(message)
|
|
34
34
|
|
|
@@ -40,7 +40,7 @@ it('should return buffer if content type is not defined', async () => {
|
|
|
40
40
|
const json = JSON.stringify(object)
|
|
41
41
|
const content = Buffer.from(json)
|
|
42
42
|
const properties = {}
|
|
43
|
-
const message = /** @type {
|
|
43
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
44
44
|
|
|
45
45
|
const buffer = decode(message)
|
|
46
46
|
const string = buffer.toString()
|
|
@@ -55,7 +55,7 @@ it('should return buffer if content type is application/octet-stream', async ()
|
|
|
55
55
|
const content = Buffer.from(json)
|
|
56
56
|
const contentType = 'application/octet-stream'
|
|
57
57
|
const properties = { contentType }
|
|
58
|
-
const message = /** @type {
|
|
58
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
59
59
|
|
|
60
60
|
const buffer = decode(message)
|
|
61
61
|
const string = buffer.toString()
|
|
@@ -68,7 +68,7 @@ it('should throw if content-type is not supported', async () => {
|
|
|
68
68
|
const content = randomBytes(8)
|
|
69
69
|
const contentType = 'wtf/' + generate()
|
|
70
70
|
const properties = { contentType }
|
|
71
|
-
const message = /** @type {
|
|
71
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
72
72
|
|
|
73
73
|
expect(() => decode(message)).toThrow('is not supported')
|
|
74
74
|
})
|
package/test/io.close.test.js
CHANGED
|
@@ -104,7 +104,7 @@ describe('close', () => {
|
|
|
104
104
|
const callback = channel.subscribe.mock.calls[0][2]
|
|
105
105
|
const content = randomBytes(8)
|
|
106
106
|
const properties = {}
|
|
107
|
-
const message = /** @type {
|
|
107
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
108
108
|
|
|
109
109
|
callback(message)
|
|
110
110
|
|
|
@@ -137,7 +137,7 @@ describe('close', () => {
|
|
|
137
137
|
const callback = channel.consume.mock.calls[0][1]
|
|
138
138
|
const content = generate()
|
|
139
139
|
const properties = { replyTo: generate(), contentType: 'text/plain' }
|
|
140
|
-
const message = /** @type {
|
|
140
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
141
141
|
|
|
142
142
|
callback(message)
|
|
143
143
|
|
package/test/io.consume.test.js
CHANGED
|
@@ -63,7 +63,7 @@ describe('group consumption', () => {
|
|
|
63
63
|
const payload = generate()
|
|
64
64
|
const content = encode(payload, contentType)
|
|
65
65
|
const properties = { contentType }
|
|
66
|
-
const message = /** @type {
|
|
66
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
67
67
|
const callback = events.subscribe.mock.calls[0][2]
|
|
68
68
|
|
|
69
69
|
await callback(message)
|
package/test/io.reply.test.js
CHANGED
|
@@ -74,7 +74,7 @@ describe('queues', () => {
|
|
|
74
74
|
it('should throw if message is missing replyTo', async () => {
|
|
75
75
|
const content = randomBytes(10)
|
|
76
76
|
const properties = {}
|
|
77
|
-
const message = /** @type {
|
|
77
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
78
78
|
const producer = requests.consume.mock.calls[0][1]
|
|
79
79
|
|
|
80
80
|
await expect(producer(message)).rejects.toThrow('is missing the `replyTo` property')
|
|
@@ -99,7 +99,7 @@ describe('reply', () => {
|
|
|
99
99
|
|
|
100
100
|
const content = randomBytes(10)
|
|
101
101
|
const properties = { replyTo: generate(), contentType: 'text/plain' }
|
|
102
|
-
const message = /** @type {
|
|
102
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
103
103
|
const producer = requests.consume.mock.calls[0][1]
|
|
104
104
|
|
|
105
105
|
await producer(message)
|
|
@@ -119,7 +119,7 @@ describe('reply', () => {
|
|
|
119
119
|
const callback = requests.consume.mock.calls[0][1]
|
|
120
120
|
const content = randomBytes(10)
|
|
121
121
|
const properties = { replyTo: generate() }
|
|
122
|
-
const message = /** @type {
|
|
122
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
123
123
|
|
|
124
124
|
await expect(callback(message)).rejects.toThrow('must return a value')
|
|
125
125
|
})
|
|
@@ -143,7 +143,7 @@ describe('encoding', () => {
|
|
|
143
143
|
const value = { [generate()]: generate() }
|
|
144
144
|
const content = encode(value, encoding)
|
|
145
145
|
const properties = { contentType: encoding, replyTo: generate() }
|
|
146
|
-
const message = /** @type {
|
|
146
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
147
147
|
const producer = requests.consume.mock.calls[0][1]
|
|
148
148
|
|
|
149
149
|
await producer(message)
|
|
@@ -161,7 +161,7 @@ describe('encoding', () => {
|
|
|
161
161
|
contentType: encoding
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
const message = /** @type {
|
|
164
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
165
165
|
const producer = requests.consume.mock.calls[0][1]
|
|
166
166
|
|
|
167
167
|
await producer(message)
|
|
@@ -190,7 +190,7 @@ describe('encoding', () => {
|
|
|
190
190
|
contentType: 'application/msgpack'
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
const message = /** @type {
|
|
193
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
194
194
|
const producer = requests.consume.mock.calls[0][1]
|
|
195
195
|
const buffer = randomBytes(8)
|
|
196
196
|
|
|
@@ -214,7 +214,7 @@ describe('encoding', () => {
|
|
|
214
214
|
correlationId: generate()
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
const message = /** @type {
|
|
217
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
218
218
|
const producer = requests.consume.mock.calls[0][1]
|
|
219
219
|
|
|
220
220
|
await expect(producer(message)).rejects.toThrow('must be of type `Buffer`')
|
package/test/io.request.test.js
CHANGED
|
@@ -174,6 +174,32 @@ describe('send', () => {
|
|
|
174
174
|
|
|
175
175
|
expect(requests.send).toHaveBeenCalledTimes(2)
|
|
176
176
|
})
|
|
177
|
+
|
|
178
|
+
it('should resend unanswered Requests on sharded connection', async () => {
|
|
179
|
+
jest.clearAllMocks()
|
|
180
|
+
|
|
181
|
+
connection = mock.connection(true)
|
|
182
|
+
io = new IO(connection)
|
|
183
|
+
|
|
184
|
+
promise = io.request(queue, payload)
|
|
185
|
+
|
|
186
|
+
// allows initializers to run
|
|
187
|
+
await immediate()
|
|
188
|
+
|
|
189
|
+
requests = await findChannel('request')
|
|
190
|
+
|
|
191
|
+
expect(requests.sharded).toStrictEqual(true)
|
|
192
|
+
expect(requests.diagnose).toHaveBeenCalledWith('remove', expect.any(Function))
|
|
193
|
+
|
|
194
|
+
const calls = requests.diagnose.mock.calls.filter((call) => call[0] === 'remove')
|
|
195
|
+
const listeners = calls.map((call) => call[1])
|
|
196
|
+
|
|
197
|
+
for (const listener of listeners) listener()
|
|
198
|
+
|
|
199
|
+
await immediate()
|
|
200
|
+
|
|
201
|
+
expect(requests.send).toHaveBeenCalledTimes(2)
|
|
202
|
+
})
|
|
177
203
|
})
|
|
178
204
|
|
|
179
205
|
describe('reply', () => {
|
|
@@ -210,7 +236,7 @@ const reply = async (content = randomBytes(8), contentType = undefined) => {
|
|
|
210
236
|
const correlationId = requests.send.mock.calls[0][2].correlationId
|
|
211
237
|
const properties = { correlationId, contentType }
|
|
212
238
|
const callback = replies.consume.mock.calls[0][1]
|
|
213
|
-
const message = /** @type {
|
|
239
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
214
240
|
|
|
215
241
|
await callback(message)
|
|
216
242
|
}
|
package/test/presets.test.js
CHANGED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { randomBytes } = require('node:crypto')
|
|
4
|
+
const { generate } = require('randomstring')
|
|
5
|
+
const { promex, sample, immediate, random, each } = require('@toa.io/generic')
|
|
6
|
+
|
|
7
|
+
const mock = require('../connection.mock')
|
|
8
|
+
|
|
9
|
+
const { create } = require('../../source/shards/channel')
|
|
10
|
+
|
|
11
|
+
it('should be', async () => {
|
|
12
|
+
expect(create).toBeInstanceOf(Function)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/** @type {Array<jest.MockedObject<comq.Connection>>} */
|
|
16
|
+
const connections = [mock.connection(), mock.connection()]
|
|
17
|
+
const type = /** @type {comq.topology.type} */ generate()
|
|
18
|
+
|
|
19
|
+
/** @type {comq.Channel} */
|
|
20
|
+
let channel
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should expose `sharded`', async () => {
|
|
27
|
+
channel = await create(connections, type)
|
|
28
|
+
|
|
29
|
+
expect(channel.sharded).toStrictEqual(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should resolve when one of the connections has created a channel', async () => {
|
|
33
|
+
/** @type {toa.generic.Promex[]} */
|
|
34
|
+
const promises = []
|
|
35
|
+
|
|
36
|
+
for (const conn of connections) {
|
|
37
|
+
const promise = promex()
|
|
38
|
+
|
|
39
|
+
conn.createChannel.mockImplementationOnce(() => promise)
|
|
40
|
+
|
|
41
|
+
promises.push(promise)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let any
|
|
45
|
+
const chan = mock.channel()
|
|
46
|
+
|
|
47
|
+
setImmediate(() => {
|
|
48
|
+
any = sample(promises)
|
|
49
|
+
any.resolve(chan)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await create(connections, type)
|
|
53
|
+
await immediate()
|
|
54
|
+
|
|
55
|
+
expect(any).toBeDefined()
|
|
56
|
+
|
|
57
|
+
// resolve pending promises
|
|
58
|
+
for (const promise of promises) if (promise !== any) promise.resolve(chan)
|
|
59
|
+
|
|
60
|
+
await Promise.all(promises)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should create failfast channels of given type', async () => {
|
|
64
|
+
await create(connections, type)
|
|
65
|
+
|
|
66
|
+
each(connections, (conn, index) => {
|
|
67
|
+
expect(conn.createChannel).toHaveBeenCalledWith(type, index)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe.each(['consume', 'subscribe'])('%s', (method) => {
|
|
72
|
+
const exchange = generate()
|
|
73
|
+
const queue = generate()
|
|
74
|
+
const group = generate()
|
|
75
|
+
const processor = jest.fn()
|
|
76
|
+
const args = method === 'consume' ? [queue, processor] : [exchange, group, processor]
|
|
77
|
+
|
|
78
|
+
it(`should ${method} using all channels`, async () => {
|
|
79
|
+
channel = await create(connections, type)
|
|
80
|
+
|
|
81
|
+
const channels = await getCreatedChannels()
|
|
82
|
+
|
|
83
|
+
expect(channels.length).toStrictEqual(2)
|
|
84
|
+
|
|
85
|
+
await channel[method](...args)
|
|
86
|
+
|
|
87
|
+
for (const chan of channels) expect(chan[method]).toHaveBeenCalledWith(...args)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it(`should should ${method} using available and pending channels`, async () => {
|
|
91
|
+
const promise = promex()
|
|
92
|
+
|
|
93
|
+
connections[0].createChannel.mockImplementationOnce(() => promise)
|
|
94
|
+
|
|
95
|
+
channel = await create(connections, type)
|
|
96
|
+
|
|
97
|
+
await channel[method](...args)
|
|
98
|
+
|
|
99
|
+
/** @type {jest.MockedObject<comq.Channel>} */
|
|
100
|
+
const chan1 = await connections[1].createChannel.mock.results[0].value
|
|
101
|
+
|
|
102
|
+
expect(chan1[method]).toHaveBeenCalled()
|
|
103
|
+
|
|
104
|
+
/** @type {jest.MockedObject<comq.Channel>} */
|
|
105
|
+
const chan0 = mock.channel()
|
|
106
|
+
|
|
107
|
+
promise.resolve(chan0)
|
|
108
|
+
|
|
109
|
+
await immediate()
|
|
110
|
+
|
|
111
|
+
expect(chan0[method]).toHaveBeenCalled()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it(`should ${method} using a channel removed from the pool once it is recovered`, async () => {
|
|
115
|
+
channel = await create(connections, type)
|
|
116
|
+
|
|
117
|
+
const channels = await getCreatedChannels()
|
|
118
|
+
const chan = sample(channels)
|
|
119
|
+
const buffer = randomBytes(8)
|
|
120
|
+
const fail = /** @type {jest.MockedFunction} */ jest.fn(async () => { throw new Error() })
|
|
121
|
+
|
|
122
|
+
chan.publish.mockImplementationOnce(fail)
|
|
123
|
+
|
|
124
|
+
// `chan` has failed and removed from the pool
|
|
125
|
+
while (fail.mock.calls.length === 0) await channel.publish(exchange, buffer)
|
|
126
|
+
|
|
127
|
+
await channel[method](...args)
|
|
128
|
+
|
|
129
|
+
expect(chan[method]).not.toHaveBeenCalled()
|
|
130
|
+
|
|
131
|
+
const calls = chan.diagnose.mock.calls.filter((call) => call[0] === 'recover')
|
|
132
|
+
const listeners = calls.map((call) => call[1])
|
|
133
|
+
|
|
134
|
+
// emit `recover` event
|
|
135
|
+
for (const listener of listeners) await listener()
|
|
136
|
+
|
|
137
|
+
await immediate()
|
|
138
|
+
|
|
139
|
+
expect(chan[method]).toHaveBeenCalled()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe.each(/** @type {string[]} */['send', 'publish', 'throw'])('%s', (method) => {
|
|
144
|
+
const label = generate()
|
|
145
|
+
const buffer = randomBytes(8)
|
|
146
|
+
const options = { contentType: generate() }
|
|
147
|
+
|
|
148
|
+
/** @type {jest.MockedObject<comq.Channel>[]} */
|
|
149
|
+
let channels
|
|
150
|
+
|
|
151
|
+
beforeEach(async () => {
|
|
152
|
+
channel = await create(connections, type)
|
|
153
|
+
channels = await getCreatedChannels()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should send to single channel', async () => {
|
|
157
|
+
await channel[method](label, buffer, options)
|
|
158
|
+
|
|
159
|
+
let used = 0
|
|
160
|
+
|
|
161
|
+
for (const chan of channels) {
|
|
162
|
+
if (chan[method].mock.calls.length === 0) continue
|
|
163
|
+
|
|
164
|
+
used++
|
|
165
|
+
|
|
166
|
+
expect(chan[method]).toHaveBeenCalledWith(label, buffer, options)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
expect(used).toStrictEqual(1)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should route pending messages among remaining shards', async () => {
|
|
173
|
+
const b = random(channels.length)
|
|
174
|
+
const broken = channels[b]
|
|
175
|
+
const options = { contentType: generate() }
|
|
176
|
+
let thrown = false
|
|
177
|
+
|
|
178
|
+
broken[method].mockImplementationOnce(async () => {
|
|
179
|
+
thrown = true
|
|
180
|
+
throw new Error()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
let i = 0
|
|
184
|
+
|
|
185
|
+
// eslint-disable-next-line no-unmodified-loop-condition
|
|
186
|
+
while (!thrown) await channel[method](label + ++i, buffer, options)
|
|
187
|
+
|
|
188
|
+
const last = label + i
|
|
189
|
+
|
|
190
|
+
// now channel has thrown an exception
|
|
191
|
+
expect(broken[method]).toHaveBeenCalledWith(last, buffer, options)
|
|
192
|
+
|
|
193
|
+
const r = (b + 1) % 2
|
|
194
|
+
const remaining = channels[r]
|
|
195
|
+
|
|
196
|
+
expect(remaining[method]).toHaveBeenCalledWith(last, buffer, options)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should wait for recovery', async () => {
|
|
200
|
+
const reject = async () => { throw new Error() }
|
|
201
|
+
|
|
202
|
+
/** @type {jest.MockedObject<comq.Channel>} */
|
|
203
|
+
let recovered
|
|
204
|
+
|
|
205
|
+
// both shards rejects and will be removed from the pool
|
|
206
|
+
channels[0][method].mockImplementationOnce(reject)
|
|
207
|
+
channels[1][method].mockImplementationOnce(reject)
|
|
208
|
+
|
|
209
|
+
setImmediate(() => {
|
|
210
|
+
const r = random(channels.length)
|
|
211
|
+
|
|
212
|
+
recovered = channels[r]
|
|
213
|
+
|
|
214
|
+
// subscriptions to the 'recover' event
|
|
215
|
+
const calls = recovered.diagnose.mock.calls.filter(
|
|
216
|
+
(call) => call[0] === 'recover')
|
|
217
|
+
|
|
218
|
+
expect(calls.length).toBeGreaterThan(0)
|
|
219
|
+
|
|
220
|
+
// emit 'recover' event to return shard to the pool
|
|
221
|
+
for (const call of calls) call[1]()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
await channel[method](label, buffer, options)
|
|
225
|
+
|
|
226
|
+
expect(recovered).toBeDefined()
|
|
227
|
+
expect(recovered[method]).toHaveBeenCalledTimes(2) // failed and succeeded
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('seal', () => {
|
|
232
|
+
it('should seal channels', async () => {
|
|
233
|
+
channel = await create(connections, type)
|
|
234
|
+
|
|
235
|
+
await channel.seal()
|
|
236
|
+
|
|
237
|
+
const channels = await getCreatedChannels()
|
|
238
|
+
|
|
239
|
+
for (const chan of channels) expect(chan.seal).toHaveBeenCalled()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('should seal channels eventually', async () => {
|
|
243
|
+
expect.assertions(connections.length + 1)
|
|
244
|
+
|
|
245
|
+
channel = await create(connections, type)
|
|
246
|
+
|
|
247
|
+
const channels = await getCreatedChannels()
|
|
248
|
+
const chan = sample(channels)
|
|
249
|
+
const promise = promex()
|
|
250
|
+
let sealed = false
|
|
251
|
+
|
|
252
|
+
chan.seal.mockImplementationOnce(() => promise)
|
|
253
|
+
|
|
254
|
+
setImmediate(() => {
|
|
255
|
+
expect(sealed).toStrictEqual(false)
|
|
256
|
+
|
|
257
|
+
promise.resolve()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
await channel.seal()
|
|
261
|
+
|
|
262
|
+
sealed = true
|
|
263
|
+
|
|
264
|
+
for (const chan of channels) expect(chan.seal).toHaveBeenCalled()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should seal pending channel', async () => {
|
|
268
|
+
const connection = sample(connections)
|
|
269
|
+
const promise = promex()
|
|
270
|
+
|
|
271
|
+
connection.createChannel.mockImplementationOnce(() => promise)
|
|
272
|
+
|
|
273
|
+
channel = await create(connections, type)
|
|
274
|
+
|
|
275
|
+
/** @type {jest.MockedObject<comq.Channel>} */
|
|
276
|
+
let chan
|
|
277
|
+
|
|
278
|
+
setImmediate(() => {
|
|
279
|
+
chan = mock.channel()
|
|
280
|
+
|
|
281
|
+
promise.resolve(chan)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
await channel.seal()
|
|
285
|
+
|
|
286
|
+
expect(chan).toBeDefined()
|
|
287
|
+
expect(chan.seal).toHaveBeenCalled()
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe('diagnose', () => {
|
|
292
|
+
const events = /** @type {comq.diagnostics.event[]} */ ['flow', 'drain', 'recover', 'discard']
|
|
293
|
+
|
|
294
|
+
/** @type {jest.MockedObject<comq.Channel>[]} */
|
|
295
|
+
let channels
|
|
296
|
+
|
|
297
|
+
beforeEach(async () => {
|
|
298
|
+
channel = await create(connections, type)
|
|
299
|
+
channels = await getCreatedChannels()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it.each(events)('should re-emit `%s` event',
|
|
303
|
+
async (event) => {
|
|
304
|
+
const listener = /** @type {Function} */ jest.fn()
|
|
305
|
+
const index = random(channels.length)
|
|
306
|
+
const chan = channels[index]
|
|
307
|
+
|
|
308
|
+
channel.diagnose(event, listener)
|
|
309
|
+
|
|
310
|
+
expect(chan.diagnose).toHaveBeenCalledWith(event, expect.any(Function))
|
|
311
|
+
|
|
312
|
+
const call = chan.diagnose.mock.calls.find((call) => call[0] === event)
|
|
313
|
+
const emit = call[1]
|
|
314
|
+
const args = [generate(), generate()]
|
|
315
|
+
|
|
316
|
+
emit(...args)
|
|
317
|
+
|
|
318
|
+
expect(listener).toHaveBeenCalledWith(...args, index)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('should emit `remove` event', async () => {
|
|
322
|
+
const queue = generate()
|
|
323
|
+
const buffer = randomBytes(8)
|
|
324
|
+
const chan = sample(channels)
|
|
325
|
+
const listener = /** @type {jest.MockedFunction} */ jest.fn()
|
|
326
|
+
|
|
327
|
+
let thrown = false
|
|
328
|
+
|
|
329
|
+
channel.diagnose('remove', listener)
|
|
330
|
+
|
|
331
|
+
chan.send.mockImplementationOnce(() => {
|
|
332
|
+
thrown = true
|
|
333
|
+
throw new Error()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// eslint-disable-next-line no-unmodified-loop-condition
|
|
337
|
+
while (!thrown) await channel.send(queue, buffer)
|
|
338
|
+
|
|
339
|
+
expect(listener).toHaveBeenCalledWith(expect.any(Number))
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('should not throw on recovery (if the "bench" is empty)', async () => {
|
|
344
|
+
channel = await create(connections, type)
|
|
345
|
+
|
|
346
|
+
const channels = await getCreatedChannels()
|
|
347
|
+
const chan = sample(channels)
|
|
348
|
+
|
|
349
|
+
expect(chan.diagnose).toHaveBeenCalledWith('recover', expect.any(Function))
|
|
350
|
+
|
|
351
|
+
const calls = chan.diagnose.mock.calls.filter((call) => call[0] === 'recover')
|
|
352
|
+
const listeners = calls.map((call) => call[1])
|
|
353
|
+
|
|
354
|
+
// emit 'recover'
|
|
355
|
+
const emit = () => { for (const listener of listeners) listener() }
|
|
356
|
+
|
|
357
|
+
expect(emit).not.toThrow()
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @return {Promise<jest.MockedObject<comq.Channel>[]>}
|
|
362
|
+
*/
|
|
363
|
+
async function getCreatedChannels () {
|
|
364
|
+
const promises = connections.map((connection) => connection.createChannel.mock.results[0].value)
|
|
365
|
+
|
|
366
|
+
return await Promise.all(promises)
|
|
367
|
+
}
|