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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { AssertionError } = require('node:assert')
|
|
4
4
|
const { generate } = require('randomstring')
|
|
5
|
-
const { promex } = require('@toa.io/generic')
|
|
5
|
+
const { promex, timeout, random } = require('@toa.io/generic')
|
|
6
6
|
const tomato = require('@toa.io/tomato')
|
|
7
7
|
const { io } = require('./io.mock')
|
|
8
8
|
const mock = { tomato }
|
|
@@ -57,8 +57,8 @@ describe('Given {token} consuming events from the {token} exchange is expected',
|
|
|
57
57
|
it('should be', async () => undefined)
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
describe('Given that events are exclusively consumed from the {token} exchange', () => {
|
|
61
|
-
tomato.steps.Gi('that events are exclusively consumed from the {token} exchange')
|
|
60
|
+
describe('Given (that )events are exclusively consumed from the {token} exchange', () => {
|
|
61
|
+
tomato.steps.Gi('(that )events are exclusively consumed from the {token} exchange')
|
|
62
62
|
|
|
63
63
|
it('should be', async () => undefined)
|
|
64
64
|
})
|
|
@@ -152,3 +152,66 @@ describe('Then the event is received', () => {
|
|
|
152
152
|
await expect(step.call(context, group)).resolves.not.toThrow()
|
|
153
153
|
})
|
|
154
154
|
})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
describe('Given I\'m publishing {quantity}B events to the {token} exchange at {quantity}Hz', () => {
|
|
158
|
+
const step = tomato.steps.Gi('I\'m publishing {quantity}B events to the {token} exchange at {quantity}Hz')
|
|
159
|
+
|
|
160
|
+
it('should be', async () => undefined)
|
|
161
|
+
|
|
162
|
+
it('should publish events continuously', async () => {
|
|
163
|
+
context = /** @type {comq.features.Context} */ {
|
|
164
|
+
io: /** @type {jest.MockedObject<comq.IO>} */ { emit: jest.fn() },
|
|
165
|
+
requestsSent: [],
|
|
166
|
+
eventsPublishedCount: 0
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const bytesQ = '1k'
|
|
170
|
+
const exchange = generate()
|
|
171
|
+
const frequencyQ = '100'
|
|
172
|
+
|
|
173
|
+
step.call(context, bytesQ, exchange, frequencyQ)
|
|
174
|
+
|
|
175
|
+
await timeout(100 + 11)
|
|
176
|
+
|
|
177
|
+
expect(context.io.emit).toHaveBeenCalledTimes(10)
|
|
178
|
+
expect(context.publishing).toBeDefined()
|
|
179
|
+
expect(context.eventsPublishedCount).toStrictEqual(10)
|
|
180
|
+
|
|
181
|
+
clearInterval(context.publishing)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('Then all events have been received', () => {
|
|
186
|
+
const step = tomato.steps.Th('all events have been received')
|
|
187
|
+
|
|
188
|
+
/** @type {jest.MockedObject<comq.features.Context>} */
|
|
189
|
+
let context
|
|
190
|
+
|
|
191
|
+
it('should be', async () => undefined)
|
|
192
|
+
|
|
193
|
+
beforeEach(async () => {
|
|
194
|
+
context = /** @type {jest.MockedObject<comq.features.Context>} */ {
|
|
195
|
+
eventsPublishedCount: random(100),
|
|
196
|
+
eventsConsumedCount: 100 + random()
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should pass if events received', async () => {
|
|
201
|
+
context.eventsConsumedCount = context.eventsPublishedCount
|
|
202
|
+
|
|
203
|
+
await expect(step.call(context)).resolves.not.toThrow()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should fail if events are not received', async () => {
|
|
207
|
+
await expect(step.call(context)).rejects.toThrow(AssertionError)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('should wait for last events (50ms)', async () => {
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
context.eventsConsumedCount = context.eventsPublishedCount
|
|
213
|
+
}, 49)
|
|
214
|
+
|
|
215
|
+
await expect(step.call(context)).resolves.not.toThrow()
|
|
216
|
+
})
|
|
217
|
+
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { AssertionError } = require('node:assert')
|
|
4
4
|
const { generate } = require('randomstring')
|
|
5
|
-
const { promex } = require('@toa.io/generic')
|
|
5
|
+
const { promex, timeout } = require('@toa.io/generic')
|
|
6
6
|
const { dump } = require('@toa.io/yaml')
|
|
7
7
|
const tomato = require('@toa.io/tomato')
|
|
8
8
|
const { io } = require('./io.mock')
|
|
@@ -42,6 +42,24 @@ describe('Given function replying {token} queue:', () => {
|
|
|
42
42
|
})
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
+
describe('Given a producer replying {token} queue', () => {
|
|
46
|
+
const step = tomato.steps.Gi('a producer replying {token} queue')
|
|
47
|
+
|
|
48
|
+
it('should be', async () => undefined)
|
|
49
|
+
|
|
50
|
+
it('should reply', async () => {
|
|
51
|
+
await step.call(context, queue)
|
|
52
|
+
|
|
53
|
+
expect(io.reply).toHaveBeenCalledWith(queue, expect.any(Function))
|
|
54
|
+
|
|
55
|
+
const producer = io.reply.mock.calls[0][1]
|
|
56
|
+
const message = generate()
|
|
57
|
+
const reply = await producer(message)
|
|
58
|
+
|
|
59
|
+
expect(reply).toStrictEqual(message)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
45
63
|
describe('Given function replying {token} queue is expected:', () => {
|
|
46
64
|
tomato.steps.Gi('function replying {token} queue is expected:')
|
|
47
65
|
|
|
@@ -140,6 +158,30 @@ describe('Then the consumer receives the reply:', () => {
|
|
|
140
158
|
})
|
|
141
159
|
})
|
|
142
160
|
|
|
161
|
+
describe('Then the consumer receives the reply', () => {
|
|
162
|
+
const step = tomato.steps.Th('the consumer receives the reply')
|
|
163
|
+
|
|
164
|
+
it('should be', async () => undefined)
|
|
165
|
+
|
|
166
|
+
it('should await reply', async () => {
|
|
167
|
+
context.reply = promex()
|
|
168
|
+
|
|
169
|
+
let completed = false
|
|
170
|
+
|
|
171
|
+
setImmediate(() => {
|
|
172
|
+
expect(completed).toStrictEqual(false)
|
|
173
|
+
|
|
174
|
+
context.reply.resolve()
|
|
175
|
+
|
|
176
|
+
completed = true
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
await step.call(context)
|
|
180
|
+
|
|
181
|
+
expect(completed).toStrictEqual(true)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
143
185
|
describe('Then the consumer does not receive the reply', () => {
|
|
144
186
|
const step = tomato.steps.Th('the consumer does not receive the reply')
|
|
145
187
|
|
|
@@ -159,3 +201,59 @@ describe('Then the consumer does not receive the reply', () => {
|
|
|
159
201
|
await expect(step.call(context)).resolves.not.toThrow(AssertionError)
|
|
160
202
|
})
|
|
161
203
|
})
|
|
204
|
+
|
|
205
|
+
describe('Given I\'m sending {quantity}B requests to the {token} queue at {quantity}Hz', () => {
|
|
206
|
+
const step = tomato.steps.Gi('I\'m sending {quantity}B requests to the {token} queue at {quantity}Hz')
|
|
207
|
+
|
|
208
|
+
it('should be', async () => undefined)
|
|
209
|
+
|
|
210
|
+
it('should send requests continuously', async () => {
|
|
211
|
+
context = /** @type {comq.features.Context} */ {
|
|
212
|
+
io: /** @type {jest.MockedObject<comq.IO>} */ { request: jest.fn() },
|
|
213
|
+
requestsSent: []
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const bytesQ = '1k'
|
|
217
|
+
const queue = generate()
|
|
218
|
+
const frequencyQ = '100'
|
|
219
|
+
|
|
220
|
+
step.call(context, bytesQ, queue, frequencyQ)
|
|
221
|
+
|
|
222
|
+
await timeout(100 + 11)
|
|
223
|
+
|
|
224
|
+
expect(context.io.request).toHaveBeenCalledTimes(10)
|
|
225
|
+
expect(context.sending).toBeDefined()
|
|
226
|
+
expect(context.requestsSent.length).toStrictEqual(10)
|
|
227
|
+
|
|
228
|
+
clearInterval(context.sending)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('Then all replies have been received', () => {
|
|
233
|
+
const step = tomato.steps.Th('all replies have been received')
|
|
234
|
+
|
|
235
|
+
/** @type {jest.MockedObject<comq.features.Context>} */
|
|
236
|
+
let context
|
|
237
|
+
|
|
238
|
+
it('should be', async () => undefined)
|
|
239
|
+
|
|
240
|
+
beforeEach(async () => {
|
|
241
|
+
context = /** @type {jest.MockedObject<comq.features.Context>} */ {
|
|
242
|
+
requestsSent: [promex(), promex()]
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should pass if replies received', async () => {
|
|
247
|
+
let completed = false
|
|
248
|
+
|
|
249
|
+
setImmediate(() => {
|
|
250
|
+
completed = true
|
|
251
|
+
|
|
252
|
+
for (const promise of context.requestsSent) promise.resolve()
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
await step.call(context)
|
|
256
|
+
|
|
257
|
+
expect(completed).toStrictEqual(true)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
@@ -6,12 +6,22 @@ declare namespace comq.features {
|
|
|
6
6
|
interface Context {
|
|
7
7
|
io?: _io.IO
|
|
8
8
|
connected?: boolean
|
|
9
|
+
connecting: Promise<any>
|
|
10
|
+
requestsSent: Promise<any>[]
|
|
9
11
|
reply?: Promise<any>
|
|
10
|
-
consumed?: Record<string, any>
|
|
11
12
|
published?: any
|
|
13
|
+
eventsPublishedCount: number
|
|
14
|
+
eventsConsumedCount: number
|
|
15
|
+
consumed?: Record<string, any>
|
|
16
|
+
consumedCount: number
|
|
12
17
|
events?: { [K in _diagnostics.event]?: boolean }
|
|
13
18
|
exception?: Error
|
|
14
19
|
expected?: Promise<any>
|
|
20
|
+
sharded: boolean
|
|
21
|
+
shard: number
|
|
22
|
+
sealing: Promise<any>
|
|
23
|
+
sending: any
|
|
24
|
+
publishing: any
|
|
15
25
|
|
|
16
26
|
connect(user?: string, password?: string): Promise<void>
|
|
17
27
|
disconnect(): Promise<void>
|
package/features/steps/broker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { timeout } = require('@toa.io/generic')
|
|
3
|
+
const { timeout, random } = require('@toa.io/generic')
|
|
4
4
|
const { execute } = require('@toa.io/command')
|
|
5
5
|
|
|
6
6
|
const { Given } = require('@cucumber/cucumber')
|
|
@@ -11,19 +11,34 @@ Given('the broker is/has {status}',
|
|
|
11
11
|
* @this {comq.features.Context}
|
|
12
12
|
*/
|
|
13
13
|
async function (status) {
|
|
14
|
-
|
|
14
|
+
const n = this.shard ?? 0
|
|
15
|
+
|
|
16
|
+
await actions[status](n)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
Given('one of the brokers is/has {status}',
|
|
20
|
+
/**
|
|
21
|
+
* @param {'up' | 'down'} status
|
|
22
|
+
* @this {comq.features.Context}
|
|
23
|
+
*/
|
|
24
|
+
async function (status) {
|
|
25
|
+
const shard = random(BROKERS_AMOUNT)
|
|
26
|
+
|
|
27
|
+
this.shard = shard
|
|
28
|
+
|
|
29
|
+
await actions[status](shard)
|
|
15
30
|
})
|
|
16
31
|
|
|
17
32
|
const actions = {
|
|
18
|
-
up: async () => {
|
|
19
|
-
await execute('docker start comq-rmq')
|
|
33
|
+
up: async (n = 0) => {
|
|
34
|
+
await execute('docker start comq-rmq-' + n)
|
|
20
35
|
await healthy()
|
|
21
36
|
},
|
|
22
|
-
down: async () => {
|
|
23
|
-
await execute('docker stop comq-rmq')
|
|
37
|
+
down: async (n = 0) => {
|
|
38
|
+
await execute('docker stop comq-rmq-' + n)
|
|
24
39
|
},
|
|
25
|
-
crashed: async () => {
|
|
26
|
-
await execute('docker kill comq-rmq')
|
|
40
|
+
crashed: async (n = 0) => {
|
|
41
|
+
await execute('docker kill comq-rmq-' + n)
|
|
27
42
|
}
|
|
28
43
|
}
|
|
29
44
|
|
|
@@ -33,6 +48,8 @@ async function healthy () {
|
|
|
33
48
|
do {
|
|
34
49
|
await timeout(1000)
|
|
35
50
|
|
|
36
|
-
process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq')
|
|
51
|
+
process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-0')
|
|
37
52
|
} while (process.output !== 'healthy')
|
|
38
53
|
}
|
|
54
|
+
|
|
55
|
+
const BROKERS_AMOUNT = 2
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
|
-
const { timeout } = require('@toa.io/generic')
|
|
4
|
+
const { timeout, promex } = require('@toa.io/generic')
|
|
5
5
|
|
|
6
6
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
7
7
|
|
|
@@ -15,6 +15,35 @@ Given('an active connection to the broker',
|
|
|
15
15
|
await this.connecting
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
+
Given('an active sharded connection',
|
|
19
|
+
/**
|
|
20
|
+
* @this {comq.features.Context}
|
|
21
|
+
*/
|
|
22
|
+
async function () {
|
|
23
|
+
this.sharded = true
|
|
24
|
+
this.connecting = this.connect()
|
|
25
|
+
|
|
26
|
+
await this.connecting
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
Given('the connection to both shards is established',
|
|
30
|
+
/**
|
|
31
|
+
* @this {comq.features.Context}
|
|
32
|
+
*/
|
|
33
|
+
async function () {
|
|
34
|
+
const promise = promex()
|
|
35
|
+
|
|
36
|
+
this.sharded = true
|
|
37
|
+
this.connecting = this.connect()
|
|
38
|
+
|
|
39
|
+
await this.connecting
|
|
40
|
+
|
|
41
|
+
// second shard is connected
|
|
42
|
+
this.io.diagnose('open', () => promise.resolve())
|
|
43
|
+
|
|
44
|
+
return promise
|
|
45
|
+
})
|
|
46
|
+
|
|
18
47
|
When('I attempt to connect to the broker for {number} second(s)',
|
|
19
48
|
/**
|
|
20
49
|
* @param {number} interval
|
|
@@ -46,6 +75,28 @@ When('I attempt to connect to the broker as {string} with password {string}',
|
|
|
46
75
|
await connect(this, user, password)
|
|
47
76
|
})
|
|
48
77
|
|
|
78
|
+
When('I attempt to establish sharded connection',
|
|
79
|
+
/**
|
|
80
|
+
* @this {comq.features.Context}
|
|
81
|
+
*/
|
|
82
|
+
async function () {
|
|
83
|
+
this.sharded = true
|
|
84
|
+
|
|
85
|
+
await connect(this)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
When('I attempt to establish a sharded connection as {string} with password {string}',
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} user
|
|
91
|
+
* @param {string} password
|
|
92
|
+
* @this {comq.features.Context}
|
|
93
|
+
*/
|
|
94
|
+
async function (user, password) {
|
|
95
|
+
this.sharded = true
|
|
96
|
+
|
|
97
|
+
await connect(this, user, password)
|
|
98
|
+
})
|
|
99
|
+
|
|
49
100
|
Then('the connection is not established',
|
|
50
101
|
/**
|
|
51
102
|
* @this {comq.features.Context}
|
|
@@ -98,6 +149,22 @@ Then('the connection is {connection-event}',
|
|
|
98
149
|
assert.equal(this.events[event], true, 'connection was not ' + key)
|
|
99
150
|
})
|
|
100
151
|
|
|
152
|
+
Given('the connection has started sealing',
|
|
153
|
+
/**
|
|
154
|
+
* @this {comq.features.Context}
|
|
155
|
+
*/
|
|
156
|
+
async function () {
|
|
157
|
+
this.sealing = this.io.seal()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
Then('the connection is sealed',
|
|
161
|
+
/**
|
|
162
|
+
* @this {comq.features.Context}
|
|
163
|
+
*/
|
|
164
|
+
async function () {
|
|
165
|
+
await this.sealing
|
|
166
|
+
})
|
|
167
|
+
|
|
101
168
|
/**
|
|
102
169
|
* @param {comq.features.Context} context
|
|
103
170
|
* @param {string} [user]
|
|
@@ -108,7 +175,7 @@ const connect = async (context, user, password) => {
|
|
|
108
175
|
try {
|
|
109
176
|
await context.connect(user, password)
|
|
110
177
|
} catch (exception) {
|
|
111
|
-
context.exception = exception
|
|
178
|
+
context.exception = exception instanceof AggregateError ? exception.errors[0] : exception
|
|
112
179
|
}
|
|
113
180
|
}
|
|
114
181
|
|
|
@@ -10,17 +10,23 @@ class Context extends World {
|
|
|
10
10
|
io
|
|
11
11
|
connected = false
|
|
12
12
|
connecting
|
|
13
|
+
requestsSent = []
|
|
13
14
|
reply
|
|
14
15
|
consumed
|
|
15
16
|
published
|
|
17
|
+
eventsPublishedCount = 0
|
|
18
|
+
eventsConsumedCount = 0
|
|
16
19
|
events = {}
|
|
17
20
|
exception
|
|
18
21
|
expected
|
|
22
|
+
sharded
|
|
23
|
+
shard
|
|
24
|
+
sealing
|
|
19
25
|
|
|
20
26
|
async connect (user, password) {
|
|
21
|
-
const
|
|
27
|
+
const urls = this.#urls(user, password)
|
|
22
28
|
|
|
23
|
-
await this.#connect(
|
|
29
|
+
await this.#connect(...urls)
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
async disconnect () {
|
|
@@ -34,13 +40,13 @@ class Context extends World {
|
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
37
|
-
* @param {string}
|
|
43
|
+
* @param {...string} urls
|
|
38
44
|
* @return {Promise<void>}
|
|
39
45
|
*/
|
|
40
|
-
async #connect (
|
|
46
|
+
async #connect (...urls) {
|
|
41
47
|
if (this.io !== undefined) await this.disconnect()
|
|
42
48
|
|
|
43
|
-
this.io = await connect(
|
|
49
|
+
this.io = await connect(...urls)
|
|
44
50
|
this.connected = true
|
|
45
51
|
|
|
46
52
|
for (const event of EVENTS) this.io.diagnose(event, () => (this.events[event] = true))
|
|
@@ -48,19 +54,29 @@ class Context extends World {
|
|
|
48
54
|
this.io.diagnose('close', () => (this.connected = false))
|
|
49
55
|
this.io.diagnose('open', () => (this.connected = true))
|
|
50
56
|
}
|
|
51
|
-
}
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
#urls (user, password) {
|
|
59
|
+
if (user === undefined) {
|
|
60
|
+
user = USER
|
|
61
|
+
password = PASSWORD
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const urls = []
|
|
65
|
+
|
|
66
|
+
urls.push(this.#url(0, user, password))
|
|
67
|
+
|
|
68
|
+
if (this.sharded) urls.push(this.#url(1, user, password))
|
|
69
|
+
|
|
70
|
+
return urls
|
|
57
71
|
}
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
#url (i, user, password) {
|
|
74
|
+
return PROTOCOL + user + ':' + password + '@' + SHARDS[i]
|
|
75
|
+
}
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
const PROTOCOL = 'amqp://'
|
|
63
|
-
const
|
|
79
|
+
const SHARDS = ['localhost:5673', 'localhost:5674']
|
|
64
80
|
const USER = 'developer'
|
|
65
81
|
const PASSWORD = 'secret'
|
|
66
82
|
|
package/features/steps/events.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
4
|
const { generate } = require('randomstring')
|
|
5
|
-
const { timeout } = require('@toa.io/generic')
|
|
5
|
+
const { timeout, quantity } = require('@toa.io/generic')
|
|
6
6
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
7
|
+
const { randomBytes } = require('node:crypto')
|
|
7
8
|
|
|
8
9
|
Given('(that ){token} is consuming events from the {token} exchange',
|
|
9
10
|
/**
|
|
@@ -27,7 +28,7 @@ Given('{token} consuming events from the {token} exchange is expected',
|
|
|
27
28
|
this.expected = consume.call(this, group, exchange)
|
|
28
29
|
})
|
|
29
30
|
|
|
30
|
-
Given('that events are exclusively consumed from the {token} exchange',
|
|
31
|
+
Given('(that )events are exclusively consumed from the {token} exchange',
|
|
31
32
|
/**
|
|
32
33
|
* @param {string} exchange
|
|
33
34
|
* @this {comq.features.Context}
|
|
@@ -68,6 +69,43 @@ Then('the event is received',
|
|
|
68
69
|
await consumed.call(this)
|
|
69
70
|
})
|
|
70
71
|
|
|
72
|
+
Given('I\'m publishing {quantity}B events to the {token} exchange at {quantity}Hz',
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} bytesQ
|
|
75
|
+
* @param {string} queue
|
|
76
|
+
* @param {string} frequencyQ
|
|
77
|
+
* @this {comq.features.Context}
|
|
78
|
+
*/
|
|
79
|
+
async function (bytesQ, queue, frequencyQ) {
|
|
80
|
+
const bytes = quantity(bytesQ)
|
|
81
|
+
const frequency = quantity(frequencyQ)
|
|
82
|
+
const buffer = randomBytes(bytes)
|
|
83
|
+
|
|
84
|
+
const delay = Math.max((1000 / frequency), 1)
|
|
85
|
+
|
|
86
|
+
const emit = async () => {
|
|
87
|
+
await this.io.emit(queue, buffer)
|
|
88
|
+
|
|
89
|
+
this.eventsPublishedCount++
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.publishing = setInterval(emit, delay)
|
|
93
|
+
|
|
94
|
+
await timeout(delay * 2) // send at least twice
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
Then('all events have been received',
|
|
98
|
+
/**
|
|
99
|
+
* @this {comq.features.Context}
|
|
100
|
+
*/
|
|
101
|
+
async function () {
|
|
102
|
+
clearInterval(this.publishing)
|
|
103
|
+
|
|
104
|
+
await timeout(50)
|
|
105
|
+
|
|
106
|
+
assert.equal(this.eventsPublishedCount, this.eventsConsumedCount, 'Not all events has been consumed')
|
|
107
|
+
})
|
|
108
|
+
|
|
71
109
|
/**
|
|
72
110
|
* @this {comq.features.Context} context
|
|
73
111
|
* @param {string} group
|
|
@@ -79,6 +117,7 @@ async function consume (group, exchange) {
|
|
|
79
117
|
|
|
80
118
|
return this.io.consume(exchange, group, async (payload) => {
|
|
81
119
|
this.consumed[group] = payload
|
|
120
|
+
this.eventsConsumedCount++
|
|
82
121
|
})
|
|
83
122
|
}
|
|
84
123
|
|
package/features/steps/rpc.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
4
|
const { randomBytes } = require('node:crypto')
|
|
5
5
|
const { parse } = require('@toa.io/yaml')
|
|
6
|
-
const { match, timeout } = require('@toa.io/generic')
|
|
6
|
+
const { match, timeout, quantity } = require('@toa.io/generic')
|
|
7
7
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
8
8
|
|
|
9
9
|
Given('function replying {token} queue:',
|
|
@@ -19,6 +19,21 @@ Given('function replying {token} queue:',
|
|
|
19
19
|
await this.io.reply(queue, producer)
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
+
Given('a producer replying {token} queue',
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} queue
|
|
25
|
+
* @this {comq.features.Context}
|
|
26
|
+
*/
|
|
27
|
+
async function (queue) {
|
|
28
|
+
const producer = async (request) => {
|
|
29
|
+
await timeout(10)
|
|
30
|
+
|
|
31
|
+
return request
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await this.io.reply(queue, producer)
|
|
35
|
+
})
|
|
36
|
+
|
|
22
37
|
Given('function replying {token} queue is expected:',
|
|
23
38
|
/**
|
|
24
39
|
* @param {string} queue
|
|
@@ -68,6 +83,14 @@ Then('the consumer receives the reply:',
|
|
|
68
83
|
assert.equal(matches, true, 'Reply mismatch')
|
|
69
84
|
})
|
|
70
85
|
|
|
86
|
+
Then('the consumer receives the reply',
|
|
87
|
+
/**
|
|
88
|
+
* @this {comq.features.Context}
|
|
89
|
+
*/
|
|
90
|
+
async function () {
|
|
91
|
+
await this.reply
|
|
92
|
+
})
|
|
93
|
+
|
|
71
94
|
Then('the consumer does not receive the reply',
|
|
72
95
|
/**
|
|
73
96
|
* @this {comq.features.Context}
|
|
@@ -83,6 +106,40 @@ Then('the consumer does not receive the reply',
|
|
|
83
106
|
assert.equal(reply, undefined, 'The reply was received')
|
|
84
107
|
})
|
|
85
108
|
|
|
109
|
+
Given('I\'m sending {quantity}B requests to the {token} queue at {quantity}Hz',
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} bytesQ
|
|
112
|
+
* @param {string} queue
|
|
113
|
+
* @param {string} frequencyQ
|
|
114
|
+
* @this {comq.features.Context}
|
|
115
|
+
*/
|
|
116
|
+
async function (bytesQ, queue, frequencyQ) {
|
|
117
|
+
const bytes = quantity(bytesQ)
|
|
118
|
+
const buffer = randomBytes(bytes)
|
|
119
|
+
const frequency = quantity(frequencyQ)
|
|
120
|
+
const delay = Math.max((1000 / frequency), 1)
|
|
121
|
+
|
|
122
|
+
const emit = () => {
|
|
123
|
+
const promise = this.io.request(queue, buffer)
|
|
124
|
+
|
|
125
|
+
this.requestsSent.push(promise)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.sending = setInterval(emit, delay)
|
|
129
|
+
|
|
130
|
+
await timeout(delay) // send at least twice
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
Then('all replies have been received',
|
|
134
|
+
/**
|
|
135
|
+
* @this {comq.features.Context}
|
|
136
|
+
*/
|
|
137
|
+
async function () {
|
|
138
|
+
clearInterval(this.sending)
|
|
139
|
+
|
|
140
|
+
await Promise.all(this.requestsSent)
|
|
141
|
+
})
|
|
142
|
+
|
|
86
143
|
async function send (queue, payload) {
|
|
87
144
|
if (this.expected) await this.expected
|
|
88
145
|
|
package/lerna.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-dev.0",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -37,15 +37,15 @@
|
|
|
37
37
|
"standard": "17.0.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@toa.io/generic": "0.4.0-dev.
|
|
40
|
+
"@toa.io/generic": "0.4.0-dev.5",
|
|
41
41
|
"amqplib": "0.10.3",
|
|
42
42
|
"msgpackr": "1.8.5"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"prepare": "husky install",
|
|
46
|
-
"test": "
|
|
47
|
-
"test:features": "
|
|
48
|
-
"lint": "
|
|
46
|
+
"test": "standard && jest",
|
|
47
|
+
"test:features": "cucumber-js",
|
|
48
|
+
"lint": "standard --fix --verbose | snazzy"
|
|
49
49
|
},
|
|
50
50
|
"standard": {
|
|
51
51
|
"env": [
|