comq 0.11.0 → 0.11.1

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.1",
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.1",
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
@@ -267,18 +267,29 @@ class Channel {
267
267
 
268
268
  this.#channel.ack(message)
269
269
  } catch (exception) {
270
- if (exception.message === 'Channel closed') return // the message will be requeued by the broker
270
+ if (exception.message === 'Channel closed') { return } // the message is requeued by the broker
271
271
 
272
- if (message.fields.redelivered) this.#discard(message, exception)
273
- else this.#requeue(message)
272
+ const redeliveries = message.properties.headers[REDELIVERY_HEADER] ?? 0
273
+
274
+ if (redeliveries >= MAX_REDELIVERIES) this.#discard(message, exception)
275
+ else {
276
+ await this.#requeue(message, redeliveries)
277
+ throw exception
278
+ }
274
279
  }
275
280
  }
276
281
 
277
282
  /**
278
283
  * @param {comq.amqp.Message} message
284
+ * @param {number} attempt
279
285
  */
280
- #requeue (message) {
281
- this.#channel.nack(message, false, true)
286
+ async #requeue (message, attempt) {
287
+ this.#channel.ack(message)
288
+
289
+ message.properties.headers[REDELIVERY_HEADER] = attempt + 1
290
+
291
+ await this.seal()
292
+ await this.#publish(message.fields.exchange, message.fields.routingKey, message.content, message.properties)
282
293
  }
283
294
 
284
295
  /**
@@ -358,6 +369,9 @@ const EXCLUSIVE = { exclusive: true }
358
369
 
359
370
  const INTERRUPTION = /** @type {Error} */ Symbol('internal interruption')
360
371
 
372
+ const MAX_REDELIVERIES = 5
373
+ const REDELIVERY_HEADER = 'x-attempt'
374
+
361
375
  function noop () {}
362
376
 
363
377
  exports.create = create
@@ -134,32 +134,6 @@ describe('acknowledgments', () => {
134
134
  else expect(options).toMatchObject({ noAck: true })
135
135
  })
136
136
 
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
137
  it('should ignore Channel ended exception', async () => {
164
138
  topology.acknowledgments = true
165
139
 
@@ -844,10 +818,7 @@ describe('diagnostics', () => {
844
818
  expect(resumed).toStrictEqual(true)
845
819
  })
846
820
 
847
- it.each(/** @type {[string, boolean][]} */ [
848
- ['', true],
849
- [' not', false]
850
- ])('should%s emit `discard` event', async (_, redelivered) => {
821
+ it('should emit `discard` event', async () => {
851
822
  jest.clearAllMocks()
852
823
 
853
824
  topology.acknowledgments = true
@@ -866,14 +837,12 @@ describe('diagnostics', () => {
866
837
 
867
838
  const callback = /** @type {Function} */ chan.consume.mock.calls[0][1]
868
839
  const content = randomBytes(8)
869
- const properties = {}
870
- const fields = { redelivered }
871
- const message = /** @type {comq.amqp.Message} */ { content, properties, fields }
840
+ const properties = { headers: { 'x-attempt': 5 } }
841
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
872
842
 
873
843
  await callback(message)
874
844
 
875
- if (redelivered) expect(listener).toHaveBeenCalledWith(message, exception)
876
- else expect(listener).not.toHaveBeenCalled()
845
+ expect(listener).toHaveBeenCalledWith(message, exception)
877
846
  })
878
847
  })
879
848