comq 0.11.0 → 0.11.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.
@@ -0,0 +1,22 @@
1
+ Feature: Poison events
2
+
3
+ Background:
4
+ Given an active connection to the broker
5
+
6
+ @manual
7
+ Scenario: Sending a poison event
8
+
9
+ This scenario will cause an exception, run it once.
10
+
11
+ Given that events from the `poison` exchange are causing exceptions
12
+ Then an event is emitted to the `poison` exchange
13
+ And after 100ms
14
+
15
+ @manual
16
+ Scenario: Consuming poison events
17
+
18
+ After the poison event is sent, run this scenario five times
19
+ and verify that the poison event was discarded.
20
+
21
+ Given that events from the `poison` exchange are causing exceptions
22
+ And after 100ms
@@ -31,15 +31,6 @@ Feature: Request-reply (RPC)
31
31
  null
32
32
  """
33
33
 
34
- Scenario: Request causing an exception is discarded
35
- Given function replying `throw_error` queue:
36
- """
37
- () => { throw new Error() }
38
- """
39
- When the consumer sends a request to the `throw_error` queue
40
- Then the message is discarded
41
- And the consumer does not receive the reply
42
-
43
34
  Scenario: Streaming requests
44
35
  Given a producer replying `ping` queue
45
36
  When the consumer sends 1k requests to the `ping` queue as a stream
@@ -38,6 +38,17 @@ Given('(that )events are exclusively consumed from the {token} exchange',
38
38
  this.consumptionPromise = consume.call(this, undefined, exchange)
39
39
  })
40
40
 
41
+ Given('(that )events from the {token} exchange are causing exceptions',
42
+ /**
43
+ * @param {string} exchange
44
+ * @this {comq.features.Context}
45
+ */
46
+ async function (exchange) {
47
+ await this.io.consume(exchange, 'exceptions', () => {
48
+ throw new Error('Expected exception')
49
+ })
50
+ })
51
+
41
52
  When('an event is emitted to the {token} exchange',
42
53
  /**
43
54
  * @param {string} exchange
@@ -9,7 +9,7 @@ Then('the message is discarded',
9
9
  * @this {comq.features.Context}
10
10
  */
11
11
  async function () {
12
- if (this.events.discard !== true) await timeout(50)
12
+ if (this.events.discard !== true) await timeout(300)
13
13
 
14
14
  assert.equal(this.events.discard, true, 'The {message} was not discarded')
15
15
  })
package/lerna.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.11.0",
2
+ "version": "0.11.2",
3
3
  "conventionalCommits": true
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "ComQ",
5
5
  "author": {
6
6
  "name": "Artem Gurtovoi",
@@ -48,7 +48,7 @@
48
48
  "scripts": {
49
49
  "prepare": "echo run: husky install",
50
50
  "test": "standard && jest",
51
- "features": "cucumber-js --fail-fast",
51
+ "features": "cucumber-js --fail-fast -t 'not @manual'",
52
52
  "lint": "standard --fix --verbose | snazzy"
53
53
  },
54
54
  "standard": {
package/readme.md CHANGED
@@ -164,7 +164,7 @@ await io.emit('numbers_added', { a: 1, b: 2 })
164
164
 
165
165
  Publish encoded Task to the `queue`.
166
166
 
167
- On the initial call, the `queue` is asserted on the Events channel using Event topology.
167
+ On the initial call, the `queue` is asserted on the Events channel using [Event topology](#topology).
168
168
 
169
169
  `async IO.process(queue: string, processor): void`
170
170
 
@@ -176,7 +176,7 @@ The `queue` is asserted on the Events channel using Event topology.
176
176
 
177
177
  ## Pipelines
178
178
 
179
- Payloads for Requests and Events can be passed as a readable stream
179
+ Payloads for Requests, Events and Tasks can be passed as a readable stream
180
180
  in [object mode](https://nodejs.org/api/stream.html#object-mode), enabling the handling of large amounts of data with
181
181
  the benefits of RabbitMQ back pressure and flow control.
182
182
 
@@ -186,6 +186,8 @@ Returns a readable stream of replies.
186
186
 
187
187
  `async IO.emit(exchange: string, stream: Readable, encoding?: string): void`
188
188
 
189
+ `async IO.enqueue(queue: string, stream: Readable, encoding?: string): void`
190
+
189
191
  ```javascript
190
192
  function * generate () {
191
193
  yield { a: 1, b: 2 };
@@ -196,6 +198,10 @@ const events = Readable.from(generate())
196
198
 
197
199
  await io.emit('numbers_added', events)
198
200
 
201
+ const tasks = Readable.from(generate())
202
+
203
+ await io.enqueue('add_numbers', tasks)
204
+
199
205
  const requests = Readable.from(generate())
200
206
 
201
207
  for await (const reply of io.request('add_numbers', requests))
@@ -405,8 +411,9 @@ See [queue assertion options](https://amqp-node.github.io/amqplib/channel_api.ht
405
411
  manual [acknowledgment mode](https://www.rabbitmq.com/confirms.html#acknowledgment-modes),
406
412
  and Replies are consumed using automatic mode.
407
413
 
408
- If an incoming message causes an exception, then it is "negatively acknowledged" and requeued. If it
409
- causes an exception again, it will be discarded.
414
+ If an incoming message causes an exception, the corresponding channel is sealed, the message is republished, and the
415
+ exception is thrown.
416
+ If the message causes exceptions five times in a row, it is discarded.
410
417
 
411
418
  > It is highly recommended to set up a dead letter exchange policy to analyze messages that caused
412
419
  > exceptions. Note that in some cases, if the problematic message is a Request, a Consumer will
package/source/channel.js CHANGED
@@ -58,6 +58,8 @@ class Channel {
58
58
  if (this.#topology.confirms) this.#channel = await this.#connection.createConfirmChannel()
59
59
  else this.#channel = await this.#connection.createChannel()
60
60
 
61
+ await this.#channel.prefetch(this.#topology.prefetch)
62
+
61
63
  this.#channel.on('drain', this.#unpause)
62
64
  this.#channel.on('return', (message) => this.#diagnostics.emit('return', message))
63
65
  }
@@ -267,18 +269,29 @@ class Channel {
267
269
 
268
270
  this.#channel.ack(message)
269
271
  } catch (exception) {
270
- if (exception.message === 'Channel closed') return // the message will be requeued by the broker
272
+ if (exception.message === 'Channel closed') { return } // the message is requeued by the broker
273
+
274
+ const redeliveries = message.properties.headers[REDELIVERY_HEADER] ?? 0
271
275
 
272
- if (message.fields.redelivered) this.#discard(message, exception)
273
- else this.#requeue(message)
276
+ if (redeliveries >= MAX_REDELIVERIES) this.#discard(message, exception)
277
+ else {
278
+ await this.#requeue(message, redeliveries)
279
+ throw exception
280
+ }
274
281
  }
275
282
  }
276
283
 
277
284
  /**
278
285
  * @param {comq.amqp.Message} message
286
+ * @param {number} attempt
279
287
  */
280
- #requeue (message) {
281
- this.#channel.nack(message, false, true)
288
+ async #requeue (message, attempt) {
289
+ this.#channel.ack(message)
290
+
291
+ message.properties.headers[REDELIVERY_HEADER] = attempt + 1
292
+
293
+ await this.seal()
294
+ await this.#publish(message.fields.exchange, message.fields.routingKey, message.content, message.properties)
282
295
  }
283
296
 
284
297
  /**
@@ -358,6 +371,9 @@ const EXCLUSIVE = { exclusive: true }
358
371
 
359
372
  const INTERRUPTION = /** @type {Error} */ Symbol('internal interruption')
360
373
 
374
+ const MAX_REDELIVERIES = 5
375
+ const REDELIVERY_HEADER = 'x-attempt'
376
+
361
377
  function noop () {}
362
378
 
363
379
  exports.create = create
@@ -42,6 +42,13 @@ it('should return Channel', async () => {
42
42
  expect(channel).toBeDefined()
43
43
  })
44
44
 
45
+ it('should set prefetch limit', async () => {
46
+ channel = await create(connection, topology)
47
+ chan = await getCreatedChannel()
48
+
49
+ expect(chan.prefetch).toHaveBeenCalledWith(topology.prefetch)
50
+ })
51
+
45
52
  it.each([true, false])('should create channel (confirms: %s)', async (confirms) => {
46
53
  const method = `create${confirms ? 'Confirm' : ''}Channel`
47
54
 
@@ -134,32 +141,6 @@ describe('acknowledgments', () => {
134
141
  else expect(options).toMatchObject({ noAck: true })
135
142
  })
136
143
 
137
- it.each(/** @type {[string, boolean][]} */ [
138
- ['nack', true],
139
- ['discard', false]
140
- ])('should %s the message caused an exception', async (_, requeue) => {
141
- topology.acknowledgments = true
142
-
143
- channel = await create(connection, topology)
144
- chan = await getCreatedChannel()
145
-
146
- const consumer = /** @type {Function} */ jest.fn(async () => { throw new Error() })
147
-
148
- await channel.consume(queue, consumer)
149
-
150
- const callback = chan.consume.mock.calls[0][1]
151
- const content = randomBytes(8)
152
- const properties = {}
153
- const fields = {}
154
- const message = /** @type {comq.amqp.Message} */ { content, properties, fields }
155
-
156
- if (!requeue) fields.redelivered = !requeue
157
-
158
- await callback(message)
159
-
160
- expect(chan.nack).toHaveBeenCalledWith(message, false, requeue)
161
- })
162
-
163
144
  it('should ignore Channel ended exception', async () => {
164
145
  topology.acknowledgments = true
165
146
 
@@ -844,10 +825,7 @@ describe('diagnostics', () => {
844
825
  expect(resumed).toStrictEqual(true)
845
826
  })
846
827
 
847
- it.each(/** @type {[string, boolean][]} */ [
848
- ['', true],
849
- [' not', false]
850
- ])('should%s emit `discard` event', async (_, redelivered) => {
828
+ it('should emit `discard` event', async () => {
851
829
  jest.clearAllMocks()
852
830
 
853
831
  topology.acknowledgments = true
@@ -866,14 +844,12 @@ describe('diagnostics', () => {
866
844
 
867
845
  const callback = /** @type {Function} */ chan.consume.mock.calls[0][1]
868
846
  const content = randomBytes(8)
869
- const properties = {}
870
- const fields = { redelivered }
871
- const message = /** @type {comq.amqp.Message} */ { content, properties, fields }
847
+ const properties = { headers: { 'x-attempt': 5 } }
848
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
872
849
 
873
850
  await callback(message)
874
851
 
875
- if (redelivered) expect(listener).toHaveBeenCalledWith(message, exception)
876
- else expect(listener).not.toHaveBeenCalled()
852
+ expect(listener).toHaveBeenCalledWith(message, exception)
877
853
  })
878
854
  })
879
855