comq 0.23.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/discussions/amqp-1.0.md +127 -0
- package/features/heartbeat.feature +16 -0
- package/features/rpc.call.feature +23 -1
- package/features/rpc.call.recovery.feature +2 -23
- package/features/rpc.parked.feature +6 -6
- 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/call.js +90 -30
- package/features/steps/connection.js +23 -0
- package/features/steps/context.js +1 -2
- package/features/steps/events.js +1 -1
- package/features/steps/hooks.js +0 -1
- package/features/steps/network.js +11 -2
- package/features/steps/networks.js +31 -2
- package/features/steps/parking.js +5 -5
- package/features/steps/retransmission.js +165 -0
- package/features/steps/rpc.js +0 -6
- package/package.json +20 -4
- package/readme.md +46 -46
- package/source/channel.js +3 -1
- package/source/connection.js +10 -90
- package/source/io.js +106 -25
- package/source/shards/channel.js +73 -12
- package/test/amqplib.mock.js +1 -1
- package/test/connection.mock.js +3 -2
- package/test/connection.test.js +16 -78
- package/test/{io.request.terms.test.js → io.call.terms.test.js} +31 -30
- package/test/io.call.test.js +5 -13
- package/test/io.reply.test.js +5 -3
- package/test/io.request.test.js +277 -2
- package/test/memory.test.js +1 -3
- package/test/shards/channel.test.js +168 -2
- package/types/amqp.d.ts +1 -1
- package/types/channel.d.ts +14 -5
- package/types/io.d.ts +18 -8
- package/features/rpc.timeout.feature +0 -19
- package/source/batch.js +0 -62
- package/test/batch.test.js +0 -94
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# AMQP 1.0
|
|
2
|
+
|
|
3
|
+
**comq stays on AMQP 0-9-1 over [amqplib](https://github.com/amqp-node/amqplib).**
|
|
4
|
+
|
|
5
|
+
RabbitMQ 4 speaks AMQP 1.0 natively and keeps 0-9-1 as a first-class protocol; both are
|
|
6
|
+
supported out of the box and neither is on its way out. The choice is therefore comq's to make on
|
|
7
|
+
what each protocol and each client gives it, and the answer today is that 1.0 costs a rewrite of
|
|
8
|
+
the transport layer, changes comq's public API, breaks the wire between comq versions, and
|
|
9
|
+
returns nothing comq uses.
|
|
10
|
+
|
|
11
|
+
Two separate questions hide in "switch to the official client", and they have different answers.
|
|
12
|
+
The client is unusable for comq today. The protocol is usable and in places cleaner, and still
|
|
13
|
+
does not pay for itself.
|
|
14
|
+
|
|
15
|
+
Measured on 2026-09-20 against RabbitMQ 4.3.6, with `rabbitmq-amqp-js-client` 1.0.0, `rhea` 3.0.5
|
|
16
|
+
and `amqplib` 2.0.1.
|
|
17
|
+
|
|
18
|
+
## The client
|
|
19
|
+
|
|
20
|
+
[`rabbitmq-amqp-js-client`](https://github.com/coders51/rabbitmq-amqp-js-client) is listed among
|
|
21
|
+
RabbitMQ's AMQP 1.0 client libraries and is maintained by coders51 rather than by the RabbitMQ
|
|
22
|
+
team, unlike the Java, .NET, Go and Python clients. It is a thin wrapper over
|
|
23
|
+
[rhea](https://github.com/amqp/rhea), which does the protocol.
|
|
24
|
+
|
|
25
|
+
What it offers an application is a connection, a publisher, a consumer and a management link.
|
|
26
|
+
What comq needs from a client, it does not reach:
|
|
27
|
+
|
|
28
|
+
| comq | the client |
|
|
29
|
+
|---|---|
|
|
30
|
+
| 16 [diagnostic events](../readme.md#diagnostics) | connection, publisher and consumer state changes are unobservable ([#40](https://github.com/coders51/rabbitmq-amqp-js-client/issues/40)) |
|
|
31
|
+
| [`seal`](../readme.md#sealing) stops consuming and waits | `Consumer.close` is synchronous and returns nothing ([#34](https://github.com/coders51/rabbitmq-amqp-js-client/issues/34)) |
|
|
32
|
+
| topology recovered after a reconnect | the links come back, the topology does not ([#42](https://github.com/coders51/rabbitmq-amqp-js-client/issues/42)) |
|
|
33
|
+
| prefetch 300, per channel | rhea's default credit window of 1000, fixed |
|
|
34
|
+
| Requests and Replies are published unconfirmed | `publish` always awaits settlement |
|
|
35
|
+
| `amqp://user:pass@host/vhost`, heartbeat 15s | host, port, username, password |
|
|
36
|
+
|
|
37
|
+
Three defects it has today would each stop comq on its own:
|
|
38
|
+
|
|
39
|
+
- **A publish to a queue that does not exist never settles.** The broker refuses the attach with
|
|
40
|
+
`amqp:not-found`; `createPublisher` resolves regardless, and the `publish` promise stays
|
|
41
|
+
pending forever.
|
|
42
|
+
- **Attaching to another connection's exclusive queue kills the process.** `createConsumer`
|
|
43
|
+
resolves, the broker ends the session, and rhea emits an unhandled `error` on a container the
|
|
44
|
+
client keeps private. That is exactly what [`back`](../readme.md#addressed-requests) does on
|
|
45
|
+
every key already held.
|
|
46
|
+
- **Every body goes out as an `amqp-value` section**, Buffers included, so a 0-9-1 consumer
|
|
47
|
+
receives the section bytes in front of the payload and `type: 'amqp-1.0'` among the properties.
|
|
48
|
+
A process on 0-9-1 and a process on the client cannot exchange a message, which rules out a
|
|
49
|
+
rolling migration. rhea with an explicit `data_section` interoperates cleanly, so this is the
|
|
50
|
+
client's message model rather than the protocol.
|
|
51
|
+
|
|
52
|
+
Queue `arguments` are typed `Record<string, string>` while the broker requires integers for
|
|
53
|
+
`x-message-ttl` (`expected integer, got longstr`) and accepts the numbers the typing forbids. The
|
|
54
|
+
dependency on rhea is `github:amqp/rhea` — a git reference that resolves to `git+ssh` in a
|
|
55
|
+
lockfile and builds on install, which would land in comq's dependency tree and in every CI that
|
|
56
|
+
installs comq.
|
|
57
|
+
|
|
58
|
+
Adopted with those gaps filled locally, "the official client" becomes "rhea, plus a management
|
|
59
|
+
link we wrote" — which is the layer comq already is, over a client with fifteen years behind it.
|
|
60
|
+
|
|
61
|
+
## The protocol
|
|
62
|
+
|
|
63
|
+
Everything comq does is expressible over AMQP 1.0, and three mechanisms fit it better than 0-9-1
|
|
64
|
+
does:
|
|
65
|
+
|
|
66
|
+
- **An unroutable publish comes back as a `RELEASED` outcome on the send itself**, where 0-9-1
|
|
67
|
+
reports it asynchronously through `mandatory` and `basic.return` and comq has to correlate it.
|
|
68
|
+
- **The `ttl` header is honoured per message**, so a [`call`](../readme.md#timeout)'s expiration
|
|
69
|
+
carries unchanged.
|
|
70
|
+
- **Credit is granted per link**, which is what comq's [channel segregation](../readme.md#channels)
|
|
71
|
+
approximates with a prefetch count per channel.
|
|
72
|
+
|
|
73
|
+
The rest holds: an exclusive queue declared over the management link is refused to a second
|
|
74
|
+
connection (`400 cannot obtain exclusive access`), so `back` still holds a Key; numeric queue
|
|
75
|
+
arguments are accepted, so the [retry ladder](../readme.md#retries) and `comq.parked` are
|
|
76
|
+
unchanged. Streams with offsets and server-side filters are the one capability 0-9-1 cannot match,
|
|
77
|
+
and comq consumes no streams.
|
|
78
|
+
|
|
79
|
+
### What it costs
|
|
80
|
+
|
|
81
|
+
**comq's public API is shaped by 0-9-1.** `Properties` is in the consumer signature, `emit`,
|
|
82
|
+
`enqueue` and `route` take publish options, and `discard`, `retry` and `return` hand listeners a
|
|
83
|
+
raw amqplib message. Every one of those changes, and so does what reads them — `@toa.io/bindings.amqp`
|
|
84
|
+
reads `message.properties.correlationId` and `properties.headers`.
|
|
85
|
+
|
|
86
|
+
**Message properties land elsewhere.** A 0-9-1 header prefixed `x-` arrives over 1.0 as a message
|
|
87
|
+
annotation rather than an application property, beside the broker's own `x-exchange` and
|
|
88
|
+
`x-routing-key`; `replyTo: 'rq'` arrives as `/queues/rq`. comq's [`x-comq-*` headers](../docs/headers.md)
|
|
89
|
+
move with them.
|
|
90
|
+
|
|
91
|
+
**The management UI and API stop reporting message rates** for AMQP 1.0 connections; queue depth
|
|
92
|
+
survives, `message_stats` is null. Frame-intercepting plugins and transactions are 0-9-1 only,
|
|
93
|
+
and comq uses neither.
|
|
94
|
+
|
|
95
|
+
**The work is the transport layer and the unit suite.** `source/connection.js` and
|
|
96
|
+
`source/channel.js` are 1,256 lines written against amqplib's channel API, `source/io.js` another
|
|
97
|
+
797 that speak its vocabulary, and `test/` is built on `test/amqplib.mock.js` — some 6,400 lines
|
|
98
|
+
that go with the thing they mock. The 88 scenarios in `features/` are behaviour-level and would
|
|
99
|
+
carry over, which makes them the whole safety net for the change.
|
|
100
|
+
|
|
101
|
+
### What it does not buy
|
|
102
|
+
|
|
103
|
+
Throughput is level and the latency comq optimises for is worse. Same broker over loopback,
|
|
104
|
+
20,000 messages of ~180 bytes, three runs, `TCP_NODELAY` on both:
|
|
105
|
+
|
|
106
|
+
| | amqplib | rhea |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| publish, awaiting confirmation or settlement | 77–88k msg/s | 79–103k msg/s |
|
|
109
|
+
| consume and acknowledge | 121–143k msg/s | 110–128k msg/s |
|
|
110
|
+
|
|
111
|
+
Sequential request and reply, 2,000 round trips, one in flight:
|
|
112
|
+
|
|
113
|
+
| | p50 | p99 |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| amqplib | 0.20 ms | 0.36–0.41 ms |
|
|
116
|
+
| rhea | 0.26–0.29 ms | 0.51–0.57 ms |
|
|
117
|
+
|
|
118
|
+
rhea enables `TCP_NODELAY` when a receiver is attached and leaves it to the `tcp_no_delay` option
|
|
119
|
+
otherwise, so a connection that only publishes is left to Nagle — the same delay comq took out of
|
|
120
|
+
amqplib in 0.20.1, and worth 22 ms per round trip when it is missed.
|
|
121
|
+
|
|
122
|
+
## What would reverse this
|
|
123
|
+
|
|
124
|
+
- comq needs RabbitMQ streams with server-side filtering.
|
|
125
|
+
- The client exposes connection and link state, a consumer close that waits, a credit setting and
|
|
126
|
+
a vhost — the four things its own issues already name.
|
|
127
|
+
- RabbitMQ announces an end for AMQP 0-9-1.
|
|
@@ -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
|
|
@@ -65,7 +65,29 @@ Feature: Calls under a key
|
|
|
65
65
|
Scenario: A caller stops waiting for a holder that crashed
|
|
66
66
|
Given a holder never answering `void` under the `a` key
|
|
67
67
|
When the consumer calls `void` under the `a` key with a 1000ms timeout
|
|
68
|
-
And
|
|
68
|
+
And the silent holder has taken the call
|
|
69
69
|
And the silent holder crashes
|
|
70
70
|
Then the consumer stops waiting
|
|
71
71
|
And a call to `void` under the `a` key is refused within 10 seconds
|
|
72
|
+
|
|
73
|
+
Scenario: A caller stops waiting at its timeout
|
|
74
|
+
Given a holder on another connection answering `slow` under the `a` key in 1000ms
|
|
75
|
+
When the consumer calls `slow` under the `a` key with a 200ms timeout
|
|
76
|
+
Then the consumer stops waiting
|
|
77
|
+
|
|
78
|
+
Scenario: A call its holder has not taken in time is never processed
|
|
79
|
+
Given a holder not taking `later` under the `a` key
|
|
80
|
+
When the consumer calls `later` under the `a` key with a 200ms timeout
|
|
81
|
+
Then the consumer stops waiting
|
|
82
|
+
When after 300ms
|
|
83
|
+
And the idle holder starts taking
|
|
84
|
+
Then the idle holder takes nothing
|
|
85
|
+
|
|
86
|
+
Scenario: A call abandoned by its signal stays in its queue
|
|
87
|
+
Given a holder not taking `stray` under the `a` key
|
|
88
|
+
When the consumer calls `stray` under the `a` key with a signal
|
|
89
|
+
And after 100ms
|
|
90
|
+
And the signal aborts
|
|
91
|
+
Then the consumer stops waiting for the aborted call
|
|
92
|
+
When the idle holder starts taking
|
|
93
|
+
Then the idle holder takes the call
|
|
@@ -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
|
|
@@ -39,13 +39,6 @@ Feature: Calls under a key across failures
|
|
|
39
39
|
And the broker is up
|
|
40
40
|
Then the call ends
|
|
41
41
|
|
|
42
|
-
Scenario: A caller stops waiting for a request at its timeout while the broker is down
|
|
43
|
-
Given an active connection to the broker
|
|
44
|
-
And a producer replying `echo` queue
|
|
45
|
-
When the broker has crashed
|
|
46
|
-
And the consumer sends a request to the `echo` queue with a 1000ms timeout
|
|
47
|
-
Then the consumer stops waiting within 5 seconds
|
|
48
|
-
|
|
49
42
|
Scenario: A caller stops waiting for a call at its timeout while the broker is down
|
|
50
43
|
Given an active connection to the broker
|
|
51
44
|
And a holder on another connection answering `echo` under the `a` key
|
|
@@ -53,20 +46,6 @@ Feature: Calls under a key across failures
|
|
|
53
46
|
And the consumer calls `echo` under the `a` key with a 1000ms timeout
|
|
54
47
|
Then the consumer stops waiting within 5 seconds
|
|
55
48
|
|
|
56
|
-
Scenario: A request re-sent after the broker restarts expires at its caller's deadline
|
|
57
|
-
|
|
58
|
-
The request is lost with the broker and sent again once the connection is restored, carrying
|
|
59
|
-
the time its caller has left.
|
|
60
|
-
|
|
61
|
-
Given an active connection to the broker
|
|
62
|
-
When the consumer sends a request to the `resent` queue with a 30000ms timeout
|
|
63
|
-
And after 200ms
|
|
64
|
-
And the broker has crashed
|
|
65
|
-
And the broker is up
|
|
66
|
-
Then the consumer stops waiting within 40 seconds
|
|
67
|
-
When a producer counting requests to the `resent` queue
|
|
68
|
-
Then the producer receives nothing
|
|
69
|
-
|
|
70
49
|
Scenario: A call reaches its holder while one of the brokers is down
|
|
71
50
|
Given an active sharded connection
|
|
72
51
|
And a holder on another connection answering `echo` under the `a` key
|
|
@@ -5,13 +5,13 @@ Feature: Parked requests
|
|
|
5
5
|
|
|
6
6
|
Scenario: A parked request can still be answered
|
|
7
7
|
|
|
8
|
-
A caller
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
A caller is certain of an eventual answer for as long as it is alive, and parking
|
|
9
|
+
is what keeps that true when no consumer can produce one: the request is kept, along
|
|
10
|
+
with who was asking. Somebody else can answer it, and the caller — still waiting,
|
|
11
|
+
still holding an exclusive reply queue — receives it.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
This is why a request has no timeout and cannot be withdrawn: either would leave the
|
|
14
|
+
parked message addressed to a caller that is no longer listening.
|
|
15
15
|
|
|
16
16
|
Given a producer failing every request to the `unanswerable` queue
|
|
17
17
|
When a request is sent to the `unanswerable` queue
|
|
@@ -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
|
}
|
package/features/steps/call.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
|
-
const amqplib = require('amqplib')
|
|
4
|
+
const amqplib = require('@toa.io/amqplib')
|
|
5
5
|
const { Given, When, Then, After } = require('@cucumber/cucumber')
|
|
6
6
|
|
|
7
7
|
const { connect, Unroutable } = require('../../')
|
|
@@ -84,11 +84,76 @@ Given('a holder never answering {token} under the {token} key',
|
|
|
84
84
|
await channel.assertExchange(exchange, 'direct', { durable: true })
|
|
85
85
|
await channel.assertQueue(queue, { exclusive: true })
|
|
86
86
|
await channel.bindQueue(queue, exchange, key)
|
|
87
|
-
await channel.consume(queue, () =>
|
|
87
|
+
await channel.consume(queue, () => (this.handed = true))
|
|
88
88
|
|
|
89
89
|
this.silent = connection
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
+
Then('the silent holder has taken the call',
|
|
93
|
+
/**
|
|
94
|
+
* The publication is awaited nowhere, and what the crash has to find already queued is what
|
|
95
|
+
* the holder has been handed.
|
|
96
|
+
*
|
|
97
|
+
* @this {comq.features.Context}
|
|
98
|
+
*/
|
|
99
|
+
async function () {
|
|
100
|
+
assert.equal(await until(() => this.handed === true, 10_000), true,
|
|
101
|
+
'The silent holder was handed nothing')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
Given('a holder not taking {token} under the {token} key',
|
|
105
|
+
/**
|
|
106
|
+
* Holds the key as comq does, and takes nothing until told to, so that a call waits in its
|
|
107
|
+
* queue. Each scenario holds a key of its own: the broker releases an exclusive queue once it
|
|
108
|
+
* has noticed the connection close, and a scenario that declared it again before then would be
|
|
109
|
+
* refused it.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} exchange
|
|
112
|
+
* @param {string} key
|
|
113
|
+
* @this {comq.features.Context}
|
|
114
|
+
*/
|
|
115
|
+
async function (exchange, key) {
|
|
116
|
+
const connection = await amqplib.connect(url(0))
|
|
117
|
+
const channel = await connection.createChannel()
|
|
118
|
+
const queue = exchange + '.' + key
|
|
119
|
+
|
|
120
|
+
await channel.assertExchange(exchange, 'direct', { durable: true })
|
|
121
|
+
await channel.assertQueue(queue, { exclusive: true })
|
|
122
|
+
await channel.bindQueue(queue, exchange, key)
|
|
123
|
+
|
|
124
|
+
this.idle = { connection, channel, queue, taken: 0 }
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
When('the idle holder starts taking',
|
|
128
|
+
/**
|
|
129
|
+
* @this {comq.features.Context}
|
|
130
|
+
*/
|
|
131
|
+
async function () {
|
|
132
|
+
const idle = this.idle
|
|
133
|
+
|
|
134
|
+
await idle.channel.consume(idle.queue, () => idle.taken++, { noAck: true })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
When('the consumer calls {token} under the {token} key with a signal',
|
|
138
|
+
/**
|
|
139
|
+
* @param {string} exchange
|
|
140
|
+
* @param {string} key
|
|
141
|
+
* @this {comq.features.Context}
|
|
142
|
+
*/
|
|
143
|
+
async function (exchange, key) {
|
|
144
|
+
this.controller = new AbortController()
|
|
145
|
+
|
|
146
|
+
call(this, this.io.call(exchange, key, { key }, { signal: this.controller.signal }))
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
When('the signal aborts',
|
|
150
|
+
/**
|
|
151
|
+
* @this {comq.features.Context}
|
|
152
|
+
*/
|
|
153
|
+
function () {
|
|
154
|
+
this.controller.abort()
|
|
155
|
+
})
|
|
156
|
+
|
|
92
157
|
When('the silent holder crashes',
|
|
93
158
|
/**
|
|
94
159
|
* Its socket is destroyed without a word, as a process that dies leaves it.
|
|
@@ -149,16 +214,6 @@ When('the consumer calls {token} under the {token} key {number} times',
|
|
|
149
214
|
this.replies = await Promise.all(calls)
|
|
150
215
|
})
|
|
151
216
|
|
|
152
|
-
When('the consumer sends a request to the {token} queue with a {number}ms timeout',
|
|
153
|
-
/**
|
|
154
|
-
* @param {string} queue
|
|
155
|
-
* @param {number} ms
|
|
156
|
-
* @this {comq.features.Context}
|
|
157
|
-
*/
|
|
158
|
-
async function (queue, ms) {
|
|
159
|
-
call(this, this.io.request(queue, { queue }, { timeout: ms }))
|
|
160
|
-
})
|
|
161
|
-
|
|
162
217
|
When('the holder is sealed',
|
|
163
218
|
/**
|
|
164
219
|
* @this {comq.features.Context}
|
|
@@ -192,19 +247,32 @@ When('the first holder disconnects',
|
|
|
192
247
|
await this.holder.close()
|
|
193
248
|
})
|
|
194
249
|
|
|
195
|
-
|
|
250
|
+
Then('the idle holder takes nothing',
|
|
196
251
|
/**
|
|
197
|
-
* @param {string} queue
|
|
198
252
|
* @this {comq.features.Context}
|
|
199
253
|
*/
|
|
200
|
-
async function (
|
|
201
|
-
|
|
254
|
+
async function () {
|
|
255
|
+
await timeout(500)
|
|
202
256
|
|
|
203
|
-
|
|
204
|
-
|
|
257
|
+
assert.equal(this.idle.taken, 0, 'An expired call was taken')
|
|
258
|
+
})
|
|
205
259
|
|
|
206
|
-
|
|
207
|
-
|
|
260
|
+
Then('the idle holder takes the call',
|
|
261
|
+
/**
|
|
262
|
+
* @this {comq.features.Context}
|
|
263
|
+
*/
|
|
264
|
+
async function () {
|
|
265
|
+
await until(() => this.idle.taken > 0, 5000)
|
|
266
|
+
|
|
267
|
+
assert.equal(this.idle.taken, 1)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
Then('the consumer stops waiting for the aborted call',
|
|
271
|
+
/**
|
|
272
|
+
* @this {comq.features.Context}
|
|
273
|
+
*/
|
|
274
|
+
async function () {
|
|
275
|
+
await assert.rejects(this.reply, (error) => error.name === 'AbortError')
|
|
208
276
|
})
|
|
209
277
|
|
|
210
278
|
Then('the call is refused as unroutable',
|
|
@@ -373,16 +441,6 @@ Then('every call is answered',
|
|
|
373
441
|
assert.deepEqual(this.replies, this.replies.map((_, n) => n))
|
|
374
442
|
})
|
|
375
443
|
|
|
376
|
-
Then('the producer receives nothing',
|
|
377
|
-
/**
|
|
378
|
-
* @this {comq.features.Context}
|
|
379
|
-
*/
|
|
380
|
-
async function () {
|
|
381
|
-
await timeout(500)
|
|
382
|
-
|
|
383
|
-
assert.equal(this.produced, 0, 'An expired request was processed')
|
|
384
|
-
})
|
|
385
|
-
|
|
386
444
|
After(
|
|
387
445
|
/**
|
|
388
446
|
* @this {comq.features.Context}
|
|
@@ -390,9 +448,11 @@ After(
|
|
|
390
448
|
async function () {
|
|
391
449
|
await Promise.all((this.holders ?? []).map((io) => io.close()))
|
|
392
450
|
await this.silent?.close().catch(() => undefined)
|
|
451
|
+
await this.idle?.connection.close().catch(() => undefined)
|
|
393
452
|
|
|
394
453
|
this.holders = []
|
|
395
454
|
this.silent = undefined
|
|
455
|
+
this.idle = undefined
|
|
396
456
|
})
|
|
397
457
|
|
|
398
458
|
/**
|
|
@@ -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/events.js
CHANGED
|
@@ -225,7 +225,7 @@ async function consumed (group) {
|
|
|
225
225
|
/**
|
|
226
226
|
* @param {string} exchange
|
|
227
227
|
* @param {any} message
|
|
228
|
-
* @param {import('amqplib').Options.Publish} [properties]
|
|
228
|
+
* @param {import('@toa.io/amqplib').Options.Publish} [properties]
|
|
229
229
|
* @return {Promise<void>}
|
|
230
230
|
* @this {comq.features.Context}
|
|
231
231
|
*/
|
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
|
}
|