comq 0.5.1 → 0.5.2

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.
@@ -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
 
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  import { connect } from 'comq'
4
2
  import { url } from '../const.js'
5
3
 
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  import { connect } from 'comq'
4
2
  import { url } from '../const.js'
5
3
 
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  import { connect } from 'comq'
4
2
  import { url } from '../const.js'
5
3
 
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  import { connect } from 'comq'
4
2
  import { url } from '../const.js'
5
3
 
package/lerna.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "useWorkspaces": true,
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "conventionalCommits": true
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "ComQ",
5
5
  "author": {
6
6
  "name": "Artem Gurtovoi",
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.#paused
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
@@ -22,8 +22,8 @@ class Channel {
22
22
  /** @type {Set<Promise<comq.Channel>>} */
23
23
  #pending = new Set()
24
24
 
25
- /** @type {Record<comq.Channel, toa.generic.Promex>} */
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.#diagnostics.emit('remove', channel.index)
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 (channel in this.#bench) this.#comeback(channel)
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[channel].resolve(channel)
151
- delete this.#bench[channel]
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 Object.values(this.#bench)) promises.push(recover.then(fn))
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 (e) {
200
+ } catch {
200
201
  this.#remove(channel)
202
+
201
203
  await this.#one(fn)
202
204
  }
203
205
  }
@@ -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 publish', async () => {
883
- chan.publish.mockImplementation(() => { throw new Error('Channel closed') })
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
- await expect(channel.publish(exchange, buffer)).rejects.toThrow()
886
- })
887
+ await expect(channel[method](label, buffer)).rejects.toThrow()
888
+ })
887
889
 
888
- it('should throw transient exceptions on send', async () => {
889
- chan.publish.mockImplementation(() => { throw new Error('Channel closed') })
890
+ it('should not throw when back pressure is applied', async () => {
891
+ chan.publish.mockImplementationOnce(backpressure.publish)
890
892
 
891
- await expect(channel.send(queue, buffer)).rejects.toThrow()
893
+ await expect(channel.publish(exchange, buffer)).resolves.not.toThrow()
892
894
  })
893
895
 
894
- it('should throw on back pressure', async () => {
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 `throw()`', async () => {
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()
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const channel = /** @type {jest.MockedFunction<(sharded?: boolean, index?: number) => comq.Channel>} */ jest.fn(
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
  */