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.
- package/features/events.poison.feature +22 -0
- package/features/rpc.feature +0 -9
- package/features/steps/events.js +11 -0
- package/features/steps/poison.js +1 -1
- package/lerna.json +1 -1
- package/package.json +2 -2
- package/readme.md +11 -4
- package/source/channel.js +19 -5
- package/test/channel.test.js +4 -35
|
@@ -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
|
package/features/rpc.feature
CHANGED
|
@@ -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
|
package/features/steps/events.js
CHANGED
|
@@ -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
|
package/features/steps/poison.js
CHANGED
|
@@ -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(
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.11.
|
|
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
|
|
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,
|
|
409
|
-
|
|
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
|
|
270
|
+
if (exception.message === 'Channel closed') { return } // the message is requeued by the broker
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
|
|
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.
|
|
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
|
package/test/channel.test.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
876
|
-
else expect(listener).not.toHaveBeenCalled()
|
|
845
|
+
expect(listener).toHaveBeenCalledWith(message, exception)
|
|
877
846
|
})
|
|
878
847
|
})
|
|
879
848
|
|