comq 0.15.7 → 0.16.0
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/.releaserc +15 -6
- package/features/channels.feature +14 -0
- package/features/recovery.feature +30 -0
- package/features/silence.feature +46 -0
- package/features/steps/.types/context.d.ts +17 -0
- package/features/steps/backpressure.js +1 -1
- package/features/steps/broker.js +1 -1
- package/features/steps/brokers.js +1 -1
- package/features/steps/channels.js +45 -0
- package/features/steps/common.js +1 -1
- package/features/steps/connection.js +41 -1
- package/features/steps/context.js +24 -4
- package/features/steps/events.js +1 -1
- package/features/steps/hooks.js +2 -0
- package/features/steps/network.js +50 -0
- package/features/steps/networks.js +90 -0
- package/features/steps/parameters.js +6 -0
- package/features/steps/poison.js +1 -1
- package/features/steps/rpc.js +1 -1
- package/features/steps/tasks.js +1 -1
- package/package.json +8 -9
- package/readme.md +17 -2
- package/source/attributes/lazy.test.js +1 -1
- package/source/attributes/track.test.js +3 -3
- package/source/channel.js +21 -0
- package/source/connection.js +112 -12
- package/source/emitter.js +23 -1
- package/source/encoders/index.js +0 -2
- package/source/events.js +2 -2
- package/source/io.js +12 -5
- package/source/shards/channel.js +28 -7
- package/source/shards/connection.js +0 -5
- package/test/amqplib.mock.js +20 -4
- package/test/channel.fixtures.js +1 -1
- package/test/channel.test.js +44 -1
- package/test/connection.mock.js +2 -0
- package/test/connection.test.js +226 -6
- package/test/decode.test.js +0 -13
- package/test/emitter.test.js +71 -0
- package/test/encode.test.js +0 -9
- package/test/encodings.js +1 -1
- package/test/helpers.js +77 -0
- package/test/io.close.test.js +25 -3
- package/test/io.diagnostics.test.js +2 -2
- package/test/io.reply.test.js +2 -2
- package/test/io.request.test.js +16 -2
- package/test/pipeline.test.js +1 -1
- package/test/shards/channel.test.js +63 -9
- package/test/shards/connection.test.js +4 -3
- package/types/channel.d.ts +4 -0
- package/types/diagnostic.d.ts +2 -2
- package/types/encoding.d.ts +1 -2
- package/types/io.d.ts +2 -0
- package/AGENTS.md +0 -35
- package/CHANGELOG.md +0 -85
- package/source/encoders/msgpack.js +0 -6
package/.releaserc
CHANGED
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
"release"
|
|
4
4
|
],
|
|
5
5
|
"plugins": [
|
|
6
|
-
"@semantic-release/commit-analyzer",
|
|
7
|
-
"@semantic-release/release-notes-generator",
|
|
8
6
|
[
|
|
9
|
-
"@semantic-release/
|
|
7
|
+
"@semantic-release/commit-analyzer",
|
|
10
8
|
{
|
|
11
|
-
"
|
|
9
|
+
"preset": "conventionalcommits",
|
|
10
|
+
"releaseRules": [
|
|
11
|
+
{
|
|
12
|
+
"breaking": true,
|
|
13
|
+
"release": "minor"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
"@semantic-release/release-notes-generator",
|
|
20
|
+
{
|
|
21
|
+
"preset": "conventionalcommits"
|
|
12
22
|
}
|
|
13
23
|
],
|
|
14
24
|
[
|
|
@@ -19,8 +29,7 @@
|
|
|
19
29
|
{
|
|
20
30
|
"assets": [
|
|
21
31
|
"package.json",
|
|
22
|
-
"package-lock.json"
|
|
23
|
-
"CHANGELOG.md"
|
|
32
|
+
"package-lock.json"
|
|
24
33
|
],
|
|
25
34
|
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
|
|
26
35
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Feature: Channel exhaustion
|
|
2
|
+
|
|
3
|
+
A connection allocates channels from a range the broker negotiates, and once the range
|
|
4
|
+
is spent there is nothing a reconnection can do about it. The refusal is reported and
|
|
5
|
+
passed on rather than retried, so that whoever asked for a channel is told instead of
|
|
6
|
+
waiting for a recovery that is not coming.
|
|
7
|
+
|
|
8
|
+
Scenario: Reporting a connection with no channel left to allocate
|
|
9
|
+
Given the AMQP channel limit is set to 2
|
|
10
|
+
And an active connection to the broker
|
|
11
|
+
And a producer replying `echo` queue
|
|
12
|
+
When emitting an event to the `exhaustion` exchange is attempted
|
|
13
|
+
Then the exception is thrown: "No channels left to allocate"
|
|
14
|
+
And the `exhausted` event is emitted
|
|
@@ -84,6 +84,36 @@ Feature: Request-Reply Topology Recovery
|
|
|
84
84
|
5
|
|
85
85
|
"""
|
|
86
86
|
|
|
87
|
+
Scenario: Sending another request while the broker is down
|
|
88
|
+
|
|
89
|
+
The reply queue is already being consumed from, so the request is published as soon as the
|
|
90
|
+
request channel recovers, which is before the reply queue has been declared again
|
|
91
|
+
|
|
92
|
+
Given function replying `add_numbers` queue:
|
|
93
|
+
"""
|
|
94
|
+
({ a, b }) => { return a + b }
|
|
95
|
+
"""
|
|
96
|
+
When the consumer sends the following request to the `add_numbers` queue:
|
|
97
|
+
"""yaml
|
|
98
|
+
a: 1
|
|
99
|
+
b: 2
|
|
100
|
+
"""
|
|
101
|
+
Then the consumer receives the reply:
|
|
102
|
+
"""yaml
|
|
103
|
+
3
|
|
104
|
+
"""
|
|
105
|
+
When the broker has crashed
|
|
106
|
+
And the consumer sends the following request to the `add_numbers` queue:
|
|
107
|
+
"""yaml
|
|
108
|
+
a: 2
|
|
109
|
+
b: 3
|
|
110
|
+
"""
|
|
111
|
+
Then the broker is up
|
|
112
|
+
And the consumer receives the reply:
|
|
113
|
+
"""yaml
|
|
114
|
+
5
|
|
115
|
+
"""
|
|
116
|
+
|
|
87
117
|
Scenario: Defining emitter while the broker is down
|
|
88
118
|
Given the broker is down
|
|
89
119
|
And `logger` consuming events from the `numbers_added` exchange is expected
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Feature: Silent Connection Tolerance
|
|
2
|
+
|
|
3
|
+
A connection that has gone silent is what a machine that woke from sleep is
|
|
4
|
+
left with: the socket is open, neither the broker nor the operating system
|
|
5
|
+
reports anything, and nothing but the watchdog tells it from an idle connection.
|
|
6
|
+
|
|
7
|
+
Background:
|
|
8
|
+
Given watchdog interval is set to 2000ms with 60s AMQP heartbeat
|
|
9
|
+
And a network that can go silent
|
|
10
|
+
|
|
11
|
+
Scenario: Restoring a connection that has gone silent
|
|
12
|
+
Given an active connection to the broker
|
|
13
|
+
And a producer replying `echo` queue
|
|
14
|
+
When the network goes silent
|
|
15
|
+
Then the connection is lost within 10 seconds
|
|
16
|
+
When the consumer sends a request to the `echo` queue
|
|
17
|
+
Then the consumer receives the reply
|
|
18
|
+
|
|
19
|
+
Scenario: Restoring a sharded connection whose every shard has gone silent
|
|
20
|
+
Given an active sharded connection
|
|
21
|
+
And a producer replying `echo` queue
|
|
22
|
+
When the network goes silent
|
|
23
|
+
Then the connection is lost within 10 seconds
|
|
24
|
+
When the consumer sends a request to the `echo` queue
|
|
25
|
+
Then the consumer receives the reply
|
|
26
|
+
|
|
27
|
+
Scenario: Answering a request sent as the connection goes silent
|
|
28
|
+
Given an active connection to the broker
|
|
29
|
+
And a producer replying `echo` queue
|
|
30
|
+
When the consumer sends a request to the `echo` queue as the network goes silent
|
|
31
|
+
Then the consumer receives the reply
|
|
32
|
+
And the connection is lost within 10 seconds
|
|
33
|
+
|
|
34
|
+
Scenario: Answering a request sent as every shard goes silent
|
|
35
|
+
Given an active sharded connection
|
|
36
|
+
And a producer replying `echo` queue
|
|
37
|
+
When the consumer sends a request to the `echo` queue as the network goes silent
|
|
38
|
+
Then the consumer receives the reply
|
|
39
|
+
And the connection is lost within 10 seconds
|
|
40
|
+
|
|
41
|
+
Scenario: Reporting that there is nowhere left to publish
|
|
42
|
+
Given an active sharded connection
|
|
43
|
+
And a producer replying `echo` queue
|
|
44
|
+
When the network goes silent
|
|
45
|
+
Then publishing is paused within 10 seconds
|
|
46
|
+
And publishing is resumed within 10 seconds
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Readable } from 'node:stream'
|
|
2
|
+
import { Socket } from 'node:net'
|
|
2
3
|
import * as _diagnostics from '../../../types/diagnostic'
|
|
3
4
|
import * as _io from '../../../types/io'
|
|
4
5
|
import * as _amqp from '../../../types/amqp'
|
|
@@ -34,10 +35,26 @@ declare namespace comq.features {
|
|
|
34
35
|
streamsValues: Record<number, any[]>
|
|
35
36
|
streamsEnded: Record<number, boolean>
|
|
36
37
|
generatorDestroyed: boolean
|
|
38
|
+
networks: Network[]
|
|
37
39
|
|
|
38
40
|
connect(user?: string, password?: string): Promise<void>
|
|
39
41
|
assert(user?: string, password?: string): Promise<void>
|
|
40
42
|
disconnect(): Promise<void>
|
|
43
|
+
unplug(): Promise<void>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Network {
|
|
47
|
+
readonly address: string
|
|
48
|
+
|
|
49
|
+
open(): Promise<void>
|
|
50
|
+
silence(): void
|
|
51
|
+
close(): Promise<void>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Tunnel {
|
|
55
|
+
client: Socket
|
|
56
|
+
upstream: Socket
|
|
57
|
+
silent: boolean
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
}
|
package/features/steps/broker.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { execFile } = require('node:child_process')
|
|
4
4
|
const { promisify } = require('node:util')
|
|
5
|
-
const { timeout } = require('
|
|
5
|
+
const { timeout } = require('../../test/helpers')
|
|
6
6
|
const { RabbitMQContainer } = require('@testcontainers/rabbitmq')
|
|
7
7
|
const { getContainerRuntimeClient } = require('testcontainers')
|
|
8
8
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { randomBytes } = require('node:crypto')
|
|
5
|
+
|
|
6
|
+
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
7
|
+
|
|
8
|
+
Given('the AMQP channel limit is set to {number}',
|
|
9
|
+
/**
|
|
10
|
+
* @param {number} limit
|
|
11
|
+
*/
|
|
12
|
+
function (limit) {
|
|
13
|
+
global.COMQ_TESTING_AMQP_CHANNEL_MAX = limit
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
When('emitting an event to the {token} exchange is attempted',
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} exchange
|
|
19
|
+
* @this {comq.features.Context}
|
|
20
|
+
*/
|
|
21
|
+
async function (exchange) {
|
|
22
|
+
this.exception = undefined
|
|
23
|
+
|
|
24
|
+
await this.io.emit(exchange, randomBytes(8))
|
|
25
|
+
.catch((exception) => { this.exception = exception })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
Then('the exception is thrown: {string}',
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} message
|
|
31
|
+
* @this {comq.features.Context}
|
|
32
|
+
*/
|
|
33
|
+
function (message) {
|
|
34
|
+
assert.notEqual(this.exception, undefined, 'no exception was thrown')
|
|
35
|
+
assert.equal(this.exception.message, message)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
Then('the {token} event is emitted',
|
|
39
|
+
/**
|
|
40
|
+
* @param {comq.diagnostics.Event} event
|
|
41
|
+
* @this {comq.features.Context}
|
|
42
|
+
*/
|
|
43
|
+
function (event) {
|
|
44
|
+
assert.equal(this.events[event], true, `the '${event}' event was not emitted`)
|
|
45
|
+
})
|
package/features/steps/common.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
|
-
const { timeout } = require('
|
|
4
|
+
const { timeout } = require('../../test/helpers')
|
|
5
5
|
|
|
6
6
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
7
7
|
|
|
@@ -167,6 +167,27 @@ Then('the connection is {connection-event}',
|
|
|
167
167
|
assert.equal(this.events[event], true, 'connection was not ' + key)
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
+
Then('the connection is lost within {number} second(s)',
|
|
171
|
+
/**
|
|
172
|
+
* @param {number} seconds
|
|
173
|
+
* @this {comq.features.Context}
|
|
174
|
+
*/
|
|
175
|
+
async function (seconds) {
|
|
176
|
+
await emitted.call(this, 'close', seconds, 'connection was not lost')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
Then('publishing is {paused-event} within {number} second(s)',
|
|
180
|
+
/**
|
|
181
|
+
* @param {'paused' | 'resumed'} key
|
|
182
|
+
* @param {number} seconds
|
|
183
|
+
* @this {comq.features.Context}
|
|
184
|
+
*/
|
|
185
|
+
async function (key, seconds) {
|
|
186
|
+
const event = PUBLISHING_EVENTS[key]
|
|
187
|
+
|
|
188
|
+
await emitted.call(this, event, seconds, 'publishing was not ' + key)
|
|
189
|
+
})
|
|
190
|
+
|
|
170
191
|
Given('the connection has started sealing',
|
|
171
192
|
/**
|
|
172
193
|
* @this {comq.features.Context}
|
|
@@ -197,6 +218,25 @@ const connect = async (context, user, password) => {
|
|
|
197
218
|
}
|
|
198
219
|
}
|
|
199
220
|
|
|
221
|
+
/**
|
|
222
|
+
* @param {comq.diagnostics.Event} event
|
|
223
|
+
* @param {number} seconds
|
|
224
|
+
* @param {string} message
|
|
225
|
+
* @this {comq.features.Context}
|
|
226
|
+
*/
|
|
227
|
+
async function emitted (event, seconds, message) {
|
|
228
|
+
const deadline = Date.now() + seconds * 1000
|
|
229
|
+
|
|
230
|
+
while (this.events[event] !== true && Date.now() < deadline) await timeout(50)
|
|
231
|
+
|
|
232
|
+
assert.equal(this.events[event], true, message)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const PUBLISHING_EVENTS = {
|
|
236
|
+
paused: 'pause',
|
|
237
|
+
resumed: 'resume'
|
|
238
|
+
}
|
|
239
|
+
|
|
200
240
|
const CONNECTION_EVENTS = {
|
|
201
241
|
lost: 'close',
|
|
202
242
|
restored: 'open'
|
|
@@ -34,6 +34,9 @@ class Context extends World {
|
|
|
34
34
|
streamsEnded = {}
|
|
35
35
|
generatorDestroyed = false
|
|
36
36
|
|
|
37
|
+
/** @type {comq.features.Network[]} */
|
|
38
|
+
networks = []
|
|
39
|
+
|
|
37
40
|
async connect (user, password) {
|
|
38
41
|
const urls = this.#urls(user, password)
|
|
39
42
|
|
|
@@ -46,6 +49,12 @@ class Context extends World {
|
|
|
46
49
|
await this.#connect(urls, assert)
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
async unplug () {
|
|
53
|
+
await Promise.all(this.networks.map((network) => network.close()))
|
|
54
|
+
|
|
55
|
+
this.networks = []
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
async disconnect () {
|
|
50
59
|
if (this.io === undefined) return
|
|
51
60
|
|
|
@@ -89,18 +98,29 @@ class Context extends World {
|
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
#url (i, user, password) {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
101
|
+
const address = this.networks[i]?.address ?? getAddress(i)
|
|
102
|
+
const url = PROTOCOL + user + ':' + password + '@' + address
|
|
103
|
+
const query = []
|
|
94
104
|
|
|
95
105
|
// the watchdog measures silence, so it may only be shortened along with the
|
|
96
106
|
// interval at which a healthy broker is expected to say something
|
|
97
|
-
|
|
107
|
+
if (global.COMQ_TESTING_AMQP_HEARTBEAT !== undefined) {
|
|
108
|
+
query.push('heartbeat=' + global.COMQ_TESTING_AMQP_HEARTBEAT)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// a broker negotiates the channel limit down to what the client asks for,
|
|
112
|
+
// which is how exhaustion is reached without opening two thousand channels
|
|
113
|
+
if (global.COMQ_TESTING_AMQP_CHANNEL_MAX !== undefined) {
|
|
114
|
+
query.push('channelMax=' + global.COMQ_TESTING_AMQP_CHANNEL_MAX)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return query.length === 0 ? url : url + '?' + query.join('&')
|
|
98
118
|
}
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
const PROTOCOL = 'amqp://'
|
|
102
122
|
|
|
103
123
|
/** @type {comq.diagnostics.Event[]} */
|
|
104
|
-
const EVENTS = ['open', 'close', 'flow', 'discard']
|
|
124
|
+
const EVENTS = ['open', 'close', 'flow', 'discard', 'pause', 'resume', 'exhausted']
|
|
105
125
|
|
|
106
126
|
exports.Context = Context
|
package/features/steps/events.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const stream = require('node:stream')
|
|
4
4
|
const assert = require('node:assert')
|
|
5
5
|
const { randomBytes } = require('node:crypto')
|
|
6
|
-
const { timeout, quantity, match } = require('
|
|
6
|
+
const { timeout, quantity, match } = require('../../test/helpers')
|
|
7
7
|
const { parse } = require('./yaml')
|
|
8
8
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
9
9
|
|
package/features/steps/hooks.js
CHANGED
|
@@ -14,12 +14,14 @@ After(
|
|
|
14
14
|
async function () {
|
|
15
15
|
delete global.COMQ_TESTING_WATCHDOG_INTERVAL
|
|
16
16
|
delete global.COMQ_TESTING_AMQP_HEARTBEAT
|
|
17
|
+
delete global.COMQ_TESTING_AMQP_CHANNEL_MAX
|
|
17
18
|
|
|
18
19
|
await Promise.all(
|
|
19
20
|
Array.from({ length: BROKERS_AMOUNT }, (_, n) => actions.up(n))
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
await this.disconnect()
|
|
24
|
+
await this.unplug()
|
|
23
25
|
})
|
|
24
26
|
|
|
25
27
|
AfterAll(async function () {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Given, When } = require('@cucumber/cucumber')
|
|
4
|
+
const { BROKERS_AMOUNT } = require('./brokers')
|
|
5
|
+
const { Network } = require('./networks')
|
|
6
|
+
|
|
7
|
+
Given('a network that can go silent',
|
|
8
|
+
/**
|
|
9
|
+
* @this {comq.features.Context}
|
|
10
|
+
*/
|
|
11
|
+
async function () {
|
|
12
|
+
for (let n = 0; n < BROKERS_AMOUNT; n++) {
|
|
13
|
+
const network = new Network(n)
|
|
14
|
+
|
|
15
|
+
await network.open()
|
|
16
|
+
|
|
17
|
+
this.networks[n] = network
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
When('the network goes silent',
|
|
22
|
+
/**
|
|
23
|
+
* @this {comq.features.Context}
|
|
24
|
+
*/
|
|
25
|
+
function () {
|
|
26
|
+
silence.call(this)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// a request that is answered over a connection that is already silent proves
|
|
30
|
+
// nothing, hence the window between the two is left as short as a step boundary
|
|
31
|
+
When('the consumer sends a request to the {token} queue as the network goes silent',
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} queue
|
|
34
|
+
* @this {comq.features.Context}
|
|
35
|
+
*/
|
|
36
|
+
function (queue) {
|
|
37
|
+
this.reply = this.io.request(queue, 'hello')
|
|
38
|
+
|
|
39
|
+
silence.call(this)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @this {comq.features.Context}
|
|
44
|
+
*/
|
|
45
|
+
function silence () {
|
|
46
|
+
// only a connection lost from now on counts as lost
|
|
47
|
+
delete this.events.close
|
|
48
|
+
|
|
49
|
+
for (const network of this.networks) network.silence()
|
|
50
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const net = require('node:net')
|
|
4
|
+
const { getAddress } = require('./brokers')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A network that can go silent, which is what a machine that woke from sleep is
|
|
8
|
+
* left with: the socket stays open, nothing is delivered over it, and neither
|
|
9
|
+
* the broker nor the operating system reports anything. Stopping, killing or
|
|
10
|
+
* pausing a broker does not reproduce it, since all three end up closing the
|
|
11
|
+
* socket the moment the connection is used.
|
|
12
|
+
*/
|
|
13
|
+
class Network {
|
|
14
|
+
/** @type {net.Server} */
|
|
15
|
+
#server
|
|
16
|
+
|
|
17
|
+
/** @type {Set<comq.features.Tunnel>} */
|
|
18
|
+
#tunnels = new Set()
|
|
19
|
+
|
|
20
|
+
/** @type {string} */
|
|
21
|
+
#host
|
|
22
|
+
|
|
23
|
+
/** @type {number} */
|
|
24
|
+
#port
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {number} [n] broker index
|
|
28
|
+
*/
|
|
29
|
+
constructor (n = 0) {
|
|
30
|
+
const [host, port] = getAddress(n).split(':')
|
|
31
|
+
|
|
32
|
+
this.#host = host
|
|
33
|
+
this.#port = Number(port)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get address () {
|
|
37
|
+
return 'localhost:' + this.#server.address().port
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async open () {
|
|
41
|
+
this.#server = net.createServer(this.#tunnel)
|
|
42
|
+
|
|
43
|
+
await new Promise((resolve) => this.#server.listen(0, '127.0.0.1', resolve))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Only the tunnels that are already open go silent: a connection established
|
|
48
|
+
* afterwards is forwarded as usual, just as it is once a machine is awake.
|
|
49
|
+
*/
|
|
50
|
+
silence () {
|
|
51
|
+
for (const tunnel of this.#tunnels) tunnel.silent = true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async close () {
|
|
55
|
+
for (const tunnel of this.#tunnels) this.#collapse(tunnel)
|
|
56
|
+
|
|
57
|
+
await new Promise((resolve) => this.#server.close(resolve))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {net.Socket} client
|
|
62
|
+
*/
|
|
63
|
+
#tunnel = (client) => {
|
|
64
|
+
const upstream = net.connect(this.#port, this.#host)
|
|
65
|
+
|
|
66
|
+
/** @type {comq.features.Tunnel} */
|
|
67
|
+
const tunnel = { client, upstream, silent: false }
|
|
68
|
+
|
|
69
|
+
this.#tunnels.add(tunnel)
|
|
70
|
+
|
|
71
|
+
client.on('data', (chunk) => { if (!tunnel.silent) upstream.write(chunk) })
|
|
72
|
+
upstream.on('data', (chunk) => { if (!tunnel.silent) client.write(chunk) })
|
|
73
|
+
|
|
74
|
+
for (const socket of [client, upstream]) {
|
|
75
|
+
socket.on('error', () => this.#collapse(tunnel))
|
|
76
|
+
socket.on('close', () => this.#collapse(tunnel))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {comq.features.Tunnel} tunnel
|
|
82
|
+
*/
|
|
83
|
+
#collapse (tunnel) {
|
|
84
|
+
this.#tunnels.delete(tunnel)
|
|
85
|
+
tunnel.client.destroy()
|
|
86
|
+
tunnel.upstream.destroy()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
exports.Network = Network
|
|
@@ -38,6 +38,12 @@ defineParameterType({
|
|
|
38
38
|
transformer: (value) => value
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
+
defineParameterType({
|
|
42
|
+
name: 'paused-event',
|
|
43
|
+
regexp: /(paused|resumed)/,
|
|
44
|
+
transformer: (value) => value
|
|
45
|
+
})
|
|
46
|
+
|
|
41
47
|
defineParameterType({
|
|
42
48
|
name: 'message',
|
|
43
49
|
regexp: /(request|event)/,
|
package/features/steps/poison.js
CHANGED
package/features/steps/rpc.js
CHANGED
|
@@ -4,7 +4,7 @@ const stream = require('node:stream')
|
|
|
4
4
|
const assert = require('node:assert')
|
|
5
5
|
const { randomBytes } = require('node:crypto')
|
|
6
6
|
const { parse } = require('./yaml')
|
|
7
|
-
const { match, timeout, quantity, random } = require('
|
|
7
|
+
const { match, timeout, quantity, random } = require('../../test/helpers')
|
|
8
8
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
9
9
|
const { Readable } = require('node:stream')
|
|
10
10
|
const { actions, BROKERS_AMOUNT } = require('./brokers')
|
package/features/steps/tasks.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
4
|
const { Given, Then, When } = require('@cucumber/cucumber')
|
|
5
|
-
const { quantity, timeout } = require('
|
|
5
|
+
const { quantity, timeout } = require('../../test/helpers')
|
|
6
6
|
const { randomBytes } = require('node:crypto')
|
|
7
7
|
const stream = require('node:stream')
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -27,30 +27,29 @@
|
|
|
27
27
|
"feat": "scripts/feat.js"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@cucumber/cucumber": "^13.2.
|
|
31
|
-
"@semantic-release/changelog": "^7.0.0",
|
|
30
|
+
"@cucumber/cucumber": "^13.2.1",
|
|
32
31
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
33
|
-
"@semantic-release/git": "^
|
|
32
|
+
"@semantic-release/git": "^11.0.1",
|
|
34
33
|
"@semantic-release/github": "^12.0.2",
|
|
35
34
|
"@semantic-release/npm": "^13.1.2",
|
|
36
35
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
37
|
-
"@testcontainers/rabbitmq": "^12.0
|
|
38
|
-
"@toa.io/generic": "^1.0.0-alpha.208",
|
|
36
|
+
"@testcontainers/rabbitmq": "^12.1.0",
|
|
39
37
|
"@types/amqplib": "^0.10.8",
|
|
40
38
|
"@types/jest": "^30.0.0",
|
|
39
|
+
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
41
40
|
"fast-glob": "^3.3.3",
|
|
42
|
-
"fuzzysort": "
|
|
41
|
+
"fuzzysort": "4.0.2",
|
|
43
42
|
"husky": "^9.1.7",
|
|
44
43
|
"jest": "^30.4.2",
|
|
45
|
-
"js-yaml": "^5.
|
|
44
|
+
"js-yaml": "^5.4.1",
|
|
46
45
|
"randomstring": "^1.3.1",
|
|
46
|
+
"semantic-release": "^25.0.9",
|
|
47
47
|
"snazzy": "^9.0.0",
|
|
48
48
|
"standard": "^17.1.2",
|
|
49
49
|
"testcontainers": "^12.0.4"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"amqplib": "^2.0.1",
|
|
53
|
-
"msgpackr": "^2.0.5",
|
|
54
53
|
"promex": "^0.1.1",
|
|
55
54
|
"reretry": "^0.1.0"
|
|
56
55
|
},
|