comq 0.16.0 → 0.16.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.
@@ -18,6 +18,20 @@ Feature: Connection Tolerance
18
18
  When the broker is up
19
19
  Then the connection is restored
20
20
 
21
+ Scenario: Restoring singleton connection after broker restart
22
+ Given an active singleton connection
23
+ When the broker is down
24
+ Then the connection is lost
25
+ When the broker is up
26
+ Then the connection is restored
27
+
28
+ Scenario: Restoring sharded singleton connection after broker crash
29
+ Given an active sharded singleton connection
30
+ When the broker has crashed
31
+ Then the connection is lost
32
+ When the broker is up
33
+ Then the connection is restored
34
+
21
35
  Scenario: Connecting with wrong credentials
22
36
  When I attempt to connect to the broker as "someone" with password "whatever"
23
37
  Then an exception is thrown: "Handshake terminated by server: 403 (ACCESS-REFUSED)"
@@ -24,6 +24,22 @@ Feature: Silent Connection Tolerance
24
24
  When the consumer sends a request to the `echo` queue
25
25
  Then the consumer receives the reply
26
26
 
27
+ Scenario: Restoring a singleton connection that has gone silent
28
+ Given an active singleton connection
29
+ And a producer replying `echo` queue
30
+ When the network goes silent
31
+ Then the connection is lost within 10 seconds
32
+ When the consumer sends a request to the `echo` queue
33
+ Then the consumer receives the reply
34
+
35
+ Scenario: Restoring a sharded singleton connection whose every shard has gone silent
36
+ Given an active sharded singleton connection
37
+ And a producer replying `echo` queue
38
+ When the network goes silent
39
+ Then the connection is lost within 10 seconds
40
+ When the consumer sends a request to the `echo` queue
41
+ Then the consumer receives the reply
42
+
27
43
  Scenario: Answering a request sent as the connection goes silent
28
44
  Given an active connection to the broker
29
45
  And a producer replying `echo` queue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "ComQ",
5
5
  "author": {
6
6
  "name": "Artem Gurtovoi",
@@ -64,17 +64,10 @@ class Connection {
64
64
  async open () {
65
65
  this.#closed = false
66
66
 
67
- if (this.#opening !== null) return this.#opening
68
-
69
- this.#opening = retry(this.#open).finally(() => { this.#opening = null })
70
-
71
- await this.#opening
67
+ await this.#reopen()
72
68
 
73
69
  if (this.#closed) return
74
70
 
75
- // close may have landed after this attempt succeeded but before `#opening` was cleared
76
- if (this.#connection === undefined) return this.open()
77
-
78
71
  this.#running = true
79
72
  }
80
73
 
@@ -172,10 +165,29 @@ class Connection {
172
165
  this.#connection = undefined
173
166
 
174
167
  if (error !== undefined && !this.#closed) {
175
- this.open().catch((exception) => this.#diagnostics.emit('error', exception))
168
+ this.#reopen().catch((exception) => this.#diagnostics.emit('error', exception))
176
169
  }
177
170
  }
178
171
 
172
+ /**
173
+ * Establishes the connection, or joins the attempt under way. A lost connection is
174
+ * restored through here and not through `open()`: what a subclass makes of `open()` is
175
+ * its own — the singleton answers it with the first opening, made once and remembered,
176
+ * which is no way to make a second one.
177
+ *
178
+ * @return {Promise<void>}
179
+ */
180
+ async #reopen () {
181
+ this.#opening ??= retry(this.#open).finally(() => { this.#opening = null })
182
+
183
+ await this.#opening
184
+
185
+ if (this.#closed) return
186
+
187
+ // close may have landed after this attempt succeeded but before `#opening` was cleared
188
+ if (this.#connection === undefined) return this.#reopen()
189
+ }
190
+
179
191
  /**
180
192
  * An AMQP connection is only closed once the broker has replied with Close-Ok,
181
193
  * which never happens on a connection that has already been lost.
@@ -184,7 +184,7 @@ describe('reconnection', () => {
184
184
 
185
185
  const boom = new Error('reconnect failed')
186
186
 
187
- connection.open = jest.fn(async () => { throw boom })
187
+ amqplib.connect.mockRejectedValueOnce(boom)
188
188
 
189
189
  conn.emit('close', new Error('broker down'))
190
190
 
@@ -194,8 +194,38 @@ describe('reconnection', () => {
194
194
 
195
195
  expect(errors).toContain(boom)
196
196
  expect(unhandled).not.toHaveBeenCalled()
197
+
198
+ // the attempt is retried in the background, and would connect in a later case
199
+ await connection.close()
197
200
  })
198
201
 
202
+ // the singleton answers `open()` with the first opening, made once and remembered
203
+ it('should reconnect a connection whose open() is remembered', async () => {
204
+ class Remembered extends Connection {
205
+ #opened = null
206
+
207
+ async open () {
208
+ this.#opened ??= super.open()
209
+
210
+ await this.#opened
211
+ }
212
+ }
213
+
214
+ const remembered = new Remembered(generate())
215
+
216
+ await remembered.open()
217
+
218
+ const first = await amqplib.connect.mock.results.at(-1).value
219
+
220
+ first.emit('close', new Error('lost'))
221
+
222
+ await expect(remembered.createChannel('event')).resolves.toBeDefined()
223
+
224
+ expect(amqplib.connect).toHaveBeenCalledTimes(3)
225
+
226
+ await remembered.close()
227
+ }, 10000)
228
+
199
229
  it('should reconnect when connection is lost while channels recover', async () => {
200
230
  const channel = await connection.createChannel('request')
201
231