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.
- 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 +21 -5
- package/test/channel.test.js +11 -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.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
|
|
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
|
@@ -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
|
|
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 (
|
|
273
|
-
else
|
|
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.
|
|
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
|
package/test/channel.test.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
876
|
-
else expect(listener).not.toHaveBeenCalled()
|
|
852
|
+
expect(listener).toHaveBeenCalledWith(message, exception)
|
|
877
853
|
})
|
|
878
854
|
})
|
|
879
855
|
|