comq 0.24.0 → 0.24.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/heartbeat.feature +16 -0
- package/features/rpc.call.recovery.feature +2 -2
- package/features/rpc.shards.retransmission.feature +46 -0
- package/features/shards.feature +1 -1
- package/features/silence.feature +3 -2
- package/features/steps/.types/context.d.ts +6 -1
- package/features/steps/brokers.js +1 -1
- package/features/steps/connection.js +23 -0
- package/features/steps/context.js +1 -2
- package/features/steps/hooks.js +0 -1
- package/features/steps/network.js +11 -2
- package/features/steps/networks.js +31 -2
- package/features/steps/retransmission.js +165 -0
- package/features/steps/rpc.js +0 -6
- package/package.json +15 -1
- package/readme.md +9 -4
- package/source/channel.js +3 -1
- package/source/connection.js +9 -86
- package/source/io.js +90 -15
- package/source/shards/channel.js +73 -12
- package/test/connection.mock.js +3 -2
- package/test/connection.test.js +15 -77
- package/test/io.call.test.js +5 -3
- package/test/io.reply.test.js +5 -3
- package/test/io.request.test.js +264 -2
- package/test/memory.test.js +0 -2
- package/test/shards/channel.test.js +168 -2
- package/types/channel.d.ts +13 -4
- package/types/io.d.ts +9 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@heavy
|
|
2
|
+
Feature: Idle connection tolerance
|
|
3
|
+
|
|
4
|
+
A connection that says nothing but heartbeats is as healthy as a busy one, and is kept for as
|
|
5
|
+
long as the broker goes on answering them, however long that is.
|
|
6
|
+
|
|
7
|
+
Background:
|
|
8
|
+
Given 1s AMQP heartbeat
|
|
9
|
+
|
|
10
|
+
Scenario: Keeping an idle connection
|
|
11
|
+
Given an active connection to the broker
|
|
12
|
+
Then the connection is not lost for 20 seconds
|
|
13
|
+
|
|
14
|
+
Scenario: Keeping an idle sharded connection
|
|
15
|
+
Given an active sharded connection
|
|
16
|
+
Then the connection is not lost for 20 seconds
|
|
@@ -17,14 +17,14 @@ Feature: Calls under a key across failures
|
|
|
17
17
|
it the key. The holder finds the key taken until the broker lets that connection go, and
|
|
18
18
|
claims it then.
|
|
19
19
|
|
|
20
|
-
Given
|
|
20
|
+
Given 1s AMQP heartbeat
|
|
21
21
|
And a network that can go silent
|
|
22
22
|
And an active connection to the broker
|
|
23
23
|
And a holder answering `echo` under the `a` key:
|
|
24
24
|
"""
|
|
25
25
|
(payload) => payload
|
|
26
26
|
"""
|
|
27
|
-
When the network goes silent
|
|
27
|
+
When the network goes silent, with the broker still hearing heartbeats
|
|
28
28
|
Then the connection is lost within 10 seconds
|
|
29
29
|
And the holder finds its key taken while the broker holds the silent connection
|
|
30
30
|
When the silent connection is let go
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
@heavy
|
|
2
|
+
Feature: Re-sending the requests a lost shard held
|
|
3
|
+
|
|
4
|
+
A Request is re-sent when the shard it went through is lost, since its Reply may be lost with it.
|
|
5
|
+
One that went through a shard that stays is answered through that shard, and is never re-sent
|
|
6
|
+
because another one was lost: a Request is not assumed to be idempotent.
|
|
7
|
+
|
|
8
|
+
Each scenario first takes one broker away from the consumer, so that the Requests it sends go
|
|
9
|
+
through the other one, and gives the broker back before losing one of them again.
|
|
10
|
+
|
|
11
|
+
Background:
|
|
12
|
+
Given 1s AMQP heartbeat
|
|
13
|
+
And a network that can go silent
|
|
14
|
+
And an active sharded connection
|
|
15
|
+
And a producer replying `echo` queue
|
|
16
|
+
# the consumer's channels exist on both brokers before one of them is taken away
|
|
17
|
+
And the consumer sends a request to the `echo` queue
|
|
18
|
+
And the consumer receives the reply
|
|
19
|
+
And a producer on another connection counting the requests to the `numbered` queue, answering when told
|
|
20
|
+
|
|
21
|
+
Scenario: Requests through the broker that stays are not re-sent when the other one is lost
|
|
22
|
+
Given the consumer's connection to broker 1 is cut
|
|
23
|
+
When the consumer sends 10 numbered requests to the `numbered` queue
|
|
24
|
+
And the consumer's connection to broker 1 is restored
|
|
25
|
+
And the consumer's connection to broker 1 is cut
|
|
26
|
+
And the producer answers
|
|
27
|
+
Then every numbered request is answered within 10 seconds
|
|
28
|
+
And every numbered request has been executed once
|
|
29
|
+
|
|
30
|
+
Scenario: Replies to the requests through the broker that stays are not lost with the other one
|
|
31
|
+
Given the consumer's connection to broker 1 is cut
|
|
32
|
+
When the consumer sends 10 numbered requests to the `numbered` queue
|
|
33
|
+
And the consumer's connection to broker 1 is restored
|
|
34
|
+
And the consumer's network to broker 1 goes silent
|
|
35
|
+
And the producer answers
|
|
36
|
+
Then every numbered request is answered within 15 seconds
|
|
37
|
+
And every numbered request has been executed once
|
|
38
|
+
|
|
39
|
+
Scenario: Requests through the broker that is lost are re-sent and answered
|
|
40
|
+
Given the consumer's connection to broker 0 is cut
|
|
41
|
+
When the consumer sends 10 numbered requests to the `numbered` queue
|
|
42
|
+
And the consumer's connection to broker 0 is restored
|
|
43
|
+
And the consumer's network to broker 1 goes silent
|
|
44
|
+
And the producer answers
|
|
45
|
+
Then every numbered request is answered within 15 seconds
|
|
46
|
+
And every numbered request has been executed twice
|
package/features/shards.feature
CHANGED
|
@@ -49,7 +49,7 @@ Feature: Sharded Connection
|
|
|
49
49
|
Then the broker is up
|
|
50
50
|
|
|
51
51
|
Scenario: Shard stops responding while requests are sent
|
|
52
|
-
Given
|
|
52
|
+
Given 1s AMQP heartbeat
|
|
53
53
|
And an active sharded connection
|
|
54
54
|
And a producer replying `stalled` queue
|
|
55
55
|
When the consumer sends 10 requests to the `stalled` queue as a broker freezes
|
package/features/silence.feature
CHANGED
|
@@ -2,10 +2,11 @@ Feature: Silent Connection Tolerance
|
|
|
2
2
|
|
|
3
3
|
A connection that has gone silent is what a machine that woke from sleep is
|
|
4
4
|
left with: the socket is open, neither the broker nor the operating system
|
|
5
|
-
reports anything, and nothing but the
|
|
5
|
+
reports anything, and nothing but the missing heartbeats tell it from an idle
|
|
6
|
+
connection.
|
|
6
7
|
|
|
7
8
|
Background:
|
|
8
|
-
Given
|
|
9
|
+
Given 1s AMQP heartbeat
|
|
9
10
|
And a network that can go silent
|
|
10
11
|
|
|
11
12
|
Scenario: Restoring a connection that has gone silent
|
|
@@ -49,7 +49,11 @@ declare namespace comq.features {
|
|
|
49
49
|
readonly address: string
|
|
50
50
|
|
|
51
51
|
open(): Promise<void>
|
|
52
|
-
silence(): void
|
|
52
|
+
silence(heard?: boolean): void
|
|
53
|
+
refuse(): void
|
|
54
|
+
admit(): void
|
|
55
|
+
release(): void
|
|
56
|
+
cut(): void
|
|
53
57
|
close(): Promise<void>
|
|
54
58
|
}
|
|
55
59
|
|
|
@@ -57,6 +61,7 @@ declare namespace comq.features {
|
|
|
57
61
|
client: Socket
|
|
58
62
|
upstream: Socket
|
|
59
63
|
silent: boolean
|
|
64
|
+
heartbeat?: NodeJS.Timeout
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
}
|
|
@@ -139,7 +139,7 @@ const actions = {
|
|
|
139
139
|
await docker('docker', ['kill', `comq-rmq-${n}`])
|
|
140
140
|
},
|
|
141
141
|
// a frozen broker keeps its connections open while answering nothing, which is
|
|
142
|
-
// what a publisher cannot tell from a working one until
|
|
142
|
+
// what a publisher cannot tell from a working one until its heartbeats go missing
|
|
143
143
|
frozen: async (n = 0) => {
|
|
144
144
|
await docker('docker', ['pause', `comq-rmq-${n}`])
|
|
145
145
|
}
|
|
@@ -5,6 +5,16 @@ const { timeout } = require('../../test/helpers')
|
|
|
5
5
|
|
|
6
6
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
7
7
|
|
|
8
|
+
Given('{number}s AMQP heartbeat',
|
|
9
|
+
/**
|
|
10
|
+
* A connection that has received nothing for two heartbeats is taken for lost.
|
|
11
|
+
*
|
|
12
|
+
* @param {number} heartbeat
|
|
13
|
+
*/
|
|
14
|
+
function (heartbeat) {
|
|
15
|
+
global.COMQ_TESTING_AMQP_HEARTBEAT = heartbeat
|
|
16
|
+
})
|
|
17
|
+
|
|
8
18
|
Given('an active connection to the broker',
|
|
9
19
|
/**
|
|
10
20
|
* @this {comq.features.Context}
|
|
@@ -176,6 +186,19 @@ Then('the connection is lost within {number} second(s)',
|
|
|
176
186
|
await emitted.call(this, 'close', seconds, 'connection was not lost')
|
|
177
187
|
})
|
|
178
188
|
|
|
189
|
+
Then('the connection is not lost for {number} seconds',
|
|
190
|
+
/**
|
|
191
|
+
* @param {number} seconds
|
|
192
|
+
* @this {comq.features.Context}
|
|
193
|
+
*/
|
|
194
|
+
async function (seconds) {
|
|
195
|
+
delete this.events.close
|
|
196
|
+
|
|
197
|
+
await timeout(seconds * 1000)
|
|
198
|
+
|
|
199
|
+
assert.equal(this.events.close, undefined, 'The connection was lost')
|
|
200
|
+
})
|
|
201
|
+
|
|
179
202
|
Then('publishing is {paused-event} within {number} second(s)',
|
|
180
203
|
/**
|
|
181
204
|
* @param {'paused' | 'resumed'} key
|
|
@@ -114,8 +114,7 @@ class Context extends World {
|
|
|
114
114
|
const url = PROTOCOL + user + ':' + password + '@' + address
|
|
115
115
|
const query = []
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
// interval at which a healthy broker is expected to say something
|
|
117
|
+
// a connection gone silent is told by its missing heartbeats, so a short one tells it soon
|
|
119
118
|
if (global.COMQ_TESTING_AMQP_HEARTBEAT !== undefined) {
|
|
120
119
|
query.push('heartbeat=' + global.COMQ_TESTING_AMQP_HEARTBEAT)
|
|
121
120
|
}
|
package/features/steps/hooks.js
CHANGED
|
@@ -26,6 +26,14 @@ When('the network goes silent',
|
|
|
26
26
|
silence.call(this)
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
+
When('the network goes silent, with the broker still hearing heartbeats',
|
|
30
|
+
/**
|
|
31
|
+
* @this {comq.features.Context}
|
|
32
|
+
*/
|
|
33
|
+
function () {
|
|
34
|
+
silence.call(this, true)
|
|
35
|
+
})
|
|
36
|
+
|
|
29
37
|
When('the network goes silent and refuses new connections',
|
|
30
38
|
/**
|
|
31
39
|
* @this {comq.features.Context}
|
|
@@ -66,11 +74,12 @@ When('the consumer sends a request to the {token} queue as the network goes sile
|
|
|
66
74
|
})
|
|
67
75
|
|
|
68
76
|
/**
|
|
77
|
+
* @param {boolean} [heard]
|
|
69
78
|
* @this {comq.features.Context}
|
|
70
79
|
*/
|
|
71
|
-
function silence () {
|
|
80
|
+
function silence (heard) {
|
|
72
81
|
// only a connection lost from now on counts as lost
|
|
73
82
|
delete this.events.close
|
|
74
83
|
|
|
75
|
-
for (const network of this.networks) network.silence()
|
|
84
|
+
for (const network of this.networks) network.silence(heard)
|
|
76
85
|
}
|
|
@@ -48,9 +48,19 @@ class Network {
|
|
|
48
48
|
/**
|
|
49
49
|
* Only the tunnels that are already open go silent: a connection established
|
|
50
50
|
* afterwards is forwarded as usual, just as it is once a machine is awake.
|
|
51
|
+
*
|
|
52
|
+
* The broker notices a silent connection by its heartbeat as soon as the client does. One
|
|
53
|
+
* that is to be held by the broker for as long as the scenario needs is kept heard from:
|
|
54
|
+
* heartbeats are sent to the broker in the client's stead, until the network lets it go.
|
|
55
|
+
*
|
|
56
|
+
* @param {boolean} [heard] whether the broker goes on hearing heartbeats
|
|
51
57
|
*/
|
|
52
|
-
silence () {
|
|
53
|
-
for (const tunnel of this.#tunnels)
|
|
58
|
+
silence (heard = false) {
|
|
59
|
+
for (const tunnel of this.#tunnels) {
|
|
60
|
+
tunnel.silent = true
|
|
61
|
+
|
|
62
|
+
if (heard) tunnel.heartbeat ??= setInterval(() => tunnel.upstream.write(HEARTBEAT), HEARTBEAT_MS)
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
/**
|
|
@@ -65,6 +75,17 @@ class Network {
|
|
|
65
75
|
this.#refusing = false
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Takes the broker away from whoever is connected through this network: the tunnels that are
|
|
80
|
+
* open are closed at both ends, and a connection attempted from now on is refused until the
|
|
81
|
+
* network admits it.
|
|
82
|
+
*/
|
|
83
|
+
cut () {
|
|
84
|
+
this.refuse()
|
|
85
|
+
|
|
86
|
+
for (const tunnel of this.#tunnels) this.#collapse(tunnel)
|
|
87
|
+
}
|
|
88
|
+
|
|
68
89
|
/**
|
|
69
90
|
* Closes the tunnels that went silent, at both ends, so the broker learns that the
|
|
70
91
|
* connection it was still holding is gone — the moment its heartbeat would otherwise tell it.
|
|
@@ -117,10 +138,18 @@ class Network {
|
|
|
117
138
|
* @param {comq.features.Tunnel} tunnel
|
|
118
139
|
*/
|
|
119
140
|
#collapse (tunnel) {
|
|
141
|
+
clearInterval(tunnel.heartbeat)
|
|
142
|
+
|
|
120
143
|
this.#tunnels.delete(tunnel)
|
|
121
144
|
tunnel.client.destroy()
|
|
122
145
|
tunnel.upstream.destroy()
|
|
123
146
|
}
|
|
124
147
|
}
|
|
125
148
|
|
|
149
|
+
/** An AMQP heartbeat frame: type 8, channel 0, no payload, and the frame end. */
|
|
150
|
+
const HEARTBEAT = Buffer.from([8, 0, 0, 0, 0, 0, 0, 0xCE])
|
|
151
|
+
|
|
152
|
+
/** Often enough for the shortest heartbeat a scenario asks for. */
|
|
153
|
+
const HEARTBEAT_MS = 250
|
|
154
|
+
|
|
126
155
|
exports.Network = Network
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { Promex } = require('promex')
|
|
5
|
+
const { Given, When, Then, After } = require('@cucumber/cucumber')
|
|
6
|
+
|
|
7
|
+
const { connect } = require('../../')
|
|
8
|
+
const { getAddress, USER, PASSWORD, BROKERS_AMOUNT } = require('./brokers')
|
|
9
|
+
const { timeout, until } = require('../../test/helpers')
|
|
10
|
+
|
|
11
|
+
Given('a producer on another connection counting the requests to the {token} queue, answering when told',
|
|
12
|
+
/**
|
|
13
|
+
* Connected to every broker directly, so that nothing done to the consumer's network reaches it.
|
|
14
|
+
* It takes every Request as it comes, counts how many times it has been handed each of them, and
|
|
15
|
+
* holds the replies back until it is told to answer: whatever the consumer re-sends by then is
|
|
16
|
+
* counted, however quickly the first attempt would have been answered.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} queue
|
|
19
|
+
* @this {comq.features.Context}
|
|
20
|
+
*/
|
|
21
|
+
async function (queue) {
|
|
22
|
+
const urls = Array.from({ length: BROKERS_AMOUNT }, (_, n) =>
|
|
23
|
+
`amqp://${USER}:${PASSWORD}@${getAddress(n)}`)
|
|
24
|
+
|
|
25
|
+
this.producer = await connect(...urls)
|
|
26
|
+
this.executions = new Map()
|
|
27
|
+
this.answering = new Promex()
|
|
28
|
+
|
|
29
|
+
await this.producer.reply(queue, async ({ id }) => {
|
|
30
|
+
this.executions.set(id, (this.executions.get(id) ?? 0) + 1)
|
|
31
|
+
|
|
32
|
+
await this.answering
|
|
33
|
+
|
|
34
|
+
return id
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
When('the producer answers',
|
|
39
|
+
/**
|
|
40
|
+
* @this {comq.features.Context}
|
|
41
|
+
*/
|
|
42
|
+
function () {
|
|
43
|
+
this.answering.resolve()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
When('the consumer sends {number} numbered requests to the {token} queue',
|
|
47
|
+
/**
|
|
48
|
+
* @param {number} amount
|
|
49
|
+
* @param {string} queue
|
|
50
|
+
* @this {comq.features.Context}
|
|
51
|
+
*/
|
|
52
|
+
function (amount, queue) {
|
|
53
|
+
this.numbered = Array.from({ length: amount }, (_, id) => {
|
|
54
|
+
const reply = this.io.request(queue, { id })
|
|
55
|
+
|
|
56
|
+
// asserted on by a later step, and an unhandled rejection would end the run before it
|
|
57
|
+
reply.catch(() => undefined)
|
|
58
|
+
|
|
59
|
+
return reply
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
When('the consumer\'s connection to broker {number} is cut',
|
|
64
|
+
/**
|
|
65
|
+
* The broker is taken away from the consumer alone: the connection closes at both ends, and the
|
|
66
|
+
* consumer is refused until it is restored. Returns once the consumer has noticed, so that what
|
|
67
|
+
* it sends next goes to the other broker.
|
|
68
|
+
*
|
|
69
|
+
* @param {number} broker
|
|
70
|
+
* @this {comq.features.Context}
|
|
71
|
+
*/
|
|
72
|
+
async function (broker) {
|
|
73
|
+
let lost = false
|
|
74
|
+
|
|
75
|
+
this.io.diagnose('close', (_error, index) => { if (index === broker) lost = true })
|
|
76
|
+
this.networks[broker].cut()
|
|
77
|
+
|
|
78
|
+
assert.equal(await until(() => lost, 10_000), true,
|
|
79
|
+
`The consumer's connection to broker ${broker} was not lost`)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
When('the consumer\'s connection to broker {number} is restored',
|
|
83
|
+
/**
|
|
84
|
+
* Returns once the consumer's channels to that broker have recovered, which is when the
|
|
85
|
+
* requests it may have lost with it are re-sent.
|
|
86
|
+
*
|
|
87
|
+
* @param {number} broker
|
|
88
|
+
* @this {comq.features.Context}
|
|
89
|
+
*/
|
|
90
|
+
async function (broker) {
|
|
91
|
+
const recovered = new Set()
|
|
92
|
+
|
|
93
|
+
this.io.diagnose('recover', (type, index) => { if (index === broker) recovered.add(type) })
|
|
94
|
+
this.networks[broker].admit()
|
|
95
|
+
|
|
96
|
+
const restored = () => recovered.has('request') && recovered.has('reply')
|
|
97
|
+
|
|
98
|
+
assert.equal(await until(restored, 20_000), true,
|
|
99
|
+
`The consumer's channels to broker ${broker} have not recovered`)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
When('the consumer\'s network to broker {number} goes silent',
|
|
103
|
+
/**
|
|
104
|
+
* @param {number} broker
|
|
105
|
+
* @this {comq.features.Context}
|
|
106
|
+
*/
|
|
107
|
+
function (broker) {
|
|
108
|
+
this.networks[broker].silence()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
Then('every numbered request is answered within {number} seconds',
|
|
112
|
+
/**
|
|
113
|
+
* @param {number} seconds
|
|
114
|
+
* @this {comq.features.Context}
|
|
115
|
+
*/
|
|
116
|
+
async function (seconds) {
|
|
117
|
+
const answered = new Array(this.numbered.length).fill(false)
|
|
118
|
+
|
|
119
|
+
this.numbered.forEach((reply, id) => reply.then(() => { answered[id] = true }, () => undefined))
|
|
120
|
+
|
|
121
|
+
await until(() => answered.every(Boolean), seconds * 1000)
|
|
122
|
+
|
|
123
|
+
const unanswered = answered.flatMap((yes, id) => yes ? [] : [id])
|
|
124
|
+
|
|
125
|
+
assert.deepEqual(unanswered, [], `Requests ${unanswered.join(', ')} are not answered`)
|
|
126
|
+
|
|
127
|
+
for (const [id, reply] of this.numbered.entries()) assert.equal(await reply, id, 'Reply mismatch')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
Then('every numbered request has been executed {word}',
|
|
131
|
+
/**
|
|
132
|
+
* A re-sent Request is taken as soon as it arrives, so a moment is given for any copy still on
|
|
133
|
+
* its way to the producer to be counted.
|
|
134
|
+
*
|
|
135
|
+
* @param {'once' | 'twice'} times
|
|
136
|
+
* @this {comq.features.Context}
|
|
137
|
+
*/
|
|
138
|
+
async function (times) {
|
|
139
|
+
const expected = TIMES[times]
|
|
140
|
+
|
|
141
|
+
await timeout(SETTLE_MS)
|
|
142
|
+
|
|
143
|
+
const executed = this.numbered.map((_, id) => this.executions.get(id) ?? 0)
|
|
144
|
+
const wrong = executed.flatMap((count, id) => count === expected ? [] : [`${id}: ${count}`])
|
|
145
|
+
|
|
146
|
+
assert.deepEqual(wrong, [], `Requests executed other than ${times} (id: times): ${wrong.join(', ')}`)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
After(
|
|
150
|
+
/**
|
|
151
|
+
* @this {comq.features.Context}
|
|
152
|
+
*/
|
|
153
|
+
async function () {
|
|
154
|
+
this.answering?.resolve()
|
|
155
|
+
await this.producer?.close()
|
|
156
|
+
|
|
157
|
+
this.producer = undefined
|
|
158
|
+
this.executions = undefined
|
|
159
|
+
this.answering = undefined
|
|
160
|
+
this.numbered = undefined
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const TIMES = { once: 1, twice: 2 }
|
|
164
|
+
|
|
165
|
+
const SETTLE_MS = 1000
|
package/features/steps/rpc.js
CHANGED
|
@@ -88,12 +88,6 @@ Given('idle timeout is set to {number}ms',
|
|
|
88
88
|
global.COMQ_TESTING_IDLE_INTERVAL = value
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
Given('watchdog interval is set to {number}ms with {number}s AMQP heartbeat',
|
|
92
|
-
function (watchdog, heartbeat) {
|
|
93
|
-
global.COMQ_TESTING_WATCHDOG_INTERVAL = watchdog
|
|
94
|
-
global.COMQ_TESTING_AMQP_HEARTBEAT = heartbeat
|
|
95
|
-
})
|
|
96
|
-
|
|
97
91
|
Given('a producer replying {token} queue',
|
|
98
92
|
/**
|
|
99
93
|
* @param {string} queue
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -67,5 +67,19 @@
|
|
|
67
67
|
"env": [
|
|
68
68
|
"jest"
|
|
69
69
|
]
|
|
70
|
+
},
|
|
71
|
+
"overrides": {
|
|
72
|
+
"test-exclude": "^8.0.0",
|
|
73
|
+
"rimraf": "^6.1.3",
|
|
74
|
+
"archiver-utils": {
|
|
75
|
+
"glob": "^13.0.6"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"allowScripts": {
|
|
79
|
+
"@parcel/watcher": false,
|
|
80
|
+
"cpu-features": false,
|
|
81
|
+
"protobufjs": false,
|
|
82
|
+
"ssh2": false,
|
|
83
|
+
"unrs-resolver": false
|
|
70
84
|
}
|
|
71
85
|
}
|
package/readme.md
CHANGED
|
@@ -457,8 +457,9 @@ Reconnection attempts will be made indefinitely, with intervals increasing up to
|
|
|
457
457
|
Unless the URL sets one, a 15 second heartbeat is requested, so that a connection that is gone
|
|
458
458
|
without a word, such as after a machine wakes from sleep, is noticed within a minute instead of
|
|
459
459
|
being left to whatever the broker suggests.
|
|
460
|
-
A connection that
|
|
461
|
-
the operating system have reported, since neither is guaranteed to report anything
|
|
460
|
+
A connection that has received nothing for two heartbeats is closed and restored regardless of what
|
|
461
|
+
the broker and the operating system have reported, since neither is guaranteed to report anything
|
|
462
|
+
at all. A connection that is merely idle receives the broker's heartbeats, and is kept.
|
|
462
463
|
Requesting `heartbeat=0` disables both.
|
|
463
464
|
If the broker rejects the connection, for example, due to access being denied, an exception will be thrown.
|
|
464
465
|
Once reconnected, the topology will be recovered, and any unanswered requests and unconfirmed events will be
|
|
@@ -480,6 +481,10 @@ connection is re-established.
|
|
|
480
481
|
|
|
481
482
|
Incoming messages are consumed from all shards.
|
|
482
483
|
|
|
484
|
+
A Reply goes back through the shard its Request arrived on, while that shard is reachable. When a
|
|
485
|
+
shard is lost, the Requests sent through it that are still unanswered are re-sent through the
|
|
486
|
+
others, since their Replies may be lost with it; a Request sent through another shard is not.
|
|
487
|
+
|
|
483
488
|
`async connect(...shards: string[]): IO`
|
|
484
489
|
|
|
485
490
|
Returns an instance of `IO` once connections to the shards are established.
|
|
@@ -795,8 +800,8 @@ Subscribe to one of the diagnostic events:
|
|
|
795
800
|
an argument.
|
|
796
801
|
- `drain`: back pressure is removed from a channel. Channel type is passed.
|
|
797
802
|
- `remove`: channel is removed from the [pool](#sharded-connection), having failed to publish.
|
|
798
|
-
- `lost`: a shard has lost its connection, hence the requests
|
|
799
|
-
re-sent. Channel type is passed.
|
|
803
|
+
- `lost`: a shard has lost its connection, hence the requests sent through it that are awaiting
|
|
804
|
+
their replies are re-sent. Channel type is passed.
|
|
800
805
|
- `recover`: channel's topology is recovered. Channel type is passed.
|
|
801
806
|
- `discard`: message is [parked](#parked-messages), having run out of attempts. Channel type,
|
|
802
807
|
raw [amqp message object](https://amqp-node.github.io/amqplib/channel_api.html#channel_consume)
|
package/source/channel.js
CHANGED
|
@@ -70,7 +70,9 @@ class Channel {
|
|
|
70
70
|
this.#failfast = index !== undefined
|
|
71
71
|
this.#release = release
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
// a shard's publish fails rather than waits for its connection to come back: the pool
|
|
74
|
+
// fails it over to another shard, and whoever published it is told where it went
|
|
75
|
+
if (this.#failfast) failsafe.disable(this.send, this.publish, this.route)
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
async create () {
|