comq 0.10.0 → 0.10.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/rpc.streams.feature +9 -9
- package/features/rpc.streams.shards.feature +3 -3
- package/features/steps/broker.js +2 -0
- package/features/steps/rpc.js +5 -5
- package/lerna.json +1 -1
- package/package.json +2 -2
- package/readme.md +4 -8
- package/source/.io/ReplyStream.js +6 -9
- package/source/io.js +50 -31
- package/types/io.d.ts +1 -4
|
@@ -5,14 +5,14 @@ Feature: Reply streams
|
|
|
5
5
|
And heartbeat interval is set to 100ms
|
|
6
6
|
And idle timeout is set to 150ms
|
|
7
7
|
|
|
8
|
-
Scenario:
|
|
8
|
+
Scenario: Requesting a stream
|
|
9
9
|
Given function replying `get_numbers` queue:
|
|
10
10
|
"""
|
|
11
11
|
function * ({ limit }) {
|
|
12
12
|
for (let i = 0; i < limit; i++) yield i
|
|
13
13
|
}
|
|
14
14
|
"""
|
|
15
|
-
When the consumer
|
|
15
|
+
When the consumer requests a stream with the following request to the `get_numbers` queue:
|
|
16
16
|
"""yaml
|
|
17
17
|
limit: 5
|
|
18
18
|
"""
|
|
@@ -21,11 +21,11 @@ Feature: Reply streams
|
|
|
21
21
|
[0, 1, 2, 3, 4]
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
Scenario:
|
|
24
|
+
Scenario: Requesting streams concurrently
|
|
25
25
|
Given a number generator with 10ms increasing delay replying `get_numbers` queue
|
|
26
|
-
When the consumer0
|
|
26
|
+
When the consumer0 requests a stream with request to the `get_numbers` queue
|
|
27
27
|
And the consumer0 receives the stream
|
|
28
|
-
And the consumer1
|
|
28
|
+
And the consumer1 requests a stream with request to the `get_numbers` queue
|
|
29
29
|
And the consumer1 receives the stream
|
|
30
30
|
And after 300ms
|
|
31
31
|
Then the consumer0 has received the stream:
|
|
@@ -39,7 +39,7 @@ Feature: Reply streams
|
|
|
39
39
|
|
|
40
40
|
Scenario: Interrupting a stream
|
|
41
41
|
Given a number generator with 20ms increasing delay replying `get_numbers` queue
|
|
42
|
-
When the consumer
|
|
42
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
43
43
|
And the consumer interrupts the stream after 3 replies
|
|
44
44
|
# feedback message must be delivered
|
|
45
45
|
And after 10ms
|
|
@@ -48,7 +48,7 @@ Feature: Reply streams
|
|
|
48
48
|
Scenario: Reply stream idle timeout
|
|
49
49
|
Given heartbeat interval is set to 300ms
|
|
50
50
|
And a number generator with 100ms increasing delay replying `get_numbers` queue
|
|
51
|
-
When the consumer
|
|
51
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
52
52
|
Then the consumer receives the stream:
|
|
53
53
|
"""yaml
|
|
54
54
|
[0, 1, 3]
|
|
@@ -59,14 +59,14 @@ Feature: Reply streams
|
|
|
59
59
|
Scenario: Reply stream heartbeat
|
|
60
60
|
# delay will exceed the idle timeout on the 3rd reply
|
|
61
61
|
Given a number generator with 90ms increasing delay replying `get_numbers` queue
|
|
62
|
-
When the consumer
|
|
62
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
63
63
|
Then the consumer interrupts the stream after 5 replies
|
|
64
64
|
And after 200ms
|
|
65
65
|
Then the generator is destroyed
|
|
66
66
|
|
|
67
67
|
Scenario: Broker crashes while streaming reply
|
|
68
68
|
Given a number generator with 90ms increasing delay replying `get_numbers` queue
|
|
69
|
-
When the consumer
|
|
69
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
70
70
|
Then the consumer receives the stream
|
|
71
71
|
And after 300ms
|
|
72
72
|
Then the broker has crashed
|
|
@@ -5,14 +5,14 @@ Feature: Reply streams over shards
|
|
|
5
5
|
And heartbeat interval is set to 100ms
|
|
6
6
|
And idle timeout is set to 150ms
|
|
7
7
|
|
|
8
|
-
Scenario:
|
|
8
|
+
Scenario: Requesting a stream using sharded connection
|
|
9
9
|
Given a generator replying `get_numbers` queue:
|
|
10
10
|
"""
|
|
11
11
|
function * ({ amount }) {
|
|
12
12
|
for (let i = 0; i < amount; i++) yield i
|
|
13
13
|
}
|
|
14
14
|
"""
|
|
15
|
-
When the consumer
|
|
15
|
+
When the consumer requests a stream with the following request to the `get_numbers` queue:
|
|
16
16
|
"""yaml
|
|
17
17
|
amount: 5
|
|
18
18
|
"""
|
|
@@ -23,7 +23,7 @@ Feature: Reply streams over shards
|
|
|
23
23
|
|
|
24
24
|
Scenario: Broker crashes while fetching a stream
|
|
25
25
|
Given a number generator with 90ms increasing delay replying `get_numbers` queue
|
|
26
|
-
When the consumer
|
|
26
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
27
27
|
And the consumer receives the stream
|
|
28
28
|
And after 300ms
|
|
29
29
|
And the broker has crashed
|
package/features/steps/broker.js
CHANGED
package/features/steps/rpc.js
CHANGED
|
@@ -136,7 +136,7 @@ When('the consumer sends a request to the {token} queue',
|
|
|
136
136
|
await send.call(this, queue, payload)
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
-
When('the consumer
|
|
139
|
+
When('the consumer requests a stream with the following request to the {token} queue:',
|
|
140
140
|
/**
|
|
141
141
|
* @param {string} queue
|
|
142
142
|
* @param {string} yaml
|
|
@@ -148,7 +148,7 @@ When('the consumer fetches a stream with the following request to the {token} qu
|
|
|
148
148
|
await fetch.call(this, queue, payload)
|
|
149
149
|
})
|
|
150
150
|
|
|
151
|
-
When('the consumer
|
|
151
|
+
When('the consumer requests a stream with request to the {token} queue',
|
|
152
152
|
/**
|
|
153
153
|
* @param {string} queue
|
|
154
154
|
* @this {comq.features.Context}
|
|
@@ -157,7 +157,7 @@ When('the consumer fetches a stream with request to the {token} queue',
|
|
|
157
157
|
await fetch.call(this, queue, null)
|
|
158
158
|
})
|
|
159
159
|
|
|
160
|
-
When('the consumer{number}
|
|
160
|
+
When('the consumer{number} requests a stream with request to the {token} queue',
|
|
161
161
|
/**
|
|
162
162
|
* @param {number} number
|
|
163
163
|
* @param {string} queue
|
|
@@ -408,9 +408,9 @@ async function send (queue, payload) {
|
|
|
408
408
|
*/
|
|
409
409
|
async function fetch (queue, payload, number) {
|
|
410
410
|
if (number === undefined) {
|
|
411
|
-
this.stream = await this.io.
|
|
411
|
+
this.stream = await this.io.request(queue, payload)
|
|
412
412
|
} else {
|
|
413
|
-
this.streams[number] = await this.io.
|
|
413
|
+
this.streams[number] = await this.io.request(queue, payload)
|
|
414
414
|
this.streamsValues[number] = []
|
|
415
415
|
this.streamsEnded[number] = false
|
|
416
416
|
}
|
package/lerna.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"scripts": {
|
|
48
48
|
"prepare": "echo run: husky install",
|
|
49
49
|
"test": "standard && jest",
|
|
50
|
-
"features": "cucumber-js",
|
|
50
|
+
"features": "cucumber-js --publish-quiet",
|
|
51
51
|
"lint": "standard --fix --verbose | snazzy"
|
|
52
52
|
},
|
|
53
53
|
"standard": {
|
package/readme.md
CHANGED
|
@@ -202,12 +202,10 @@ await io.reply('get_numbers', function * ({ amount }) {
|
|
|
202
202
|
})
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
-
The Reply stream may be consumed by using the `IO.
|
|
206
|
-
|
|
207
|
-
`async IO.fetch(queue: string, payload: any, encoding?: string): Readable`
|
|
205
|
+
The Reply stream may be consumed by using the `IO.request` function:
|
|
208
206
|
|
|
209
207
|
```javascript
|
|
210
|
-
const stream = await io.
|
|
208
|
+
const stream = await io.request('get_numbers', { amount: 10 })
|
|
211
209
|
|
|
212
210
|
for await (const number of stream)
|
|
213
211
|
console.log(number)
|
|
@@ -220,8 +218,6 @@ for await (const number of stream)
|
|
|
220
218
|
<img alt="Reply topology" width="500" height="411" src="./docs/reply-stream-topology-light.jpg">
|
|
221
219
|
</picture>
|
|
222
220
|
|
|
223
|
-
On the first call of the `IO.fetch` for each request queue, an exclusive queue for replies is asserted (a stream queue).
|
|
224
|
-
|
|
225
221
|
When the producer function of `IO.reply` returns an Iterator for the first time across all request queues,
|
|
226
222
|
a control queue is asserted on the [Reply channel](#channels)
|
|
227
223
|
using the [reply topology](#exchanges-and-queues).
|
|
@@ -235,7 +231,7 @@ the request will be retransmitted upon reconnection.
|
|
|
235
231
|
|
|
236
232
|
A heartbeat message is sent to the `replyTo` queue whenever a Reply stream idles for 5 seconds.
|
|
237
233
|
If the Consumer of the reply stream doesn't receive a reply or a heartbeat message for 12 seconds, the stream returned
|
|
238
|
-
by `IO.
|
|
234
|
+
by `IO.request` is destroyed.
|
|
239
235
|
These intervals are not configurable.
|
|
240
236
|
|
|
241
237
|
An "end stream" message is sent to the `replyTo` queue when the Reply stream is finished.
|
|
@@ -247,7 +243,7 @@ See also [Reply stream shutdown](#reply-stream-shutdown).
|
|
|
247
243
|
:warning:
|
|
248
244
|
|
|
249
245
|
While consuming the Reply stream if the broker connection is lost,
|
|
250
|
-
or if the Consumer crashes or destroys the stream returned by `IO.
|
|
246
|
+
or if the Consumer crashes or destroys the stream returned by `IO.request`,
|
|
251
247
|
some of the values yielded by the Reply stream may be lost.
|
|
252
248
|
|
|
253
249
|
To avoid inconsistency, it is strongly recommended to use the Reply stream only with _safe_ Producers, which do not
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { IDLE_INTERVAL, control } = require('./const')
|
|
4
|
-
|
|
5
3
|
const { Readable } = require('node:stream')
|
|
4
|
+
const { promex } = require('@toa.io/generic')
|
|
5
|
+
const { IDLE_INTERVAL, control } = require('./const')
|
|
6
6
|
|
|
7
7
|
class ReplyStream extends Readable {
|
|
8
|
-
|
|
8
|
+
confirmation = promex()
|
|
9
9
|
|
|
10
10
|
/** @type {ReturnType<setTimeout> | null} */
|
|
11
11
|
#timeout = null
|
|
@@ -36,14 +36,12 @@ class ReplyStream extends Readable {
|
|
|
36
36
|
constructor (request, reply) {
|
|
37
37
|
super({ objectMode: true })
|
|
38
38
|
|
|
39
|
-
this.#confirmation = request.reply
|
|
40
39
|
this.#emitter = request.emitter
|
|
41
40
|
this.#correlationId = request.properties.correlationId
|
|
42
41
|
this.#idleInterval = global['COMQ_TESTING_IDLE_INTERVAL'] || IDLE_INTERVAL
|
|
43
42
|
this.#reply = reply
|
|
44
43
|
|
|
45
|
-
this.#emitter.on(this.#correlationId, this.
|
|
46
|
-
this.#confirmation.catch(this._clear.bind(this))
|
|
44
|
+
this.#emitter.on(this.#correlationId, this.arrange.bind(this))
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
_destroy (error, callback) {
|
|
@@ -59,9 +57,8 @@ class ReplyStream extends Readable {
|
|
|
59
57
|
/**
|
|
60
58
|
* @param {unknown} payload
|
|
61
59
|
* @param {comq.amqp.Properties} properties
|
|
62
|
-
* @private
|
|
63
60
|
*/
|
|
64
|
-
|
|
61
|
+
arrange (payload, properties) {
|
|
65
62
|
if (properties.headers.index !== this.#index) {
|
|
66
63
|
this._buffer(payload, properties)
|
|
67
64
|
|
|
@@ -113,7 +110,7 @@ class ReplyStream extends Readable {
|
|
|
113
110
|
switch (message) {
|
|
114
111
|
case control.ok:
|
|
115
112
|
this.#control = { properties }
|
|
116
|
-
this
|
|
113
|
+
this.confirmation.resolve()
|
|
117
114
|
break
|
|
118
115
|
case control.heartbeat:
|
|
119
116
|
break
|
package/source/io.js
CHANGED
|
@@ -32,7 +32,7 @@ class IO {
|
|
|
32
32
|
#emitters = new Map()
|
|
33
33
|
|
|
34
34
|
/** @type {comq.ReplyEmitter | null} */
|
|
35
|
-
#
|
|
35
|
+
#control = null
|
|
36
36
|
|
|
37
37
|
/** @type {Set<toa.generic.Promex>} */
|
|
38
38
|
#pendingReplies = new Set()
|
|
@@ -86,33 +86,12 @@ class IO {
|
|
|
86
86
|
)
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
const { buffer, properties, emitter, reply } = this.#createRequest(queue, payload, encoding)
|
|
90
|
-
|
|
91
|
-
emitter.once(properties.correlationId, reply.resolve)
|
|
92
|
-
|
|
93
|
-
await this.#requests.send(queue, buffer, properties)
|
|
94
|
-
|
|
95
|
-
return reply
|
|
96
|
-
}))
|
|
97
|
-
|
|
98
|
-
fetch = lazy(this, [this.#createRequestReplyChannels, this.#consumeReplies],
|
|
99
|
-
failsafe(this, this.#recover,
|
|
100
|
-
/**
|
|
101
|
-
* @param {string} queue
|
|
102
|
-
* @param {any} payload
|
|
103
|
-
* @param {comq.Encoding} [encoding]
|
|
104
|
-
* @returns {Promise<Readable>}
|
|
105
|
-
*/
|
|
106
|
-
async (queue, payload, encoding) => {
|
|
107
89
|
const request = this.#createRequest(queue, payload, encoding)
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
this.#addReplyStream(stream)
|
|
90
|
+
const reply = this.#createReply(request)
|
|
111
91
|
|
|
112
92
|
await this.#requests.send(queue, request.buffer, request.properties)
|
|
113
|
-
await request.reply // wait for the confirmation
|
|
114
93
|
|
|
115
|
-
return
|
|
94
|
+
return reply
|
|
116
95
|
}))
|
|
117
96
|
|
|
118
97
|
consume = lazy(this, this.#createEventChannel,
|
|
@@ -247,9 +226,9 @@ class IO {
|
|
|
247
226
|
? reply
|
|
248
227
|
: stream.Readable.from(reply)
|
|
249
228
|
|
|
250
|
-
this.#
|
|
229
|
+
this.#control ??= await this.#createControl()
|
|
251
230
|
|
|
252
|
-
const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#
|
|
231
|
+
const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#control,
|
|
253
232
|
(message, properties) => this.#reply(request, message, properties))
|
|
254
233
|
|
|
255
234
|
this.#addReplyPipe(pipe)
|
|
@@ -291,18 +270,29 @@ class IO {
|
|
|
291
270
|
const [buffer, contentType] = this.#encode(payload, encoding)
|
|
292
271
|
const emitter = this.#emitters.get(queue)
|
|
293
272
|
const correlationId = randomBytes(8).toString('hex')
|
|
294
|
-
const reply = this.#createReply()
|
|
295
273
|
|
|
296
274
|
/** @type {comq.amqp.Properties} */
|
|
297
275
|
const properties = { contentType, correlationId, replyTo: emitter.queue }
|
|
298
276
|
|
|
299
|
-
return { buffer, emitter,
|
|
277
|
+
return { buffer, emitter, properties }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {comq.Request} request
|
|
282
|
+
* @return {Promise<any>}
|
|
283
|
+
*/
|
|
284
|
+
#createReply (request) {
|
|
285
|
+
const reply = this.#createPendingReply()
|
|
286
|
+
|
|
287
|
+
request.emitter.once(request.properties.correlationId, this.#getReplyResolver(request, reply))
|
|
288
|
+
|
|
289
|
+
return reply
|
|
300
290
|
}
|
|
301
291
|
|
|
302
292
|
/**
|
|
303
293
|
* @return {toa.generic.Promex}
|
|
304
294
|
*/
|
|
305
|
-
#
|
|
295
|
+
#createPendingReply () {
|
|
306
296
|
const reply = promex()
|
|
307
297
|
|
|
308
298
|
this.#pendingReplies.add(reply)
|
|
@@ -314,11 +304,40 @@ class IO {
|
|
|
314
304
|
return reply
|
|
315
305
|
}
|
|
316
306
|
|
|
307
|
+
/**
|
|
308
|
+
* @param {comq.Request} request
|
|
309
|
+
* @param reply
|
|
310
|
+
*/
|
|
311
|
+
#getReplyResolver (request, reply) {
|
|
312
|
+
return async (payload, properties) => {
|
|
313
|
+
const isStream = properties.headers?.index !== undefined
|
|
314
|
+
|
|
315
|
+
if (isStream) {
|
|
316
|
+
const stream = this.#createReplyStream(request, payload, properties)
|
|
317
|
+
|
|
318
|
+
await stream.confirmation
|
|
319
|
+
|
|
320
|
+
reply.resolve(stream)
|
|
321
|
+
} else {
|
|
322
|
+
reply.resolve(payload)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#createReplyStream (request, payload, properties) {
|
|
328
|
+
const stream = new io.ReplyStream(request, this.#reply.bind(this))
|
|
329
|
+
|
|
330
|
+
stream.arrange(payload, properties)
|
|
331
|
+
this.#addReplyStream(/** @type {comq.Destroyable} */ stream)
|
|
332
|
+
|
|
333
|
+
return stream
|
|
334
|
+
}
|
|
335
|
+
|
|
317
336
|
/**
|
|
318
337
|
* @return {Promise<comq.ReplyEmitter>}
|
|
319
338
|
*/
|
|
320
|
-
async #
|
|
321
|
-
const queue = '
|
|
339
|
+
async #createControl () {
|
|
340
|
+
const queue = 'control'
|
|
322
341
|
|
|
323
342
|
await this.#consumeReplies(queue)
|
|
324
343
|
|
package/types/io.d.ts
CHANGED
|
@@ -22,18 +22,15 @@ declare namespace comq {
|
|
|
22
22
|
buffer: Buffer
|
|
23
23
|
emitter: ReplyEmitter
|
|
24
24
|
properties: _amqp.Properties
|
|
25
|
-
reply: any
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
interface IO extends _diagnostics.Diagnosable {
|
|
29
28
|
reply (queue: string, produce: Producer): Promise<void>
|
|
30
29
|
|
|
31
|
-
request<Reply = any, Request = any> (queue: string, payload: Request, encoding?: _encoding.Encoding): Promise<Reply>
|
|
30
|
+
request<Reply = any, Request = any> (queue: string, payload: Request, encoding?: _encoding.Encoding): Promise<Reply> | Promise<Readable>
|
|
32
31
|
|
|
33
32
|
request (queue: string, stream: Readable, encoding?: _encoding.Encoding): Promise<Readable>
|
|
34
33
|
|
|
35
|
-
fetch (queue: string, payload: any, encoding?: _encoding.Encoding): Promise<Readable>
|
|
36
|
-
|
|
37
34
|
consume<T = any> (exchange: string, group: string, consumer: Consumer<T>): Promise<void>
|
|
38
35
|
|
|
39
36
|
consume<T = any> (exchange: string, consumer: Consumer<T>): Promise<void>
|