comq 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/features/rpc.call.feature +71 -0
- package/features/rpc.call.recovery.feature +75 -0
- package/features/rpc.call.shards.feature +24 -0
- package/features/rpc.parked.feature +20 -0
- package/features/rpc.timeout.feature +19 -0
- package/features/silence.feature +3 -2
- package/features/steps/call.js +462 -0
- package/features/steps/context.js +4 -0
- package/features/steps/network.js +26 -0
- package/features/steps/networks.js +40 -4
- package/features/steps/parked.js +102 -0
- package/package.json +1 -1
- package/readme.md +141 -31
- package/source/attributes/recall.js +2 -1
- package/source/channel.js +128 -1
- package/source/connection.js +2 -0
- package/source/events.js +1 -1
- package/source/index.js +2 -0
- package/source/io.js +212 -13
- package/source/shards/channel.js +20 -3
- package/source/unroutable.js +27 -0
- package/test/amqplib.mock.js +1 -0
- package/test/channel.held.test.js +237 -0
- package/test/connection.mock.js +1 -0
- package/test/connection.test.js +7 -0
- package/test/io.call.test.js +161 -0
- package/test/io.request.terms.test.js +247 -0
- package/test/shards/channel.test.js +68 -0
- package/types/channel.d.ts +3 -0
- package/types/diagnostic.d.ts +1 -1
- package/types/index.d.ts +9 -0
- package/types/io.d.ts +42 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Feature: Calls under a key
|
|
2
|
+
|
|
3
|
+
A call reaches the one connection holding its key. A call under a key nobody holds is refused
|
|
4
|
+
at once, and a holder that stops withdraws its key before it finishes what it holds.
|
|
5
|
+
|
|
6
|
+
Background:
|
|
7
|
+
Given an active connection to the broker
|
|
8
|
+
|
|
9
|
+
Scenario: Answered by the holder of the key
|
|
10
|
+
Given a holder answering `sums` under the `a` key:
|
|
11
|
+
"""
|
|
12
|
+
({ a, b }) => a + b
|
|
13
|
+
"""
|
|
14
|
+
When the consumer calls `sums` under the `a` key with:
|
|
15
|
+
"""yaml
|
|
16
|
+
a: 1
|
|
17
|
+
b: 2
|
|
18
|
+
"""
|
|
19
|
+
Then the consumer receives the reply:
|
|
20
|
+
"""yaml
|
|
21
|
+
3
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
Scenario: Answered by the holder of that key only
|
|
25
|
+
Given a holder answering `names` under the `a` key:
|
|
26
|
+
"""
|
|
27
|
+
() => 'a'
|
|
28
|
+
"""
|
|
29
|
+
And a holder answering `names` under the `b` key:
|
|
30
|
+
"""
|
|
31
|
+
() => 'b'
|
|
32
|
+
"""
|
|
33
|
+
When the consumer calls `names` under the `b` key
|
|
34
|
+
Then the consumer receives the reply:
|
|
35
|
+
"""yaml
|
|
36
|
+
b
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
Scenario: Refused when nobody holds the key
|
|
40
|
+
Given a holder answering `names` under the `a` key:
|
|
41
|
+
"""
|
|
42
|
+
() => 'a'
|
|
43
|
+
"""
|
|
44
|
+
When the consumer calls `names` under the `c` key
|
|
45
|
+
Then the call is refused as unroutable
|
|
46
|
+
|
|
47
|
+
Scenario: A sealed holder answers what it holds and refuses what follows
|
|
48
|
+
Given a holder on another connection answering `slow` under the `a` key in 500ms
|
|
49
|
+
When the consumer calls `slow` under the `a` key
|
|
50
|
+
And after 100ms
|
|
51
|
+
And the holder is sealed
|
|
52
|
+
Then the consumer receives the reply
|
|
53
|
+
When the consumer calls `slow` under the `a` key
|
|
54
|
+
Then the call is refused as unroutable
|
|
55
|
+
|
|
56
|
+
Scenario: A second holder waits for the key
|
|
57
|
+
Given a holder on another connection answering `echo` under the `a` key
|
|
58
|
+
When a second holder on another connection holds `echo` under the `a` key
|
|
59
|
+
Then the second holder finds the key taken
|
|
60
|
+
When the first holder disconnects
|
|
61
|
+
Then the second holder holds the key
|
|
62
|
+
When the consumer calls `echo` under the `a` key
|
|
63
|
+
Then the consumer receives the reply
|
|
64
|
+
|
|
65
|
+
Scenario: A caller stops waiting for a holder that crashed
|
|
66
|
+
Given a holder never answering `void` under the `a` key
|
|
67
|
+
When the consumer calls `void` under the `a` key with a 1000ms timeout
|
|
68
|
+
And after 100ms
|
|
69
|
+
And the silent holder crashes
|
|
70
|
+
Then the consumer stops waiting
|
|
71
|
+
And a call to `void` under the `a` key is refused within 10 seconds
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
@heavy
|
|
2
|
+
Feature: Calls under a key across failures
|
|
3
|
+
|
|
4
|
+
A holder that loses its connection holds its key again once the connection is restored, and a
|
|
5
|
+
caller's timeout ends its wait whatever the broker is doing.
|
|
6
|
+
|
|
7
|
+
Scenario: A holder holds its key again after the broker restarts
|
|
8
|
+
Given an active connection to the broker
|
|
9
|
+
And a holder on another connection answering `echo` under the `a` key
|
|
10
|
+
When the broker has crashed
|
|
11
|
+
And the broker is up
|
|
12
|
+
Then a call to `echo` under the `a` key is answered within 60 seconds
|
|
13
|
+
|
|
14
|
+
Scenario: A holder holds its key again once the broker lets its silent connection go
|
|
15
|
+
|
|
16
|
+
The holder reconnects while the broker still holds the connection that went silent, and with
|
|
17
|
+
it the key. The holder finds the key taken until the broker lets that connection go, and
|
|
18
|
+
claims it then.
|
|
19
|
+
|
|
20
|
+
Given watchdog interval is set to 2000ms with 60s AMQP heartbeat
|
|
21
|
+
And a network that can go silent
|
|
22
|
+
And an active connection to the broker
|
|
23
|
+
And a holder answering `echo` under the `a` key:
|
|
24
|
+
"""
|
|
25
|
+
(payload) => payload
|
|
26
|
+
"""
|
|
27
|
+
When the network goes silent
|
|
28
|
+
Then the connection is lost within 10 seconds
|
|
29
|
+
And the holder finds its key taken while the broker holds the silent connection
|
|
30
|
+
When the silent connection is let go
|
|
31
|
+
Then a call to `echo` under the `a` key from another connection is answered within 60 seconds
|
|
32
|
+
|
|
33
|
+
Scenario: A call in flight when the broker crashes ends
|
|
34
|
+
Given an active connection to the broker
|
|
35
|
+
And a holder on another connection answering `slow` under the `a` key in 2000ms
|
|
36
|
+
When the consumer calls `slow` under the `a` key
|
|
37
|
+
And after 200ms
|
|
38
|
+
And the broker has crashed
|
|
39
|
+
And the broker is up
|
|
40
|
+
Then the call ends
|
|
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
|
+
Scenario: A caller stops waiting for a call at its timeout while the broker is down
|
|
50
|
+
Given an active connection to the broker
|
|
51
|
+
And a holder on another connection answering `echo` under the `a` key
|
|
52
|
+
When the broker has crashed
|
|
53
|
+
And the consumer calls `echo` under the `a` key with a 1000ms timeout
|
|
54
|
+
Then the consumer stops waiting within 5 seconds
|
|
55
|
+
|
|
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
|
+
Scenario: A call reaches its holder while one of the brokers is down
|
|
71
|
+
Given an active sharded connection
|
|
72
|
+
And a holder on another connection answering `echo` under the `a` key
|
|
73
|
+
When one of the brokers has crashed
|
|
74
|
+
And the consumer calls `echo` under the `a` key 20 times
|
|
75
|
+
Then every call is answered
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Feature: Calls under a key over a sharded connection
|
|
2
|
+
|
|
3
|
+
A call is published to one shard, and a shard where nobody holds the key returns it: it is
|
|
4
|
+
published on the next one, and refused once every shard has returned it.
|
|
5
|
+
|
|
6
|
+
Background:
|
|
7
|
+
Given an active sharded connection
|
|
8
|
+
|
|
9
|
+
Scenario: Answered by a holder on one broker only
|
|
10
|
+
Given a holder connected to broker 0 answering `echo` under the `a` key
|
|
11
|
+
When the consumer calls `echo` under the `a` key 20 times
|
|
12
|
+
Then every call is answered
|
|
13
|
+
|
|
14
|
+
Scenario: Refused when nobody holds the key on any broker
|
|
15
|
+
Given a holder connected to broker 0 answering `echo` under the `a` key
|
|
16
|
+
When the consumer calls `echo` under the `b` key
|
|
17
|
+
Then the call is refused as unroutable
|
|
18
|
+
|
|
19
|
+
Scenario: A key taken on one broker is held on the other, and on both once it is let go
|
|
20
|
+
Given a holder connected to broker 0 answering `whom` under the `a` key as `first`
|
|
21
|
+
When a second holder on another connection holds `whom` under the `a` key as `second`
|
|
22
|
+
Then the second holder finds the key taken
|
|
23
|
+
When the first holder disconnects
|
|
24
|
+
Then every call to `whom` under the `a` key is answered by `second` within 30 seconds
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Feature: Parked requests
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given an active connection to the broker
|
|
5
|
+
|
|
6
|
+
Scenario: A parked request can still be answered
|
|
7
|
+
|
|
8
|
+
A caller that sets no timeout is certain of an eventual answer for as long as it is
|
|
9
|
+
alive, and parking is what keeps that true when no consumer can produce one: the
|
|
10
|
+
request is kept, along with who was asking. Somebody else can answer it, and the
|
|
11
|
+
caller — still waiting, still holding an exclusive reply queue — receives it.
|
|
12
|
+
|
|
13
|
+
A caller that sets a timeout trades that for an end to the wait: once it stops
|
|
14
|
+
waiting, a parked request is addressed to a caller that has gone.
|
|
15
|
+
|
|
16
|
+
Given a producer failing every request to the `unanswerable` queue
|
|
17
|
+
When a request is sent to the `unanswerable` queue
|
|
18
|
+
Then the request is parked
|
|
19
|
+
When the parked request from the `unanswerable` queue is answered by hand
|
|
20
|
+
Then the caller receives the reply
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Feature: Request timeout
|
|
2
|
+
|
|
3
|
+
A caller may say how long it waits for a reply. A request nobody took by then is dropped by the
|
|
4
|
+
broker; one already taken is still processed, and its reply is discarded.
|
|
5
|
+
|
|
6
|
+
Background:
|
|
7
|
+
Given an active connection to the broker
|
|
8
|
+
|
|
9
|
+
Scenario: A caller stops waiting at its timeout
|
|
10
|
+
Given a producer replying `slow` queue in 1000ms
|
|
11
|
+
When the consumer sends a request to the `slow` queue with a 200ms timeout
|
|
12
|
+
Then the consumer stops waiting
|
|
13
|
+
|
|
14
|
+
Scenario: A request nobody took in time is never processed
|
|
15
|
+
When the consumer sends a request to the `later` queue with a 200ms timeout
|
|
16
|
+
Then the consumer stops waiting
|
|
17
|
+
When after 300ms
|
|
18
|
+
And a producer counting requests to the `later` queue
|
|
19
|
+
Then the producer receives nothing
|
package/features/silence.feature
CHANGED
|
@@ -57,6 +57,7 @@ Feature: Silent Connection Tolerance
|
|
|
57
57
|
Scenario: Reporting that there is nowhere left to publish
|
|
58
58
|
Given an active sharded connection
|
|
59
59
|
And a producer replying `echo` queue
|
|
60
|
-
When the network goes silent
|
|
60
|
+
When the network goes silent and refuses new connections
|
|
61
61
|
Then publishing is paused within 10 seconds
|
|
62
|
-
|
|
62
|
+
When the network admits new connections
|
|
63
|
+
Then publishing is resumed within 10 seconds
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const amqplib = require('amqplib')
|
|
5
|
+
const { Given, When, Then, After } = require('@cucumber/cucumber')
|
|
6
|
+
|
|
7
|
+
const { connect, Unroutable } = require('../../')
|
|
8
|
+
const { parse } = require('./yaml')
|
|
9
|
+
const { getAddress, USER, PASSWORD } = require('./brokers')
|
|
10
|
+
const { timeout, until } = require('../../test/helpers')
|
|
11
|
+
|
|
12
|
+
Given('a holder answering {token} under the {token} key:',
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} exchange
|
|
15
|
+
* @param {string} key
|
|
16
|
+
* @param {string} javascript
|
|
17
|
+
* @this {comq.features.Context}
|
|
18
|
+
*/
|
|
19
|
+
async function (exchange, key, javascript) {
|
|
20
|
+
// eslint-disable-next-line no-new-func
|
|
21
|
+
const producer = new Function('return ' + javascript)()
|
|
22
|
+
|
|
23
|
+
this.io.diagnose('taken', () => (this.taken = true))
|
|
24
|
+
|
|
25
|
+
await this.io.back(exchange, key, producer)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
Given('a holder on another connection answering {token} under the {token} key',
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} exchange
|
|
31
|
+
* @param {string} key
|
|
32
|
+
* @this {comq.features.Context}
|
|
33
|
+
*/
|
|
34
|
+
async function (exchange, key) {
|
|
35
|
+
this.holder = await another(this)
|
|
36
|
+
|
|
37
|
+
await this.holder.back(exchange, key, echo)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
Given('a holder on another connection answering {token} under the {token} key in {number}ms',
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} exchange
|
|
43
|
+
* @param {string} key
|
|
44
|
+
* @param {number} delay
|
|
45
|
+
* @this {comq.features.Context}
|
|
46
|
+
*/
|
|
47
|
+
async function (exchange, key, delay) {
|
|
48
|
+
this.holder = await another(this)
|
|
49
|
+
|
|
50
|
+
await this.holder.back(exchange, key, async (payload) => {
|
|
51
|
+
await timeout(delay)
|
|
52
|
+
|
|
53
|
+
return echo(payload)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
Given('a holder connected to broker {number} answering {token} under the {token} key',
|
|
58
|
+
/**
|
|
59
|
+
* @param {number} broker
|
|
60
|
+
* @param {string} exchange
|
|
61
|
+
* @param {string} key
|
|
62
|
+
* @this {comq.features.Context}
|
|
63
|
+
*/
|
|
64
|
+
async function (broker, exchange, key) {
|
|
65
|
+
this.holder = await another(this, broker)
|
|
66
|
+
|
|
67
|
+
await this.holder.back(exchange, key, echo)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
Given('a holder never answering {token} under the {token} key',
|
|
71
|
+
/**
|
|
72
|
+
* Holds the key as comq does, and takes a Request without ever answering it, so that losing
|
|
73
|
+
* its connection loses the Request with it.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} exchange
|
|
76
|
+
* @param {string} key
|
|
77
|
+
* @this {comq.features.Context}
|
|
78
|
+
*/
|
|
79
|
+
async function (exchange, key) {
|
|
80
|
+
const connection = await amqplib.connect(url(0))
|
|
81
|
+
const channel = await connection.createChannel()
|
|
82
|
+
const queue = exchange + '.' + key
|
|
83
|
+
|
|
84
|
+
await channel.assertExchange(exchange, 'direct', { durable: true })
|
|
85
|
+
await channel.assertQueue(queue, { exclusive: true })
|
|
86
|
+
await channel.bindQueue(queue, exchange, key)
|
|
87
|
+
await channel.consume(queue, () => undefined)
|
|
88
|
+
|
|
89
|
+
this.silent = connection
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
When('the silent holder crashes',
|
|
93
|
+
/**
|
|
94
|
+
* Its socket is destroyed without a word, as a process that dies leaves it.
|
|
95
|
+
*
|
|
96
|
+
* @this {comq.features.Context}
|
|
97
|
+
*/
|
|
98
|
+
function () {
|
|
99
|
+
const connection = this.silent
|
|
100
|
+
|
|
101
|
+
this.silent = undefined
|
|
102
|
+
|
|
103
|
+
connection.on('error', () => undefined)
|
|
104
|
+
connection.connection.stream.destroy()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
When('the consumer calls {token} under the {token} key',
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} exchange
|
|
110
|
+
* @param {string} key
|
|
111
|
+
* @this {comq.features.Context}
|
|
112
|
+
*/
|
|
113
|
+
async function (exchange, key) {
|
|
114
|
+
call(this, this.io.call(exchange, key, { key }))
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
When('the consumer calls {token} under the {token} key with:',
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} exchange
|
|
120
|
+
* @param {string} key
|
|
121
|
+
* @param {string} yaml
|
|
122
|
+
* @this {comq.features.Context}
|
|
123
|
+
*/
|
|
124
|
+
async function (exchange, key, yaml) {
|
|
125
|
+
call(this, this.io.call(exchange, key, parse(yaml)))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
When('the consumer calls {token} under the {token} key with a {number}ms timeout',
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} exchange
|
|
131
|
+
* @param {string} key
|
|
132
|
+
* @param {number} ms
|
|
133
|
+
* @this {comq.features.Context}
|
|
134
|
+
*/
|
|
135
|
+
async function (exchange, key, ms) {
|
|
136
|
+
call(this, this.io.call(exchange, key, { key }, { timeout: ms }))
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
When('the consumer calls {token} under the {token} key {number} times',
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} exchange
|
|
142
|
+
* @param {string} key
|
|
143
|
+
* @param {number} times
|
|
144
|
+
* @this {comq.features.Context}
|
|
145
|
+
*/
|
|
146
|
+
async function (exchange, key, times) {
|
|
147
|
+
const calls = Array.from({ length: times }, (_, n) => this.io.call(exchange, key, n))
|
|
148
|
+
|
|
149
|
+
this.replies = await Promise.all(calls)
|
|
150
|
+
})
|
|
151
|
+
|
|
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
|
+
When('the holder is sealed',
|
|
163
|
+
/**
|
|
164
|
+
* @this {comq.features.Context}
|
|
165
|
+
*/
|
|
166
|
+
async function () {
|
|
167
|
+
await this.holder.seal()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
When('a second holder on another connection holds {token} under the {token} key',
|
|
171
|
+
/**
|
|
172
|
+
* @param {string} exchange
|
|
173
|
+
* @param {string} key
|
|
174
|
+
* @this {comq.features.Context}
|
|
175
|
+
*/
|
|
176
|
+
async function (exchange, key) {
|
|
177
|
+
const second = await another(this)
|
|
178
|
+
|
|
179
|
+
second.diagnose('taken', () => (this.taken = true))
|
|
180
|
+
|
|
181
|
+
this.holding = second.back(exchange, key, echo)
|
|
182
|
+
|
|
183
|
+
// asserted on by a later step, and an unhandled rejection would end the run before it
|
|
184
|
+
this.holding.catch(() => undefined)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
When('the first holder disconnects',
|
|
188
|
+
/**
|
|
189
|
+
* @this {comq.features.Context}
|
|
190
|
+
*/
|
|
191
|
+
async function () {
|
|
192
|
+
await this.holder.close()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
When('a producer counting requests to the {token} queue',
|
|
196
|
+
/**
|
|
197
|
+
* @param {string} queue
|
|
198
|
+
* @this {comq.features.Context}
|
|
199
|
+
*/
|
|
200
|
+
async function (queue) {
|
|
201
|
+
this.produced = 0
|
|
202
|
+
|
|
203
|
+
await this.io.reply(queue, () => {
|
|
204
|
+
this.produced++
|
|
205
|
+
|
|
206
|
+
return null
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
Then('the call is refused as unroutable',
|
|
211
|
+
/**
|
|
212
|
+
* @this {comq.features.Context}
|
|
213
|
+
*/
|
|
214
|
+
async function () {
|
|
215
|
+
await assert.rejects(this.reply, (error) => error instanceof Unroutable)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
Then('the consumer stops waiting',
|
|
219
|
+
/**
|
|
220
|
+
* @this {comq.features.Context}
|
|
221
|
+
*/
|
|
222
|
+
async function () {
|
|
223
|
+
await assert.rejects(this.reply, (error) => error.name === 'TimeoutError')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
Given('a holder connected to broker {number} answering {token} under the {token} key as {token}',
|
|
227
|
+
/**
|
|
228
|
+
* @param {number} broker
|
|
229
|
+
* @param {string} exchange
|
|
230
|
+
* @param {string} key
|
|
231
|
+
* @param {string} name
|
|
232
|
+
* @this {comq.features.Context}
|
|
233
|
+
*/
|
|
234
|
+
async function (broker, exchange, key, name) {
|
|
235
|
+
this.holder = await another(this, broker)
|
|
236
|
+
|
|
237
|
+
await this.holder.back(exchange, key, () => name)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
When('a second holder on another connection holds {token} under the {token} key as {token}',
|
|
241
|
+
/**
|
|
242
|
+
* @param {string} exchange
|
|
243
|
+
* @param {string} key
|
|
244
|
+
* @param {string} name
|
|
245
|
+
* @this {comq.features.Context}
|
|
246
|
+
*/
|
|
247
|
+
async function (exchange, key, name) {
|
|
248
|
+
const second = await another(this)
|
|
249
|
+
|
|
250
|
+
second.diagnose('taken', () => (this.taken = true))
|
|
251
|
+
|
|
252
|
+
await second.back(exchange, key, () => name)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
Then('the second holder finds the key taken',
|
|
256
|
+
/**
|
|
257
|
+
* @this {comq.features.Context}
|
|
258
|
+
*/
|
|
259
|
+
async function () {
|
|
260
|
+
assert.equal(await until(() => this.taken === true), true, 'The key was not found taken')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
Then('every call to {token} under the {token} key is answered by {token} within {number} seconds', { timeout: 120_000 },
|
|
264
|
+
/**
|
|
265
|
+
* @param {string} exchange
|
|
266
|
+
* @param {string} key
|
|
267
|
+
* @param {string} name
|
|
268
|
+
* @param {number} seconds
|
|
269
|
+
* @this {comq.features.Context}
|
|
270
|
+
*/
|
|
271
|
+
async function (exchange, key, name, seconds) {
|
|
272
|
+
const answered = await eventually(async () => {
|
|
273
|
+
const calls = Array.from({ length: 20 }, () => this.io.call(exchange, key, null, { timeout: 1000 }))
|
|
274
|
+
const replies = await Promise.all(calls)
|
|
275
|
+
|
|
276
|
+
if (!replies.every((reply) => reply === name)) throw new Error('Answered by another')
|
|
277
|
+
}, seconds)
|
|
278
|
+
|
|
279
|
+
assert.equal(answered, true, `Calls were not all answered by ${name}`)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
Then('the second holder holds the key',
|
|
283
|
+
/**
|
|
284
|
+
* @this {comq.features.Context}
|
|
285
|
+
*/
|
|
286
|
+
async function () {
|
|
287
|
+
await this.holding
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
Then('the holder finds its key taken while the broker holds the silent connection', { timeout: 60_000 },
|
|
291
|
+
/**
|
|
292
|
+
* @this {comq.features.Context}
|
|
293
|
+
*/
|
|
294
|
+
async function () {
|
|
295
|
+
assert.equal(await until(() => this.taken === true, 50_000), true, 'The key was not found taken')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
Then('a call to {token} under the {token} key is answered within {number} seconds', { timeout: 120_000 },
|
|
299
|
+
/**
|
|
300
|
+
* @param {string} exchange
|
|
301
|
+
* @param {string} key
|
|
302
|
+
* @param {number} seconds
|
|
303
|
+
* @this {comq.features.Context}
|
|
304
|
+
*/
|
|
305
|
+
async function (exchange, key, seconds) {
|
|
306
|
+
const answered = await eventually(() => this.io.call(exchange, key, { key }, { timeout: 1000 }), seconds)
|
|
307
|
+
|
|
308
|
+
assert.equal(answered, true, 'The call was not answered')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
Then('a call to {token} under the {token} key from another connection is answered within {number} seconds', { timeout: 120_000 },
|
|
312
|
+
/**
|
|
313
|
+
* @param {string} exchange
|
|
314
|
+
* @param {string} key
|
|
315
|
+
* @param {number} seconds
|
|
316
|
+
* @this {comq.features.Context}
|
|
317
|
+
*/
|
|
318
|
+
async function (exchange, key, seconds) {
|
|
319
|
+
const caller = await another(this)
|
|
320
|
+
const answered = await eventually(() => caller.call(exchange, key, { key }, { timeout: 1000 }), seconds)
|
|
321
|
+
|
|
322
|
+
assert.equal(answered, true, 'The call was not answered')
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
Then('a call to {token} under the {token} key is refused within {number} seconds', { timeout: 60_000 },
|
|
326
|
+
/**
|
|
327
|
+
* @param {string} exchange
|
|
328
|
+
* @param {string} key
|
|
329
|
+
* @param {number} seconds
|
|
330
|
+
* @this {comq.features.Context}
|
|
331
|
+
*/
|
|
332
|
+
async function (exchange, key, seconds) {
|
|
333
|
+
const refused = await eventually(async () => {
|
|
334
|
+
try {
|
|
335
|
+
await this.io.call(exchange, key, { key }, { timeout: 1000 })
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (error instanceof Unroutable) return
|
|
338
|
+
|
|
339
|
+
throw error
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
throw new Error('Answered')
|
|
343
|
+
}, seconds)
|
|
344
|
+
|
|
345
|
+
assert.equal(refused, true, 'The call was not refused')
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
Then('the call ends', { timeout: 90_000 },
|
|
349
|
+
/**
|
|
350
|
+
* @this {comq.features.Context}
|
|
351
|
+
*/
|
|
352
|
+
async function () {
|
|
353
|
+
await this.reply.catch(() => undefined)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
Then('the consumer stops waiting within {number} second(s)', { timeout: 90_000 },
|
|
357
|
+
/**
|
|
358
|
+
* @param {number} seconds
|
|
359
|
+
* @this {comq.features.Context}
|
|
360
|
+
*/
|
|
361
|
+
async function (seconds) {
|
|
362
|
+
const stopped = this.reply.then(() => 'answered', (error) => error.name === 'TimeoutError' ? 'stopped' : error.message)
|
|
363
|
+
const late = timeout(seconds * 1000).then(() => 'still waiting')
|
|
364
|
+
|
|
365
|
+
assert.equal(await Promise.race([stopped, late]), 'stopped')
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
Then('every call is answered',
|
|
369
|
+
/**
|
|
370
|
+
* @this {comq.features.Context}
|
|
371
|
+
*/
|
|
372
|
+
async function () {
|
|
373
|
+
assert.deepEqual(this.replies, this.replies.map((_, n) => n))
|
|
374
|
+
})
|
|
375
|
+
|
|
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
|
+
After(
|
|
387
|
+
/**
|
|
388
|
+
* @this {comq.features.Context}
|
|
389
|
+
*/
|
|
390
|
+
async function () {
|
|
391
|
+
await Promise.all((this.holders ?? []).map((io) => io.close()))
|
|
392
|
+
await this.silent?.close().catch(() => undefined)
|
|
393
|
+
|
|
394
|
+
this.holders = []
|
|
395
|
+
this.silent = undefined
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Another connection, to the brokers the consumer is connected to, or to one of them.
|
|
400
|
+
*
|
|
401
|
+
* @param {comq.features.Context} context
|
|
402
|
+
* @param {number} [broker]
|
|
403
|
+
* @returns {Promise<comq.IO>}
|
|
404
|
+
*/
|
|
405
|
+
async function another (context, broker) {
|
|
406
|
+
const urls = broker !== undefined
|
|
407
|
+
? [url(broker)]
|
|
408
|
+
: context.sharded ? [url(0), url(1)] : [url(0)]
|
|
409
|
+
|
|
410
|
+
const io = await connect(...urls)
|
|
411
|
+
|
|
412
|
+
context.holders ??= []
|
|
413
|
+
context.holders.push(io)
|
|
414
|
+
|
|
415
|
+
return io
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* @param {comq.features.Context} context
|
|
420
|
+
* @param {Promise<any>} reply
|
|
421
|
+
*/
|
|
422
|
+
function call (context, reply) {
|
|
423
|
+
// asserted on by a later step, and an unhandled rejection would end the run before it
|
|
424
|
+
reply.catch(() => undefined)
|
|
425
|
+
|
|
426
|
+
context.reply = reply
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @param {number} broker
|
|
431
|
+
* @returns {string}
|
|
432
|
+
*/
|
|
433
|
+
function url (broker) {
|
|
434
|
+
return `amqp://${USER}:${PASSWORD}@${getAddress(broker)}`
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Makes an attempt until it succeeds, or until the time is up.
|
|
439
|
+
*
|
|
440
|
+
* @param {() => Promise<unknown>} attempt
|
|
441
|
+
* @param {number} seconds
|
|
442
|
+
* @returns {Promise<boolean>} whether it succeeded
|
|
443
|
+
*/
|
|
444
|
+
async function eventually (attempt, seconds) {
|
|
445
|
+
const deadline = Date.now() + seconds * 1000
|
|
446
|
+
|
|
447
|
+
while (Date.now() < deadline) {
|
|
448
|
+
try {
|
|
449
|
+
await attempt()
|
|
450
|
+
|
|
451
|
+
return true
|
|
452
|
+
} catch {
|
|
453
|
+
await timeout(500)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return false
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function echo (payload) {
|
|
461
|
+
return payload
|
|
462
|
+
}
|
|
@@ -21,6 +21,8 @@ class Context extends World {
|
|
|
21
21
|
attempts = []
|
|
22
22
|
counts = {}
|
|
23
23
|
parked = {}
|
|
24
|
+
awaited
|
|
25
|
+
answer
|
|
24
26
|
processed
|
|
25
27
|
enqueued
|
|
26
28
|
tasksProcessedCount = 0
|
|
@@ -69,6 +71,8 @@ class Context extends World {
|
|
|
69
71
|
this.attempts = []
|
|
70
72
|
this.counts = {}
|
|
71
73
|
this.parked = {}
|
|
74
|
+
this.awaited = undefined
|
|
75
|
+
this.answer = undefined
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
/**
|