comq 0.5.1-dev.1 → 0.5.2-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/workflows/codeql.yml +2 -0
- package/examples/events/consumer.js +0 -2
- package/examples/events/producer.js +0 -2
- package/examples/rpc/consumer.js +0 -2
- package/examples/rpc/producer.js +0 -2
- package/lerna.json +1 -1
- package/package.json +1 -1
- package/readme.md +16 -11
- package/source/channel.js +6 -3
- package/source/shards/channel.js +12 -10
- package/test/channel.test.js +12 -9
- package/test/connection.mock.js +1 -1
- package/test/shards/channel.test.js +34 -0
|
@@ -14,9 +14,11 @@ name: "CodeQL"
|
|
|
14
14
|
on:
|
|
15
15
|
push:
|
|
16
16
|
branches: [ "main" ]
|
|
17
|
+
paths: [ "source/**" ]
|
|
17
18
|
pull_request:
|
|
18
19
|
# The branches below must be a subset of the branches above
|
|
19
20
|
branches: [ "main" ]
|
|
21
|
+
paths: [ "source/**" ]
|
|
20
22
|
schedule:
|
|
21
23
|
- cron: '15 8 * * 3'
|
|
22
24
|
|
package/examples/rpc/consumer.js
CHANGED
package/examples/rpc/producer.js
CHANGED
package/lerna.json
CHANGED
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -173,6 +173,13 @@ The following encoding formats are supported:
|
|
|
173
173
|
- `application/octet-stream`
|
|
174
174
|
- `text/plain`
|
|
175
175
|
|
|
176
|
+
## Flow Control
|
|
177
|
+
|
|
178
|
+
When [back pressure](https://www.rabbitmq.com/flow-control.html) is applied to a channel or the
|
|
179
|
+
underlying broker connection is lost, any current and future outgoing messages will be paused.
|
|
180
|
+
Corresponding returned promises will remain in a `pending` state until the pressure is removed or
|
|
181
|
+
the connection is restored.
|
|
182
|
+
|
|
176
183
|
## Connection Tolerance
|
|
177
184
|
|
|
178
185
|
When initially connecting to the broker or if the established connection is lost, connection
|
|
@@ -191,10 +198,6 @@ A sharded connection is a mechanism that uses multiple connections simultaneousl
|
|
|
191
198
|
balancing and mitigate failover scenarios, utilizing a set of broker instances that are **not**
|
|
192
199
|
combined into a cluster.
|
|
193
200
|
|
|
194
|
-
`async connect(...shards: string[]): IO`
|
|
195
|
-
|
|
196
|
-
Returns an instance of `IO` once a successful connection to one of the shards is established.
|
|
197
|
-
|
|
198
201
|
Outgoing messages are sent to a single connection chosen at random from the shard pool. Shards that
|
|
199
202
|
lose their underlying connection or experience channel [back pressure](#flow-control) on
|
|
200
203
|
corresponding channel are removed from the pool until the issue is resolved. Pending messages
|
|
@@ -203,6 +206,10 @@ are available, messages will wait until any shard's connection is re-established
|
|
|
203
206
|
|
|
204
207
|
Incoming messages are consumed from all shards.
|
|
205
208
|
|
|
209
|
+
`async connect(...shards: string[]): IO`
|
|
210
|
+
|
|
211
|
+
Returns an instance of `IO` once a successful connection to one of the shards is established.
|
|
212
|
+
|
|
206
213
|
### Example
|
|
207
214
|
|
|
208
215
|
```javascript
|
|
@@ -218,13 +225,6 @@ await io.close()
|
|
|
218
225
|
|
|
219
226
|
```
|
|
220
227
|
|
|
221
|
-
## Flow Control
|
|
222
|
-
|
|
223
|
-
When [back pressure](https://www.rabbitmq.com/flow-control.html) is applied to a channel or the
|
|
224
|
-
underlying broker connection is lost, any current and future outgoing messages will be paused.
|
|
225
|
-
Corresponding returned promises will remain in a `pending` state until the pressure is removed or
|
|
226
|
-
the connection is restored.
|
|
227
|
-
|
|
228
228
|
## Topology
|
|
229
229
|
|
|
230
230
|
Topology is designed to deliver maximum performance while ensuring that the **at least once**
|
|
@@ -366,3 +366,8 @@ established, there is no way to capture the initial `open` event.
|
|
|
366
366
|
```javascript
|
|
367
367
|
io.diagnose('flow', (type) => console.log(`Back pressure was applied to the ${type} channel`))
|
|
368
368
|
```
|
|
369
|
+
|
|
370
|
+
# Gratitude
|
|
371
|
+
|
|
372
|
+
I want to express my deep appreciation to [@mzabolotko](https://github.com/mzabolotko) for his
|
|
373
|
+
generous contribution of time and expertise.
|
package/source/channel.js
CHANGED
|
@@ -198,7 +198,7 @@ class Channel {
|
|
|
198
198
|
* @param {comq.amqp.options.Publish} options
|
|
199
199
|
*/
|
|
200
200
|
async #publish (exchange, queue, buffer, options) {
|
|
201
|
-
if (this.#paused !== undefined) await this.#
|
|
201
|
+
if (this.#paused !== undefined) await this.#unpaused()
|
|
202
202
|
|
|
203
203
|
options = Object.assign({ persistent: this.#topology.persistent }, options)
|
|
204
204
|
|
|
@@ -282,8 +282,6 @@ class Channel {
|
|
|
282
282
|
this.#paused = promex()
|
|
283
283
|
this.#channel.once('drain', this.#unpause)
|
|
284
284
|
this.#diagnostics.emit('flow')
|
|
285
|
-
|
|
286
|
-
if (this.#failfast) throw INTERRUPTION
|
|
287
285
|
}
|
|
288
286
|
|
|
289
287
|
/**
|
|
@@ -299,6 +297,11 @@ class Channel {
|
|
|
299
297
|
this.#diagnostics.emit('drain')
|
|
300
298
|
}
|
|
301
299
|
|
|
300
|
+
async #unpaused () {
|
|
301
|
+
if (this.#failfast) throw INTERRUPTION // tell shards.Channel to remove this one from the pool
|
|
302
|
+
else await this.#paused
|
|
303
|
+
}
|
|
304
|
+
|
|
302
305
|
async #recover (exception) {
|
|
303
306
|
if (permanent(exception)) return false
|
|
304
307
|
else await this.#recovery
|
package/source/shards/channel.js
CHANGED
|
@@ -22,8 +22,8 @@ class Channel {
|
|
|
22
22
|
/** @type {Set<Promise<comq.Channel>>} */
|
|
23
23
|
#pending = new Set()
|
|
24
24
|
|
|
25
|
-
/** @type {
|
|
26
|
-
#bench =
|
|
25
|
+
/** @type {Map<comq.Channel, toa.generic.Promex>} */
|
|
26
|
+
#bench = new Map()
|
|
27
27
|
|
|
28
28
|
/** @type {comq.topology.type} */
|
|
29
29
|
#type
|
|
@@ -87,6 +87,8 @@ class Channel {
|
|
|
87
87
|
this.#add(channel)
|
|
88
88
|
this.#pipe(channel)
|
|
89
89
|
|
|
90
|
+
channel.diagnose('flow', () => this.#remove((channel)))
|
|
91
|
+
channel.diagnose('drain', () => this.#recover(channel))
|
|
90
92
|
channel.diagnose('recover', () => this.#recover(channel))
|
|
91
93
|
}
|
|
92
94
|
|
|
@@ -127,18 +129,17 @@ class Channel {
|
|
|
127
129
|
#remove (channel) {
|
|
128
130
|
if (!this.#channels.has(channel)) return
|
|
129
131
|
|
|
130
|
-
this.#
|
|
131
|
-
|
|
132
|
-
this.#bench[channel] = promex()
|
|
132
|
+
this.#bench.set(channel, promex())
|
|
133
133
|
this.#channels.delete(channel)
|
|
134
134
|
this.#pool = Array.from(this.#channels)
|
|
135
|
+
this.#diagnostics.emit('remove', channel.index)
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/**
|
|
138
139
|
* @param {comq.Channel} channel
|
|
139
140
|
*/
|
|
140
141
|
#recover (channel) {
|
|
141
|
-
if (
|
|
142
|
+
if (this.#bench.has(channel)) this.#comeback(channel)
|
|
142
143
|
|
|
143
144
|
this.#add(channel)
|
|
144
145
|
|
|
@@ -147,8 +148,8 @@ class Channel {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
#comeback (channel) {
|
|
150
|
-
this.#bench
|
|
151
|
-
|
|
151
|
+
this.#bench.get(channel).resolve(channel)
|
|
152
|
+
this.#bench.delete(channel)
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
/**
|
|
@@ -180,7 +181,7 @@ class Channel {
|
|
|
180
181
|
|
|
181
182
|
for (const channel of this.#channels) promises.push(fn(channel))
|
|
182
183
|
for (const pending of this.#pending) promises.push(pending.then(fn))
|
|
183
|
-
for (const recover of
|
|
184
|
+
for (const recover of this.#bench.values()) promises.push(recover.then(fn))
|
|
184
185
|
|
|
185
186
|
return promises
|
|
186
187
|
}
|
|
@@ -196,8 +197,9 @@ class Channel {
|
|
|
196
197
|
|
|
197
198
|
try {
|
|
198
199
|
await fn(channel)
|
|
199
|
-
} catch
|
|
200
|
+
} catch {
|
|
200
201
|
this.#remove(channel)
|
|
202
|
+
|
|
201
203
|
await this.#one(fn)
|
|
202
204
|
}
|
|
203
205
|
}
|
package/test/channel.test.js
CHANGED
|
@@ -871,6 +871,7 @@ describe('diagnostics', () => {
|
|
|
871
871
|
describe('transient', () => {
|
|
872
872
|
const exchange = generate()
|
|
873
873
|
const queue = generate()
|
|
874
|
+
const label = generate()
|
|
874
875
|
const buffer = randomBytes(8)
|
|
875
876
|
const index = random()
|
|
876
877
|
|
|
@@ -879,25 +880,27 @@ describe('transient', () => {
|
|
|
879
880
|
chan = await getCreatedChannel()
|
|
880
881
|
})
|
|
881
882
|
|
|
882
|
-
it('should throw transient exceptions on
|
|
883
|
-
|
|
883
|
+
it.each(/** @type {string[]} */ ['publish', 'send'])('should throw transient exceptions on %s',
|
|
884
|
+
async (method) => {
|
|
885
|
+
chan.publish.mockImplementation(() => { throw new Error('Channel closed') })
|
|
884
886
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
+
await expect(channel[method](label, buffer)).rejects.toThrow()
|
|
888
|
+
})
|
|
887
889
|
|
|
888
|
-
it('should throw
|
|
889
|
-
chan.publish.
|
|
890
|
+
it('should not throw when back pressure is applied', async () => {
|
|
891
|
+
chan.publish.mockImplementationOnce(backpressure.publish)
|
|
890
892
|
|
|
891
|
-
await expect(channel.
|
|
893
|
+
await expect(channel.publish(exchange, buffer)).resolves.not.toThrow()
|
|
892
894
|
})
|
|
893
895
|
|
|
894
|
-
it('should throw
|
|
896
|
+
it('should throw if backpressure was applied before', async () => {
|
|
895
897
|
chan.publish.mockImplementationOnce(backpressure.publish)
|
|
896
898
|
|
|
899
|
+
await expect(channel.publish(exchange, buffer)).resolves.not.toThrow()
|
|
897
900
|
await expect(channel.publish(exchange, buffer)).rejects.toBeDefined()
|
|
898
901
|
})
|
|
899
902
|
|
|
900
|
-
it('should not ignore exceptions on `
|
|
903
|
+
it('should not ignore exceptions on `fire()`', async () => {
|
|
901
904
|
chan.publish.mockImplementation(() => { throw new Error('Channel closed') })
|
|
902
905
|
|
|
903
906
|
await expect(channel.fire(queue, buffer)).rejects.toThrow()
|
package/test/connection.mock.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const channel = /** @type {jest.MockedFunction<(sharded?: boolean, index?: number) => comq.Channel
|
|
3
|
+
const channel = /** @type {jest.MockedFunction<(sharded?: boolean, index?: number) => jest.MockedObject<comq.Channel>>} */ jest.fn(
|
|
4
4
|
(sharded = false, index = undefined) => ({
|
|
5
5
|
index,
|
|
6
6
|
sharded,
|
|
@@ -357,6 +357,40 @@ it('should not throw on recovery (if the "bench" is empty)', async () => {
|
|
|
357
357
|
expect(emit).not.toThrow()
|
|
358
358
|
})
|
|
359
359
|
|
|
360
|
+
it('should remove channel with back pressure from the pool', async () => {
|
|
361
|
+
channel = await create(connections, type)
|
|
362
|
+
|
|
363
|
+
const queue = generate()
|
|
364
|
+
const buffer = randomBytes(8)
|
|
365
|
+
const channels = await getCreatedChannels()
|
|
366
|
+
const chan = sample(channels)
|
|
367
|
+
const flowCalls = chan.diagnose.mock.calls.filter((call) => call[0] === 'flow')
|
|
368
|
+
const drainCalls = chan.diagnose.mock.calls.filter((call) => call[0] === 'drain')
|
|
369
|
+
const flowListeners = flowCalls.map((call) => call[1])
|
|
370
|
+
const drainListeners = drainCalls.map((call) => call[1])
|
|
371
|
+
|
|
372
|
+
async function send () {
|
|
373
|
+
for (let i = 0; i < 10; i++) await channel.send(queue, buffer)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await send()
|
|
377
|
+
expect(chan.send).toHaveBeenCalled()
|
|
378
|
+
chan.send.mockClear()
|
|
379
|
+
|
|
380
|
+
// emit 'flow'
|
|
381
|
+
for (const listener of flowListeners) listener()
|
|
382
|
+
|
|
383
|
+
await send()
|
|
384
|
+
expect(chan.send).not.toHaveBeenCalled()
|
|
385
|
+
chan.send.mockClear()
|
|
386
|
+
|
|
387
|
+
// emit 'drain'
|
|
388
|
+
for (const listener of drainListeners) listener()
|
|
389
|
+
|
|
390
|
+
await send()
|
|
391
|
+
expect(chan.send).toHaveBeenCalled()
|
|
392
|
+
})
|
|
393
|
+
|
|
360
394
|
/**
|
|
361
395
|
* @return {Promise<jest.MockedObject<comq.Channel>[]>}
|
|
362
396
|
*/
|