comq 0.13.0 → 0.15.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/.github/workflows/dev.yaml +28 -0
- package/.github/workflows/release.yaml +24 -54
- package/.husky/pre-push +5 -0
- package/.releaserc +35 -0
- package/AGENTS.md +35 -0
- package/CHANGELOG.md +29 -0
- package/docker-compose.yaml +2 -2
- package/features/backpressure.feature +1 -2
- package/features/connection.feature +1 -0
- package/features/readme.md +9 -2
- package/features/recovery.feature +1 -0
- package/features/rpc.streams.feature +1 -0
- package/features/rpc.streams.shards.feature +1 -0
- package/features/shards.feature +6 -4
- package/features/steps/backpressure.js +32 -30
- package/features/steps/broker.js +4 -30
- package/features/steps/brokers.js +125 -0
- package/features/steps/connection.js +15 -0
- package/features/steps/context.js +2 -4
- package/features/steps/events.js +1 -1
- package/features/steps/hooks.js +14 -1
- package/features/steps/rpc.js +1 -1
- package/features/steps/yaml.js +15 -0
- package/package.json +27 -17
- package/readme.md +8 -5
- package/scripts/feat.js +67 -0
- package/source/channel.js +12 -1
- package/source/connection.js +92 -12
- package/source/events.js +1 -1
- package/source/io.js +42 -8
- package/test/amqplib.mock.js +6 -0
- package/test/channel.test.js +23 -0
- package/test/connection.test.js +112 -5
- package/test/io.request.timeout.test.js +61 -0
- package/types/diagnostic.d.ts +1 -1
- package/types/io.d.ts +6 -2
- package/.github/workflows/codeql.yml +0 -78
- package/fixplan.md +0 -507
- package/lerna.json +0 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Dev
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- dev
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
features-fast:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
env:
|
|
12
|
+
HUSKY: 0
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout
|
|
16
|
+
uses: actions/checkout@v7
|
|
17
|
+
|
|
18
|
+
- name: Setup Node
|
|
19
|
+
uses: actions/setup-node@v7
|
|
20
|
+
with:
|
|
21
|
+
node-version: "24"
|
|
22
|
+
package-manager-cache: false
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: npm ci
|
|
26
|
+
|
|
27
|
+
- name: Run fast feature tests
|
|
28
|
+
run: npm run features:fast
|
|
@@ -1,73 +1,43 @@
|
|
|
1
1
|
name: Release
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
|
|
5
|
-
types: [closed]
|
|
4
|
+
push:
|
|
6
5
|
branches:
|
|
7
6
|
- release
|
|
8
|
-
- alpha
|
|
9
|
-
paths:
|
|
10
|
-
- 'source/**'
|
|
11
|
-
- 'types/**'
|
|
12
|
-
- 'package-lock.json'
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
issues: write
|
|
11
|
+
pull-requests: write
|
|
12
|
+
id-token: write
|
|
16
13
|
|
|
17
14
|
jobs:
|
|
18
|
-
|
|
19
|
-
if: github.event.pull_request.merged == true
|
|
15
|
+
release:
|
|
20
16
|
runs-on: ubuntu-latest
|
|
17
|
+
env:
|
|
18
|
+
HUSKY: 0
|
|
21
19
|
|
|
22
20
|
steps:
|
|
23
|
-
- name: Setup Node
|
|
24
|
-
uses: actions/setup-node@v6
|
|
25
|
-
with:
|
|
26
|
-
node-version: 20
|
|
27
|
-
registry-url: 'https://registry.npmjs.org'
|
|
28
|
-
|
|
29
21
|
- name: Checkout
|
|
30
|
-
uses: actions/checkout@
|
|
22
|
+
uses: actions/checkout@v7
|
|
31
23
|
with:
|
|
32
24
|
fetch-depth: 0
|
|
25
|
+
persist-credentials: false
|
|
33
26
|
|
|
34
|
-
- name:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- name: Import GPG key
|
|
38
|
-
id: gpg
|
|
39
|
-
uses: crazy-max/ghaction-import-gpg@v6
|
|
27
|
+
- name: Setup Node
|
|
28
|
+
uses: actions/setup-node@v7
|
|
40
29
|
with:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
git_commit_gpgsign: true
|
|
30
|
+
node-version: "24"
|
|
31
|
+
registry-url: https://registry.npmjs.org
|
|
32
|
+
package-manager-cache: false
|
|
45
33
|
|
|
46
|
-
- name:
|
|
47
|
-
run:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
npx lerna@7.3.0 version --conventional-prerelease --preid alpha --dist-tag alpha --no-commit-hooks --no-changelog --yes --message "ci: %s" --git-tag-command "echo %s" --sign-git-commit
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
git fetch
|
|
55
|
-
git checkout dev
|
|
56
|
-
git merge ${{ github.base_ref }}
|
|
57
|
-
git push --all origin --no-verify
|
|
58
|
-
env:
|
|
59
|
-
GITHUB_TOKEN: ${{ secrets.TOABOT_GITHUB_TOKEN }}
|
|
60
|
-
GIT_AUTHOR_NAME: ${{ steps.gpg.outputs.name }}
|
|
61
|
-
GIT_AUTHOR_EMAIL: ${{ steps.gpg.outputs.email }}
|
|
62
|
-
GIT_COMMITTER_NAME: ${{ steps.gpg.outputs.name }}
|
|
63
|
-
GIT_COMMITTER_EMAIL: ${{ steps.gpg.outputs.email }}
|
|
34
|
+
- name: Install dependencies
|
|
35
|
+
run: npm ci
|
|
36
|
+
|
|
37
|
+
- name: Run tests
|
|
38
|
+
run: npm test
|
|
64
39
|
|
|
65
|
-
- name:
|
|
40
|
+
- name: Semantic release
|
|
66
41
|
env:
|
|
67
|
-
|
|
68
|
-
run:
|
|
69
|
-
if [ ${{ github.base_ref }} = release ]; then
|
|
70
|
-
npm publish
|
|
71
|
-
else
|
|
72
|
-
npm publish --tag alpha
|
|
73
|
-
fi
|
|
42
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
43
|
+
run: npx semantic-release
|
package/.husky/pre-push
ADDED
package/.releaserc
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": [
|
|
3
|
+
"release"
|
|
4
|
+
],
|
|
5
|
+
"plugins": [
|
|
6
|
+
"@semantic-release/commit-analyzer",
|
|
7
|
+
"@semantic-release/release-notes-generator",
|
|
8
|
+
[
|
|
9
|
+
"@semantic-release/changelog",
|
|
10
|
+
{
|
|
11
|
+
"changelogFile": "CHANGELOG.md"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
"@semantic-release/npm"
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
"@semantic-release/git",
|
|
19
|
+
{
|
|
20
|
+
"assets": [
|
|
21
|
+
"package.json",
|
|
22
|
+
"package-lock.json",
|
|
23
|
+
"CHANGELOG.md"
|
|
24
|
+
],
|
|
25
|
+
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
"@semantic-release/github",
|
|
30
|
+
{
|
|
31
|
+
"owner": "toa-io"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
]
|
|
35
|
+
}
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Cursor Cloud specific instructions
|
|
4
|
+
|
|
5
|
+
### Project Overview
|
|
6
|
+
|
|
7
|
+
ComQ is a Node.js communication library over AMQP (RabbitMQ). It provides RPC, pub/sub, task queues, pipelines, and sharded connections.
|
|
8
|
+
|
|
9
|
+
### Services
|
|
10
|
+
|
|
11
|
+
Feature tests start two RabbitMQ instances automatically via Testcontainers (`features/steps/brokers.js`). Credentials: `developer` / `secret`. AMQP ports are assigned dynamically on the host.
|
|
12
|
+
|
|
13
|
+
### Docker
|
|
14
|
+
|
|
15
|
+
Feature tests require a running Docker daemon. In nested container environments, start Docker with:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
sudo dockerd --exec-opt native.cgroupdriver=cgroupfs &
|
|
19
|
+
sudo chmod 666 /var/run/docker.sock
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Do not use `docker compose up` for feature tests — Testcontainers manages broker lifecycle. `docker compose` may fail here because `deploy.resources.limits.memory` requires cgroupv2 memory limits that are unavailable in nested setups.
|
|
23
|
+
|
|
24
|
+
### Commands
|
|
25
|
+
|
|
26
|
+
- `npm test` — runs `standard` (lint) + `jest` (300 unit tests, no RabbitMQ required)
|
|
27
|
+
- `npm run features` — runs all Cucumber feature tests (starts RabbitMQ via Testcontainers)
|
|
28
|
+
- `npm run lint` — runs `standard --fix --verbose | snazzy`
|
|
29
|
+
|
|
30
|
+
### Testing Notes
|
|
31
|
+
|
|
32
|
+
- Unit tests (`jest`) use mocks and do NOT require running RabbitMQ.
|
|
33
|
+
- Feature tests (`cucumber-js`) start brokers in `BeforeAll` and stop them in `AfterAll`.
|
|
34
|
+
- Recovery/connection tests (in `features/connection.feature`, `features/recovery.feature`, `features/shards.feature`) stop/kill broker containers and take 5-15 minutes to complete.
|
|
35
|
+
- To run only fast feature tests (no broker stop/start): `npx cucumber-js --fail-fast -t 'not @manual' features/events.feature features/rpc.feature features/tasks.feature features/singleton.feature features/properties.feature features/tasks.shards.feature`
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# [0.15.0](https://github.com/toa-io/comq/compare/v0.14.0...v0.15.0) (2026-08-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* avoid channel recover deadlock after broker restart ([8eeaf2e](https://github.com/toa-io/comq/commit/8eeaf2e12321be71b57913adb6a8fb04bd24d439)), closes [#recovery](https://github.com/toa-io/comq/issues/recovery)
|
|
7
|
+
* destroy half-open connections via socket idle watchdog ([9f7ed5f](https://github.com/toa-io/comq/commit/9f7ed5fe50e9ab1857547a6e908185b6b472162f))
|
|
8
|
+
* ignore close events from replaced AMQP connections ([c1070ab](https://github.com/toa-io/comq/commit/c1070abeca9eb30a1c0ed07925bc06e530c1d9ce))
|
|
9
|
+
* reconnect hang after broker restart ([#235](https://github.com/toa-io/comq/issues/235)) ([be07079](https://github.com/toa-io/comq/commit/be07079863194301d0279e2bdd551260196ce20a))
|
|
10
|
+
* retry transient network errors on initial connect ([c436a20](https://github.com/toa-io/comq/commit/c436a2017a2b52d102afa05bc694c9f22faf2065)), closes [#transient](https://github.com/toa-io/comq/issues/transient)
|
|
11
|
+
* tolerate empty YAML documents after js-yaml 5 bump ([0a05ecb](https://github.com/toa-io/comq/commit/0a05ecbf744822b3aa884382642ce51a93922dfc))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* allow request() to time out pending replies ([ac9e2bd](https://github.com/toa-io/comq/commit/ac9e2bd7eb0834d19d404c84f24548bd5475017f))
|
|
17
|
+
* tag container-manipulation features [@heavy](https://github.com/heavy) and add features:fast script ([c0f52ec](https://github.com/toa-io/comq/commit/c0f52ecf72c010e387e853b3daed6884c1cef474))
|
|
18
|
+
|
|
19
|
+
# [0.14.0](https://github.com/toa-io/comq/compare/v0.13.0...v0.14.0) (2026-05-24)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **features:** manage RabbitMQ brokers with Testcontainers ([1d9ca59](https://github.com/toa-io/comq/commit/1d9ca591ff458c75caf87ab63224b15942ba7d07))
|
|
25
|
+
* **features:** use Testcontainers for RabbitMQ in Gherkin tests ([#219](https://github.com/toa-io/comq/issues/219)) ([a33e66b](https://github.com/toa-io/comq/commit/a33e66bfc357f0f089c728bc60c08f6f24b2c8d1))
|
|
26
|
+
|
|
27
|
+
# Changelog
|
|
28
|
+
|
|
29
|
+
All notable changes to this project are documented in this file by [semantic-release](https://github.com/semantic-release/semantic-release).
|
package/docker-compose.yaml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
services:
|
|
2
2
|
rabbitmq-0:
|
|
3
|
-
image: rabbitmq:3
|
|
3
|
+
image: rabbitmq:4.3-management
|
|
4
4
|
container_name: comq-rmq-0
|
|
5
5
|
ports:
|
|
6
6
|
- '5673:5672'
|
|
@@ -18,7 +18,7 @@ services:
|
|
|
18
18
|
limits:
|
|
19
19
|
memory: 512MB
|
|
20
20
|
rabbitmq-1:
|
|
21
|
-
image: rabbitmq:3
|
|
21
|
+
image: rabbitmq:4.3-management
|
|
22
22
|
container_name: comq-rmq-1
|
|
23
23
|
ports:
|
|
24
24
|
- '5674:5672'
|
package/features/readme.md
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
# Features
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Feature tests start two RabbitMQ instances automatically via [Testcontainers](https://node.testcontainers.org/modules/rabbitmq/).
|
|
4
|
+
|
|
5
|
+
Run them with:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run features
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Docker must be available on the host. No manual `docker compose` setup is required.
|
|
@@ -64,6 +64,7 @@ Feature: Reply streams
|
|
|
64
64
|
And after 200ms
|
|
65
65
|
Then the generator is destroyed
|
|
66
66
|
|
|
67
|
+
@heavy
|
|
67
68
|
Scenario: Broker crashes while streaming reply
|
|
68
69
|
Given a number generator with 90ms increasing delay replying `get_numbers` queue
|
|
69
70
|
When the consumer requests a stream with request to the `get_numbers` queue
|
|
@@ -21,6 +21,7 @@ Feature: Reply streams over shards
|
|
|
21
21
|
[0, 1, 2, 3, 4]
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
@heavy
|
|
24
25
|
Scenario: Broker crashes while fetching a stream
|
|
25
26
|
Given a number generator with 90ms increasing delay replying `get_numbers` queue
|
|
26
27
|
When the consumer requests a stream with request to the `get_numbers` queue
|
package/features/shards.feature
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
@heavy
|
|
1
2
|
Feature: Sharded Connection
|
|
2
3
|
|
|
3
4
|
Scenario: Establishing sharded connection
|
|
@@ -5,9 +6,10 @@ Feature: Sharded Connection
|
|
|
5
6
|
|
|
6
7
|
Scenario: Connecting while a shard is down
|
|
7
8
|
Given one of the brokers has crashed
|
|
8
|
-
When I attempt to establish sharded connection
|
|
9
|
-
Then
|
|
10
|
-
|
|
9
|
+
When I attempt to establish sharded connection for 1 second
|
|
10
|
+
Then the connection is not established
|
|
11
|
+
When the broker is up
|
|
12
|
+
Then the connection is established
|
|
11
13
|
|
|
12
14
|
Scenario: Connecting with wrong credentials
|
|
13
15
|
When I attempt to establish a sharded connection as "someone" with password "whatever"
|
|
@@ -41,7 +43,7 @@ Feature: Sharded Connection
|
|
|
41
43
|
Scenario: Shard crashes while publishing events
|
|
42
44
|
Given an active sharded connection
|
|
43
45
|
And events are exclusively consumed from the `flood` exchange
|
|
44
|
-
And I'm publishing 1kB events to the `flood` exchange at
|
|
46
|
+
And I'm publishing 1kB events to the `flood` exchange at 10Hz
|
|
45
47
|
When one of the brokers has crashed
|
|
46
48
|
Then no exceptions are thrown
|
|
47
49
|
And all events have been received
|
|
@@ -4,43 +4,45 @@ const assert = require('node:assert')
|
|
|
4
4
|
const { randomBytes } = require('node:crypto')
|
|
5
5
|
const { quantity, timeout } = require('@toa.io/generic')
|
|
6
6
|
|
|
7
|
-
const { When
|
|
7
|
+
const { When } = require('@cucumber/cucumber')
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const BASE_PAYLOAD = quantity('500k')
|
|
10
|
+
const BASE_BATCH = 20
|
|
11
|
+
const DEADLINE = 25_000
|
|
12
|
+
|
|
13
|
+
When('I\'m flooding the {token} queue until back pressure is applied',
|
|
10
14
|
/**
|
|
11
|
-
* @param {string} bytesQ
|
|
12
15
|
* @param {string} queue
|
|
13
|
-
* @param {string} frequencyQ
|
|
14
|
-
* @param {number} seconds
|
|
15
16
|
* @this {comq.features.Context}
|
|
16
17
|
*/
|
|
17
|
-
async function (
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
async function (queue) {
|
|
19
|
+
const deadline = Date.now() + DEADLINE
|
|
20
|
+
const pending = []
|
|
21
|
+
const errors = []
|
|
22
|
+
let iteration = 0
|
|
23
|
+
|
|
24
|
+
while (!this.events.flow && Date.now() < deadline) {
|
|
25
|
+
if (errors.length > 0) throw errors[0]
|
|
26
|
+
|
|
27
|
+
const payload = BASE_PAYLOAD * (iteration + 1)
|
|
28
|
+
const batch = BASE_BATCH * (iteration + 1)
|
|
29
|
+
const buffer = randomBytes(payload)
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < batch; i++) {
|
|
32
|
+
pending.push(
|
|
33
|
+
this.io.request(queue, buffer).catch((exception) => {
|
|
34
|
+
errors.push(exception)
|
|
35
|
+
})
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
iteration++
|
|
40
|
+
await timeout(0)
|
|
41
|
+
|
|
42
|
+
if (errors.length > 0) throw errors[0]
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
})
|
|
45
|
+
if (errors.length > 0) throw errors[0]
|
|
39
46
|
|
|
40
|
-
Then('back pressure was applied',
|
|
41
|
-
/**
|
|
42
|
-
* @this {comq.features.Context}
|
|
43
|
-
*/
|
|
44
|
-
function () {
|
|
45
47
|
assert.equal(this.events.flow, true, 'Back pressure hasn\'t been applied')
|
|
46
48
|
})
|
package/features/steps/broker.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const { execute } = require('@toa.io/command')
|
|
3
|
+
const { random } = require('@toa.io/generic')
|
|
5
4
|
|
|
6
5
|
const { Given } = require('@cucumber/cucumber')
|
|
6
|
+
const { BROKERS_AMOUNT, actions } = require('./brokers')
|
|
7
7
|
|
|
8
8
|
Given('the broker is/has {status}',
|
|
9
9
|
/**
|
|
10
|
-
* @param {'up' | 'down'} status
|
|
10
|
+
* @param {'up' | 'down' | 'crashed'} status
|
|
11
11
|
* @this {comq.features.Context}
|
|
12
12
|
*/
|
|
13
13
|
async function (status) {
|
|
@@ -18,7 +18,7 @@ Given('the broker is/has {status}',
|
|
|
18
18
|
|
|
19
19
|
Given('one of the brokers is/has {status}',
|
|
20
20
|
/**
|
|
21
|
-
* @param {'up' | 'down'} status
|
|
21
|
+
* @param {'up' | 'down' | 'crashed'} status
|
|
22
22
|
* @this {comq.features.Context}
|
|
23
23
|
*/
|
|
24
24
|
async function (status) {
|
|
@@ -28,29 +28,3 @@ Given('one of the brokers is/has {status}',
|
|
|
28
28
|
|
|
29
29
|
await actions[status](shard)
|
|
30
30
|
})
|
|
31
|
-
|
|
32
|
-
const actions = {
|
|
33
|
-
up: async (n = 0) => {
|
|
34
|
-
await execute('docker start comq-rmq-' + n)
|
|
35
|
-
await healthy(n)
|
|
36
|
-
},
|
|
37
|
-
down: async (n = 0) => {
|
|
38
|
-
await execute('docker stop comq-rmq-' + n)
|
|
39
|
-
},
|
|
40
|
-
crashed: async (n = 0) => {
|
|
41
|
-
await execute('docker kill comq-rmq-' + n)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function healthy (n) {
|
|
46
|
-
let process
|
|
47
|
-
|
|
48
|
-
do {
|
|
49
|
-
await timeout(HEALTHCHECK_INTERVAL)
|
|
50
|
-
|
|
51
|
-
process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-' + n)
|
|
52
|
-
} while (process.output !== 'healthy')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const BROKERS_AMOUNT = 2
|
|
56
|
-
const HEALTHCHECK_INTERVAL = global.COMQ_TESTING_HEALTHCHECK_INTERVAL ?? 1000
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { execFile } = require('node:child_process')
|
|
4
|
+
const { promisify } = require('node:util')
|
|
5
|
+
const { timeout } = require('@toa.io/generic')
|
|
6
|
+
const { RabbitMQContainer } = require('@testcontainers/rabbitmq')
|
|
7
|
+
const { getContainerRuntimeClient } = require('testcontainers')
|
|
8
|
+
|
|
9
|
+
const docker = promisify(execFile)
|
|
10
|
+
|
|
11
|
+
const IMAGE = 'rabbitmq:4.3-management'
|
|
12
|
+
const USER = 'developer'
|
|
13
|
+
const PASSWORD = 'secret'
|
|
14
|
+
const AMQP_PORT = 5672
|
|
15
|
+
const BROKERS_AMOUNT = 2
|
|
16
|
+
const HOST_PORTS = [5673, 5674]
|
|
17
|
+
const HEALTHCHECK_INTERVAL = global.COMQ_TESTING_HEALTHCHECK_INTERVAL ?? 1000
|
|
18
|
+
|
|
19
|
+
/** @type {import('@testcontainers/rabbitmq').StartedRabbitMQContainer[]} */
|
|
20
|
+
const brokers = []
|
|
21
|
+
|
|
22
|
+
async function startBrokers () {
|
|
23
|
+
if (brokers.length > 0) return
|
|
24
|
+
|
|
25
|
+
for (let n = 0; n < BROKERS_AMOUNT; n++) await removeStale(`comq-rmq-${n}`)
|
|
26
|
+
|
|
27
|
+
const started = await Promise.all(
|
|
28
|
+
HOST_PORTS.map((hostPort, n) =>
|
|
29
|
+
new RabbitMQContainer(IMAGE)
|
|
30
|
+
.withEnvironment({
|
|
31
|
+
RABBITMQ_DEFAULT_USER: USER,
|
|
32
|
+
RABBITMQ_DEFAULT_PASS: PASSWORD
|
|
33
|
+
})
|
|
34
|
+
.withAutoRemove(false)
|
|
35
|
+
.withName(`comq-rmq-${n}`)
|
|
36
|
+
.withExposedPorts({ container: AMQP_PORT, host: hostPort })
|
|
37
|
+
.withHealthCheck({
|
|
38
|
+
test: ['CMD', 'rabbitmq-diagnostics', '-q', 'ping'],
|
|
39
|
+
interval: 5000,
|
|
40
|
+
timeout: 1000,
|
|
41
|
+
retries: 5
|
|
42
|
+
})
|
|
43
|
+
.start()
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
brokers.push(...started)
|
|
48
|
+
|
|
49
|
+
await Promise.all(
|
|
50
|
+
Array.from({ length: BROKERS_AMOUNT }, (_, n) => healthy(n))
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function stopBrokers () {
|
|
55
|
+
await Promise.all(brokers.map((broker) => broker.stop({ remove: true })))
|
|
56
|
+
brokers.length = 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} name
|
|
61
|
+
*/
|
|
62
|
+
async function removeStale (name) {
|
|
63
|
+
const client = await getContainerRuntimeClient()
|
|
64
|
+
const listed = await client.container.list()
|
|
65
|
+
|
|
66
|
+
for (const info of listed) {
|
|
67
|
+
if (!info.Names.some((entry) => entry === `/${name}`)) continue
|
|
68
|
+
|
|
69
|
+
const container = client.container.getById(info.Id)
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await client.container.stop(container, { timeout: 0 })
|
|
73
|
+
} catch {
|
|
74
|
+
// already stopped
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await container.remove({ force: true })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {number} [n]
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function getAddress (n = 0) {
|
|
86
|
+
if (brokers[n] === undefined) throw new Error(`Broker ${n} is not started`)
|
|
87
|
+
|
|
88
|
+
return `localhost:${HOST_PORTS[n]}`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {number} [n]
|
|
93
|
+
*/
|
|
94
|
+
async function healthy (n = 0) {
|
|
95
|
+
do {
|
|
96
|
+
await timeout(HEALTHCHECK_INTERVAL)
|
|
97
|
+
|
|
98
|
+
const { stdout } = await docker('docker', ['inspect', '-f', '{{.State.Health.Status}}', `comq-rmq-${n}`])
|
|
99
|
+
|
|
100
|
+
if (stdout.trim() === 'healthy') return
|
|
101
|
+
} while (true)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const actions = {
|
|
105
|
+
up: async (n = 0) => {
|
|
106
|
+
await docker('docker', ['start', `comq-rmq-${n}`])
|
|
107
|
+
await healthy(n)
|
|
108
|
+
},
|
|
109
|
+
down: async (n = 0) => {
|
|
110
|
+
await docker('docker', ['stop', `comq-rmq-${n}`])
|
|
111
|
+
},
|
|
112
|
+
crashed: async (n = 0) => {
|
|
113
|
+
await docker('docker', ['kill', `comq-rmq-${n}`])
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
BROKERS_AMOUNT,
|
|
119
|
+
USER,
|
|
120
|
+
PASSWORD,
|
|
121
|
+
startBrokers,
|
|
122
|
+
stopBrokers,
|
|
123
|
+
getAddress,
|
|
124
|
+
actions
|
|
125
|
+
}
|
|
@@ -88,6 +88,21 @@ When('I attempt to establish sharded connection',
|
|
|
88
88
|
await connect(this)
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
+
When('I attempt to establish sharded connection for {number} second(s)',
|
|
92
|
+
/**
|
|
93
|
+
* @param {number} interval
|
|
94
|
+
* @this {comq.features.Context}
|
|
95
|
+
*/
|
|
96
|
+
async function (interval) {
|
|
97
|
+
this.sharded = true
|
|
98
|
+
|
|
99
|
+
const gap = timeout(interval * 1000)
|
|
100
|
+
|
|
101
|
+
this.connecting = connect(this)
|
|
102
|
+
|
|
103
|
+
await Promise.any([this.connecting, gap])
|
|
104
|
+
})
|
|
105
|
+
|
|
91
106
|
When('I attempt to establish a sharded connection as {string} with password {string}',
|
|
92
107
|
/**
|
|
93
108
|
* @param {string} user
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { World } = require('@cucumber/cucumber')
|
|
4
4
|
const { connect, assert } = require('../../')
|
|
5
|
+
const { getAddress, USER, PASSWORD } = require('./brokers')
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @implements {comq.features.Context}
|
|
@@ -88,14 +89,11 @@ class Context extends World {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
#url (i, user, password) {
|
|
91
|
-
return PROTOCOL + user + ':' + password + '@' +
|
|
92
|
+
return PROTOCOL + user + ':' + password + '@' + getAddress(i)
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
const PROTOCOL = 'amqp://'
|
|
96
|
-
const SHARDS = ['localhost:5673', 'localhost:5674']
|
|
97
|
-
const USER = 'developer'
|
|
98
|
-
const PASSWORD = 'secret'
|
|
99
97
|
|
|
100
98
|
/** @type {comq.diagnostics.Event[]} */
|
|
101
99
|
const EVENTS = ['open', 'close', 'flow', 'discard']
|
package/features/steps/events.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 { timeout, quantity, match } = require('@toa.io/generic')
|
|
7
|
-
const {
|
|
7
|
+
const { parse } = require('./yaml')
|
|
8
8
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
9
9
|
|
|
10
10
|
Given('(that ){token} is consuming events from the {token} exchange',
|
package/features/steps/hooks.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { After } = require('@cucumber/cucumber')
|
|
3
|
+
const { BeforeAll, After, AfterAll } = require('@cucumber/cucumber')
|
|
4
|
+
const { startBrokers, stopBrokers, actions, BROKERS_AMOUNT } = require('./brokers')
|
|
5
|
+
|
|
6
|
+
BeforeAll({ timeout: 120_000 }, async function () {
|
|
7
|
+
await startBrokers()
|
|
8
|
+
})
|
|
4
9
|
|
|
5
10
|
After(
|
|
6
11
|
/**
|
|
7
12
|
* @this {comq.features.Context}
|
|
8
13
|
*/
|
|
9
14
|
async function () {
|
|
15
|
+
await Promise.all(
|
|
16
|
+
Array.from({ length: BROKERS_AMOUNT }, (_, n) => actions.up(n))
|
|
17
|
+
)
|
|
18
|
+
|
|
10
19
|
await this.disconnect()
|
|
11
20
|
})
|
|
21
|
+
|
|
22
|
+
AfterAll(async function () {
|
|
23
|
+
await stopBrokers()
|
|
24
|
+
})
|