amqplib 2.1.0 → 2.2.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/publish.yml +63 -9
- package/CHANGELOG.md +6 -0
- package/README.md +54 -0
- package/callback_api.d.ts +17 -1
- package/index.d.ts +17 -1
- package/lib/recovery.js +71 -10
- package/package.json +1 -1
- package/test/recovery.test.js +409 -12
- package/.claude/settings.local.json +0 -7
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
# Runs when a GitHub release is published (draft releases do not trigger it).
|
|
2
|
+
#
|
|
3
|
+
# Tests the tagged commit on every supported Node.js version, then stages the
|
|
4
|
+
# package on npm with `npm stage publish` using OIDC trusted publishing, so no
|
|
5
|
+
# npm token is stored in this repository. Nothing goes live until a maintainer
|
|
6
|
+
# reviews the staged version and approves it with 2FA:
|
|
7
|
+
#
|
|
8
|
+
# npm stage list amqplib
|
|
9
|
+
# npm stage approve <stage-id>
|
|
10
|
+
#
|
|
11
|
+
# Prerequisites on npmjs.com: the package's trusted publisher must be set to
|
|
12
|
+
# this repository and this workflow file (publish.yml), with allowed actions
|
|
13
|
+
# restricted to `npm stage publish`.
|
|
14
|
+
|
|
1
15
|
name: Publish
|
|
2
16
|
|
|
3
17
|
on:
|
|
4
18
|
release:
|
|
5
|
-
types: [
|
|
19
|
+
types: [published]
|
|
20
|
+
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
6
23
|
|
|
7
24
|
jobs:
|
|
8
25
|
build:
|
|
@@ -19,17 +36,18 @@ jobs:
|
|
|
19
36
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
20
37
|
|
|
21
38
|
steps:
|
|
22
|
-
- uses: actions/checkout@
|
|
39
|
+
- uses: actions/checkout@v4
|
|
23
40
|
|
|
24
41
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
25
|
-
uses: actions/setup-node@
|
|
42
|
+
uses: actions/setup-node@v4
|
|
26
43
|
with:
|
|
27
44
|
node-version: ${{ matrix.node-version }}
|
|
28
45
|
cache: "npm"
|
|
29
46
|
|
|
30
47
|
- run: npm ci
|
|
31
48
|
|
|
32
|
-
-
|
|
49
|
+
- name: Wait for RabbitMQ
|
|
50
|
+
run: |
|
|
33
51
|
n=0
|
|
34
52
|
while :
|
|
35
53
|
do
|
|
@@ -43,17 +61,53 @@ jobs:
|
|
|
43
61
|
|
|
44
62
|
- run: make test
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
stage:
|
|
47
65
|
needs: build
|
|
48
66
|
runs-on: ubuntu-latest
|
|
67
|
+
permissions:
|
|
68
|
+
contents: read
|
|
69
|
+
id-token: write # required for OIDC trusted publishing
|
|
49
70
|
steps:
|
|
50
|
-
- uses: actions/checkout@
|
|
71
|
+
- uses: actions/checkout@v4
|
|
72
|
+
|
|
51
73
|
- uses: actions/setup-node@v4
|
|
52
74
|
with:
|
|
53
|
-
node-version:
|
|
75
|
+
node-version: 24.x
|
|
54
76
|
cache: "npm"
|
|
55
77
|
registry-url: https://registry.npmjs.org/
|
|
78
|
+
|
|
79
|
+
# npm stage publish needs npm 11.15.0 or later
|
|
80
|
+
- run: npm install -g npm@^11.15.0
|
|
81
|
+
|
|
56
82
|
- run: npm ci
|
|
57
|
-
|
|
83
|
+
|
|
84
|
+
- name: Check release tag matches package.json version
|
|
85
|
+
id: version
|
|
86
|
+
env:
|
|
87
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
88
|
+
run: |
|
|
89
|
+
version="$(node -p "require('./package.json').version")"
|
|
90
|
+
if [[ "$RELEASE_TAG" != "v${version}" ]]; then
|
|
91
|
+
echo "Release tag ${RELEASE_TAG} does not match package.json version v${version}" >&2
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
|
95
|
+
|
|
96
|
+
# Pre-release versions (e.g. 2.2.0-rc.0) are staged under the `next`
|
|
97
|
+
# dist-tag; everything else under `latest`. The tag is fixed at staging
|
|
98
|
+
# time and cannot be changed on approval.
|
|
99
|
+
- name: Choose dist-tag
|
|
100
|
+
id: dist-tag
|
|
101
|
+
env:
|
|
102
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
103
|
+
run: |
|
|
104
|
+
if [[ "$VERSION" == *-* ]]; then
|
|
105
|
+
echo "tag=next" >> "$GITHUB_OUTPUT"
|
|
106
|
+
else
|
|
107
|
+
echo "tag=latest" >> "$GITHUB_OUTPUT"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
- name: Stage publish
|
|
58
111
|
env:
|
|
59
|
-
|
|
112
|
+
DIST_TAG: ${{ steps.dist-tag.outputs.tag }}
|
|
113
|
+
run: npm stage publish --tag "$DIST_TAG" --provenance
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Change log for amqplib
|
|
2
2
|
|
|
3
|
+
## v2.2.0
|
|
4
|
+
- Add `calculateDelay` recovery option for supplying a custom reconnect delay strategy, e.g. full jitter, decorrelated jitter or a fixed schedule. When set, `maxDelay` is not applied to its return value; when it throws or returns an invalid value, recovery gives up as if `maxRetries` were exhausted (fixes #855, thanks [@GiHoon1123](https://github.com/GiHoon1123))
|
|
5
|
+
- The built-in reconnect delay no longer exceeds `maxDelay`. Previously the cap was applied before jitter, so delays could overshoot it by up to the jitter fraction. The exponential base is now capped at `maxDelay / (1 + jitter)` so the full jitter range fits under the cap; with default settings the steady-state delay is now spread over 20-30s rather than 24-36s
|
|
6
|
+
- Channel operations attempted after recovery has given up (`reconnect-failed`) now reject with the failure instead of waiting forever
|
|
7
|
+
- Add `initialMaxRetries` recovery option to bound retries before the first successful connection separately from `maxRetries`, so startup can fail fast while steady-state recovery retries indefinitely (fixes #856)
|
|
8
|
+
|
|
3
9
|
## v2.1.0
|
|
4
10
|
- Add `waitForConnect` recovery option. When `false`, `connect` returns the recovering connection immediately instead of waiting for the first successful connection, so listeners for `connect`, `connect-failed` and `reconnect-scheduled` can be attached before the initial attempt, and `close()` can cancel it. Expose `waitForConnect()` on recovering connections to await the first connection (fixes #858)
|
|
5
11
|
- Fix `setup` being run when `close()` is called while the initial connection attempt is in flight
|
package/README.md
CHANGED
|
@@ -210,6 +210,60 @@ callback is invoked immediately with the handle instead of after the first
|
|
|
210
210
|
connection, and `waitForConnect(callback)` can be used to be notified once
|
|
211
211
|
connected.
|
|
212
212
|
|
|
213
|
+
### Custom delay strategy
|
|
214
|
+
|
|
215
|
+
By default, reconnect delays follow an exponential backoff with jitter,
|
|
216
|
+
controlled by `initialDelay`, `maxDelay`, `factor` and `jitter`. To use a
|
|
217
|
+
different strategy entirely (full jitter, decorrelated jitter, a fixed step
|
|
218
|
+
schedule, etc.), provide a `calculateDelay` function instead:
|
|
219
|
+
|
|
220
|
+
```javascript
|
|
221
|
+
const connection = await amqplib.connect('amqp://localhost', {
|
|
222
|
+
recovery: {
|
|
223
|
+
maxRetries: Infinity,
|
|
224
|
+
// Called with the reconnect attempt number, starting at 1.
|
|
225
|
+
// Must return the delay in milliseconds.
|
|
226
|
+
calculateDelay(attempt) {
|
|
227
|
+
return Math.min(30000, 100 * 2 ** (attempt - 1));
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`maxDelay` only bounds the built-in strategy. Once you provide `calculateDelay`,
|
|
234
|
+
amqplib does not cap its return value - you're responsible for enforcing your
|
|
235
|
+
own maximum, as the example above does with `Math.min`.
|
|
236
|
+
|
|
237
|
+
When `calculateDelay` is absent, the built-in strategy is used. If it throws,
|
|
238
|
+
or returns something other than a finite, non-negative number, amqplib does
|
|
239
|
+
not fall back to the built-in strategy. Instead recovery gives up as if
|
|
240
|
+
`maxRetries` had been exhausted: the initial `connect` rejects (or the
|
|
241
|
+
callback receives the error), pending channel operations reject, and
|
|
242
|
+
`reconnect-failed` is emitted with the error. A broken `calculateDelay` is a
|
|
243
|
+
bug in caller-supplied code, so it is surfaced rather than papered over.
|
|
244
|
+
|
|
245
|
+
### Separate retry budget for the initial connection
|
|
246
|
+
|
|
247
|
+
By default `maxRetries` bounds every phase of recovery, including the attempts
|
|
248
|
+
made before the first connection succeeds. To give the very first connection
|
|
249
|
+
its own budget, set `initialMaxRetries`; once connected, `maxRetries` applies.
|
|
250
|
+
A common production posture is to fail fast at startup, so a misconfigured or
|
|
251
|
+
unreachable broker fails the deployment, while never giving up on a service
|
|
252
|
+
that has already connected:
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
const connection = await amqplib.connect('amqp://localhost', {
|
|
256
|
+
recovery: {
|
|
257
|
+
initialMaxRetries: 5, // give up (and reject connect) after 5 failed retries at startup
|
|
258
|
+
maxRetries: Infinity, // but keep reconnecting forever once connected
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
When the initial budget is exhausted `connect` rejects (or the callback receives
|
|
264
|
+
the error) and `reconnect-failed` is emitted. `initialMaxRetries` defaults to
|
|
265
|
+
`maxRetries`, so behaviour is unchanged unless it is set.
|
|
266
|
+
|
|
213
267
|
## Error handling in event handlers
|
|
214
268
|
|
|
215
269
|
If a user-supplied event handler throws a synchronous error, the throw will
|
package/callback_api.d.ts
CHANGED
|
@@ -169,8 +169,15 @@ export interface RecoveryOptions {
|
|
|
169
169
|
factor?: number;
|
|
170
170
|
/** Jitter factor (0–1) applied to delay to avoid thundering herd. Default: 0.2 */
|
|
171
171
|
jitter?: number;
|
|
172
|
-
/** Maximum number of reconnect attempts. Default: Infinity */
|
|
172
|
+
/** Maximum number of reconnect attempts. Also bounds the initial connection unless `initialMaxRetries` is set. Default: Infinity */
|
|
173
173
|
maxRetries?: number;
|
|
174
|
+
/**
|
|
175
|
+
* Maximum number of retries before the first successful connection. Once
|
|
176
|
+
* connected, `maxRetries` applies instead. Lets startup fail fast against an
|
|
177
|
+
* unreachable broker while steady-state recovery keeps retrying.
|
|
178
|
+
* Default: the value of `maxRetries`
|
|
179
|
+
*/
|
|
180
|
+
initialMaxRetries?: number;
|
|
174
181
|
/** Optional setup function called after each successful connection */
|
|
175
182
|
setup?: ((model: Connection) => Promise<void>) | ((model: Connection, done: (err?: Error) => void) => void);
|
|
176
183
|
/**
|
|
@@ -179,6 +186,15 @@ export interface RecoveryOptions {
|
|
|
179
186
|
* Use `waitForConnect()` to be notified of the first connection. Default: true
|
|
180
187
|
*/
|
|
181
188
|
waitForConnect?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Optional custom delay strategy. Called with the reconnect attempt number
|
|
191
|
+
* (starting at 1) and must return the delay in milliseconds. Falls back to
|
|
192
|
+
* the built-in exponential-backoff-with-jitter strategy when absent. If it
|
|
193
|
+
* throws, or returns something other than a finite, non-negative number,
|
|
194
|
+
* recovery gives up as if `maxRetries` were exhausted: the initial connection
|
|
195
|
+
* fails with the error and `reconnect-failed` is emitted.
|
|
196
|
+
*/
|
|
197
|
+
calculateDelay?: (attempt: number) => number;
|
|
182
198
|
}
|
|
183
199
|
|
|
184
200
|
export interface RecoveringConnection extends events.EventEmitter {
|
package/index.d.ts
CHANGED
|
@@ -143,8 +143,15 @@ export interface RecoveryOptions {
|
|
|
143
143
|
factor?: number;
|
|
144
144
|
/** Jitter factor (0–1) applied to delay to avoid thundering herd. Default: 0.2 */
|
|
145
145
|
jitter?: number;
|
|
146
|
-
/** Maximum number of reconnect attempts. Default: Infinity */
|
|
146
|
+
/** Maximum number of reconnect attempts. Also bounds the initial connection unless `initialMaxRetries` is set. Default: Infinity */
|
|
147
147
|
maxRetries?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Maximum number of retries before the first successful connection. Once
|
|
150
|
+
* connected, `maxRetries` applies instead. Lets startup fail fast against an
|
|
151
|
+
* unreachable broker while steady-state recovery keeps retrying.
|
|
152
|
+
* Default: the value of `maxRetries`
|
|
153
|
+
*/
|
|
154
|
+
initialMaxRetries?: number;
|
|
148
155
|
/** Optional setup function called after each successful connection */
|
|
149
156
|
setup?: ((model: ChannelModel) => Promise<void>) | ((model: ChannelModel, done: (err?: Error) => void) => void);
|
|
150
157
|
/**
|
|
@@ -154,6 +161,15 @@ export interface RecoveryOptions {
|
|
|
154
161
|
* first connection. Default: true
|
|
155
162
|
*/
|
|
156
163
|
waitForConnect?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Optional custom delay strategy. Called with the reconnect attempt number
|
|
166
|
+
* (starting at 1) and must return the delay in milliseconds. Falls back to
|
|
167
|
+
* the built-in exponential-backoff-with-jitter strategy when absent. If it
|
|
168
|
+
* throws, or returns something other than a finite, non-negative number,
|
|
169
|
+
* recovery gives up as if `maxRetries` were exhausted: the initial connection
|
|
170
|
+
* fails with the error and `reconnect-failed` is emitted.
|
|
171
|
+
*/
|
|
172
|
+
calculateDelay?: (attempt: number) => number;
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
export interface RecoveringChannelModel extends events.EventEmitter {
|
package/lib/recovery.js
CHANGED
|
@@ -8,6 +8,7 @@ const DEFAULT_RECOVERY = {
|
|
|
8
8
|
maxRetries: Infinity,
|
|
9
9
|
setup: null,
|
|
10
10
|
waitForConnect: true,
|
|
11
|
+
calculateDelay: null,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
function splitConnectionOptions(connOptions) {
|
|
@@ -43,22 +44,32 @@ function toFiniteNumber(value, fallback) {
|
|
|
43
44
|
return Number.isFinite(n) ? n : fallback;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
function toRetryCount(value, fallback) {
|
|
48
|
+
if (value == null) return fallback;
|
|
49
|
+
if (value === Infinity) return Infinity;
|
|
50
|
+
const n = toFiniteNumber(value, fallback);
|
|
51
|
+
return n < 0 ? 0 : n;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
function normaliseRecoveryOptions(recovery) {
|
|
47
55
|
const source = recovery === true ? {} : recovery || {};
|
|
48
56
|
const initialDelay = Math.max(0, toFiniteNumber(source.initialDelay, DEFAULT_RECOVERY.initialDelay));
|
|
49
57
|
const maxDelay = Math.max(initialDelay, toFiniteNumber(source.maxDelay, DEFAULT_RECOVERY.maxDelay));
|
|
50
58
|
const factor = Math.max(1, toFiniteNumber(source.factor, DEFAULT_RECOVERY.factor));
|
|
51
59
|
const jitter = Math.min(1, Math.max(0, toFiniteNumber(source.jitter, DEFAULT_RECOVERY.jitter)));
|
|
52
|
-
const
|
|
60
|
+
const maxRetries = toRetryCount(source.maxRetries, Infinity);
|
|
61
|
+
const initialMaxRetries = toRetryCount(source.initialMaxRetries, maxRetries);
|
|
53
62
|
|
|
54
63
|
return {
|
|
55
64
|
initialDelay,
|
|
56
65
|
maxDelay,
|
|
57
66
|
factor,
|
|
58
67
|
jitter,
|
|
59
|
-
maxRetries
|
|
68
|
+
maxRetries,
|
|
69
|
+
initialMaxRetries,
|
|
60
70
|
setup: typeof source.setup === 'function' ? source.setup : null,
|
|
61
71
|
waitForConnect: source.waitForConnect !== false,
|
|
72
|
+
calculateDelay: typeof source.calculateDelay === 'function' ? source.calculateDelay : null,
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
75
|
|
|
@@ -67,13 +78,39 @@ function toError(err, fallbackMessage) {
|
|
|
67
78
|
return new Error(err ? String(err) : fallbackMessage);
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
function
|
|
71
|
-
|
|
81
|
+
function calculateBuiltinDelay(recovery, attempt) {
|
|
82
|
+
// Capping the base at maxDelay and then clamping base+offset to maxDelay
|
|
83
|
+
// would put a point mass on maxDelay itself: once base saturates, every
|
|
84
|
+
// positive jitter draw (half of them) collapses onto the same value,
|
|
85
|
+
// defeating the point of jitter. Capping the base at
|
|
86
|
+
// maxDelay / (1 + jitter) instead means the maximum possible offset
|
|
87
|
+
// lands exactly on maxDelay, so the result never exceeds it without
|
|
88
|
+
// needing a final clamp - the distribution stays smooth right up to the cap.
|
|
89
|
+
const cappedBase = recovery.maxDelay / (1 + recovery.jitter);
|
|
90
|
+
const base = Math.min(cappedBase, recovery.initialDelay * recovery.factor ** (attempt - 1));
|
|
72
91
|
const jitterPart = base * recovery.jitter;
|
|
73
92
|
const offset = jitterPart > 0 ? Math.random() * jitterPart * 2 - jitterPart : 0;
|
|
74
93
|
return Math.max(0, Math.round(base + offset));
|
|
75
94
|
}
|
|
76
95
|
|
|
96
|
+
// A custom `calculateDelay` that throws, or returns something other than a
|
|
97
|
+
// finite, non-negative number, is a bug in caller-supplied code. Rather than
|
|
98
|
+
// silently falling back to the built-in strategy, the error is treated as a
|
|
99
|
+
// terminal recovery failure by the caller (see _scheduleReconnect), so it
|
|
100
|
+
// surfaces immediately instead of manifesting as unexplained delay behavior.
|
|
101
|
+
function calculateDelay(recovery, attempt) {
|
|
102
|
+
if (!recovery.calculateDelay) {
|
|
103
|
+
return calculateBuiltinDelay(recovery, attempt);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const custom = recovery.calculateDelay(attempt);
|
|
107
|
+
if (typeof custom === 'number' && Number.isFinite(custom) && custom >= 0) {
|
|
108
|
+
return Math.round(custom);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error(`calculateDelay must return a finite, non-negative number of milliseconds (got ${String(custom)})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
77
114
|
function runSetup(setup, model) {
|
|
78
115
|
if (!setup) return Promise.resolve();
|
|
79
116
|
|
|
@@ -118,6 +155,8 @@ class RecoveringCore extends EventEmitter {
|
|
|
118
155
|
this._waiters = [];
|
|
119
156
|
this._connecting = false;
|
|
120
157
|
this._stopped = false;
|
|
158
|
+
this._failure = null;
|
|
159
|
+
this._connectedOnce = false;
|
|
121
160
|
this._timer = null;
|
|
122
161
|
this._attempt = 0;
|
|
123
162
|
|
|
@@ -205,7 +244,7 @@ class RecoveringCore extends EventEmitter {
|
|
|
205
244
|
|
|
206
245
|
_waitForConnection() {
|
|
207
246
|
if (this._model) return Promise.resolve(this._model);
|
|
208
|
-
if (this._stopped) return Promise.reject(new Error('Connection closed'));
|
|
247
|
+
if (this._stopped) return Promise.reject(this._failure || new Error('Connection closed'));
|
|
209
248
|
|
|
210
249
|
return new Promise((resolve, reject) => {
|
|
211
250
|
this._waiters.push({ resolve, reject });
|
|
@@ -277,6 +316,7 @@ class RecoveringCore extends EventEmitter {
|
|
|
277
316
|
|
|
278
317
|
this._model = model;
|
|
279
318
|
this._attempt = 0;
|
|
319
|
+
this._connectedOnce = true;
|
|
280
320
|
this._bindModel(model);
|
|
281
321
|
this._resolveInitialConnection();
|
|
282
322
|
this._resolvePendingWaiters(model);
|
|
@@ -296,15 +336,24 @@ class RecoveringCore extends EventEmitter {
|
|
|
296
336
|
|
|
297
337
|
if (this._timer) return;
|
|
298
338
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
this.
|
|
302
|
-
this.emit('reconnect-failed', err);
|
|
339
|
+
const maxRetries = this._connectedOnce ? this._recovery.maxRetries : this._recovery.initialMaxRetries;
|
|
340
|
+
if (this._attempt >= maxRetries) {
|
|
341
|
+
this._abandonRecovery(err);
|
|
303
342
|
return;
|
|
304
343
|
}
|
|
305
344
|
|
|
306
345
|
const attempt = this._attempt + 1;
|
|
307
|
-
|
|
346
|
+
let delay;
|
|
347
|
+
try {
|
|
348
|
+
delay = calculateDelay(this._recovery, attempt);
|
|
349
|
+
} catch (delayErr) {
|
|
350
|
+
// Both callers of _scheduleReconnect run from timers or socket events
|
|
351
|
+
// where a throw would either be an unhandled rejection or an uncaught
|
|
352
|
+
// exception. Give up recovery cleanly instead so the failure reaches the
|
|
353
|
+
// initial connect promise, pending channel waiters and 'reconnect-failed'.
|
|
354
|
+
this._abandonRecovery(toError(delayErr, 'calculateDelay failed'));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
308
357
|
this._attempt = attempt;
|
|
309
358
|
|
|
310
359
|
this.emit('reconnect-scheduled', { attempt, delay, error: err });
|
|
@@ -314,6 +363,18 @@ class RecoveringCore extends EventEmitter {
|
|
|
314
363
|
}, delay);
|
|
315
364
|
}
|
|
316
365
|
|
|
366
|
+
_abandonRecovery(err) {
|
|
367
|
+
// Recovery is over and cannot be restarted, so treat the connection as
|
|
368
|
+
// stopped. Remember why, so channel operations attempted afterwards
|
|
369
|
+
// reject with the underlying failure instead of waiting forever for a
|
|
370
|
+
// connection that will never arrive.
|
|
371
|
+
this._stopped = true;
|
|
372
|
+
this._failure = err;
|
|
373
|
+
this._rejectInitialConnection(err);
|
|
374
|
+
this._rejectPendingWaiters(err);
|
|
375
|
+
this.emit('reconnect-failed', err);
|
|
376
|
+
}
|
|
377
|
+
|
|
317
378
|
_closeModelNoThrow(model) {
|
|
318
379
|
return this._adapters.closeModel(model).catch(() => {});
|
|
319
380
|
}
|
package/package.json
CHANGED
package/test/recovery.test.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
const { describe, it } = require('node:test');
|
|
4
2
|
const assert = require('node:assert');
|
|
5
3
|
const EventEmitter = require('node:events');
|
|
@@ -16,11 +14,11 @@ class FakePromiseModel extends EventEmitter {
|
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
createChannel(options) {
|
|
19
|
-
return Promise.resolve({kind: 'channel', id: this.id, options});
|
|
17
|
+
return Promise.resolve({ kind: 'channel', id: this.id, options });
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
createConfirmChannel(options) {
|
|
23
|
-
return Promise.resolve({kind: 'confirm', id: this.id, options});
|
|
21
|
+
return Promise.resolve({ kind: 'confirm', id: this.id, options });
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
updateSecret(_newSecret, _reason) {
|
|
@@ -43,7 +41,7 @@ class FakeCallbackModel extends EventEmitter {
|
|
|
43
41
|
cb = options;
|
|
44
42
|
options = undefined;
|
|
45
43
|
}
|
|
46
|
-
cb && cb(null, {kind: 'channel', id: this.id, options});
|
|
44
|
+
cb && cb(null, { kind: 'channel', id: this.id, options });
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
createConfirmChannel(options, cb) {
|
|
@@ -51,7 +49,7 @@ class FakeCallbackModel extends EventEmitter {
|
|
|
51
49
|
cb = options;
|
|
52
50
|
options = undefined;
|
|
53
51
|
}
|
|
54
|
-
cb && cb(null, {kind: 'confirm', id: this.id, options});
|
|
52
|
+
cb && cb(null, { kind: 'confirm', id: this.id, options });
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
updateSecret(_newSecret, _reason, cb) {
|
|
@@ -199,6 +197,303 @@ describe('recovery', () => {
|
|
|
199
197
|
await client.close();
|
|
200
198
|
});
|
|
201
199
|
|
|
200
|
+
it('built-in delay never exceeds maxDelay even at full jitter', async () => {
|
|
201
|
+
const models = [];
|
|
202
|
+
let opened = 0;
|
|
203
|
+
|
|
204
|
+
function openModel() {
|
|
205
|
+
const model = new FakePromiseModel(++opened);
|
|
206
|
+
models.push(model);
|
|
207
|
+
return Promise.resolve(model);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
211
|
+
initialDelay: 5,
|
|
212
|
+
maxDelay: 5,
|
|
213
|
+
factor: 1,
|
|
214
|
+
jitter: 1,
|
|
215
|
+
maxRetries: Infinity,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const originalRandom = Math.random;
|
|
219
|
+
let delay;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Force the maximum positive jitter. The base is capped at
|
|
223
|
+
// maxDelay / (1 + jitter) = 2.5, so base + offset tops out at
|
|
224
|
+
// exactly maxDelay (5) instead of overshooting it.
|
|
225
|
+
Math.random = () => 1;
|
|
226
|
+
|
|
227
|
+
delay = await new Promise((resolve) => {
|
|
228
|
+
client.once('reconnect-scheduled', (info) => resolve(info.delay));
|
|
229
|
+
models[models.length - 1].emit('close', new Error('socket closed'));
|
|
230
|
+
});
|
|
231
|
+
} finally {
|
|
232
|
+
Math.random = originalRandom;
|
|
233
|
+
await client.close();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
assert.equal(delay, 5);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('does not collapse onto maxDelay for every positive jitter draw once the base saturates', async () => {
|
|
240
|
+
const models = [];
|
|
241
|
+
let opened = 0;
|
|
242
|
+
|
|
243
|
+
function openModel() {
|
|
244
|
+
const model = new FakePromiseModel(++opened);
|
|
245
|
+
models.push(model);
|
|
246
|
+
return Promise.resolve(model);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
250
|
+
initialDelay: 100,
|
|
251
|
+
maxDelay: 100,
|
|
252
|
+
factor: 1,
|
|
253
|
+
jitter: 0.2,
|
|
254
|
+
maxRetries: Infinity,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const originalRandom = Math.random;
|
|
258
|
+
let delay;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// Math.random() just above the midpoint gives a small positive
|
|
262
|
+
// offset. Capping base+offset at maxDelay (the old behavior) would
|
|
263
|
+
// collapse this to exactly 100 instead of the slightly lower value
|
|
264
|
+
// this offset actually produces.
|
|
265
|
+
Math.random = () => 0.6;
|
|
266
|
+
|
|
267
|
+
delay = await new Promise((resolve) => {
|
|
268
|
+
client.once('reconnect-scheduled', (info) => resolve(info.delay));
|
|
269
|
+
models[models.length - 1].emit('close', new Error('socket closed'));
|
|
270
|
+
});
|
|
271
|
+
} finally {
|
|
272
|
+
Math.random = originalRandom;
|
|
273
|
+
await client.close();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// base = 100 / 1.2 = 83.33, jitterPart = 16.67,
|
|
277
|
+
// offset = 0.6 * 16.67 * 2 - 16.67 = 3.33, result = round(83.33 + 3.33) = 87
|
|
278
|
+
assert.equal(delay, 87);
|
|
279
|
+
assert.notEqual(delay, 100);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('uses a custom calculateDelay strategy when provided', async () => {
|
|
283
|
+
const models = [];
|
|
284
|
+
let opened = 0;
|
|
285
|
+
const attemptsSeen = [];
|
|
286
|
+
|
|
287
|
+
function openModel() {
|
|
288
|
+
const model = new FakePromiseModel(++opened);
|
|
289
|
+
models.push(model);
|
|
290
|
+
return Promise.resolve(model);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
294
|
+
initialDelay: 1,
|
|
295
|
+
maxDelay: 1,
|
|
296
|
+
jitter: 0,
|
|
297
|
+
maxRetries: 3,
|
|
298
|
+
calculateDelay(attempt) {
|
|
299
|
+
attemptsSeen.push(attempt);
|
|
300
|
+
return attempt * 1000;
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await new Promise((resolve) => {
|
|
305
|
+
client.once('reconnect-scheduled', ({ attempt, delay }) => {
|
|
306
|
+
assert.equal(1, attempt);
|
|
307
|
+
assert.equal(1000, delay);
|
|
308
|
+
resolve();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
models[0].emit('close', new Error('socket closed'));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
assert.deepEqual([1], attemptsSeen);
|
|
315
|
+
|
|
316
|
+
await client.close();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('gives up recovery with reconnect-failed when calculateDelay throws', async () => {
|
|
320
|
+
const models = [];
|
|
321
|
+
let opened = 0;
|
|
322
|
+
|
|
323
|
+
function openModel() {
|
|
324
|
+
const model = new FakePromiseModel(++opened);
|
|
325
|
+
models.push(model);
|
|
326
|
+
return Promise.resolve(model);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
330
|
+
initialDelay: 5,
|
|
331
|
+
maxDelay: 5,
|
|
332
|
+
jitter: 0,
|
|
333
|
+
maxRetries: 3,
|
|
334
|
+
calculateDelay() {
|
|
335
|
+
throw new Error('calculateDelay is broken');
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const err = await new Promise((resolve) => {
|
|
340
|
+
client.once('reconnect-failed', resolve);
|
|
341
|
+
models[0].emit('close', new Error('socket closed'));
|
|
342
|
+
});
|
|
343
|
+
assert.match(err.message, /calculateDelay is broken/);
|
|
344
|
+
await assert.rejects(client.createChannel(), /calculateDelay is broken/);
|
|
345
|
+
|
|
346
|
+
await client.close();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('reports a validation error instead of a JSON error for a circular-reference return value', async () => {
|
|
350
|
+
const models = [];
|
|
351
|
+
let opened = 0;
|
|
352
|
+
|
|
353
|
+
function openModel() {
|
|
354
|
+
const model = new FakePromiseModel(++opened);
|
|
355
|
+
models.push(model);
|
|
356
|
+
return Promise.resolve(model);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
360
|
+
initialDelay: 5,
|
|
361
|
+
maxDelay: 5,
|
|
362
|
+
jitter: 0,
|
|
363
|
+
maxRetries: 3,
|
|
364
|
+
calculateDelay() {
|
|
365
|
+
const circular = {};
|
|
366
|
+
circular.self = circular;
|
|
367
|
+
return circular;
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Must be our own validation error, not a JSON.stringify TypeError from
|
|
372
|
+
// trying to serialize the circular value into the message.
|
|
373
|
+
const err = await new Promise((resolve) => {
|
|
374
|
+
client.once('reconnect-failed', resolve);
|
|
375
|
+
models[0].emit('close', new Error('socket closed'));
|
|
376
|
+
});
|
|
377
|
+
assert.match(err.message, /calculateDelay must return a finite, non-negative number/);
|
|
378
|
+
|
|
379
|
+
await client.close();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('gives up recovery when calculateDelay returns an invalid value', async () => {
|
|
383
|
+
const models = [];
|
|
384
|
+
let opened = 0;
|
|
385
|
+
|
|
386
|
+
function openModel() {
|
|
387
|
+
const model = new FakePromiseModel(++opened);
|
|
388
|
+
models.push(model);
|
|
389
|
+
return Promise.resolve(model);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
393
|
+
initialDelay: 5,
|
|
394
|
+
maxDelay: 5,
|
|
395
|
+
jitter: 0,
|
|
396
|
+
maxRetries: 3,
|
|
397
|
+
calculateDelay() {
|
|
398
|
+
return -1;
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const err = await new Promise((resolve) => {
|
|
403
|
+
client.once('reconnect-failed', resolve);
|
|
404
|
+
models[0].emit('close', new Error('socket closed'));
|
|
405
|
+
});
|
|
406
|
+
assert.match(err.message, /calculateDelay must return a finite, non-negative number/);
|
|
407
|
+
|
|
408
|
+
await client.close();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('does not coerce non-number calculateDelay return values', async () => {
|
|
412
|
+
const models = [];
|
|
413
|
+
let opened = 0;
|
|
414
|
+
|
|
415
|
+
function openModel() {
|
|
416
|
+
const model = new FakePromiseModel(++opened);
|
|
417
|
+
models.push(model);
|
|
418
|
+
return Promise.resolve(model);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
422
|
+
initialDelay: 5,
|
|
423
|
+
maxDelay: 5,
|
|
424
|
+
jitter: 0,
|
|
425
|
+
maxRetries: 3,
|
|
426
|
+
calculateDelay() {
|
|
427
|
+
return '1000'; // a string, not a number - must not be accepted as-is
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const err = await new Promise((resolve) => {
|
|
432
|
+
client.once('reconnect-failed', resolve);
|
|
433
|
+
models[0].emit('close', new Error('socket closed'));
|
|
434
|
+
});
|
|
435
|
+
assert.match(err.message, /calculateDelay must return a finite, non-negative number/);
|
|
436
|
+
|
|
437
|
+
await client.close();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('rejects the initial connection when calculateDelay throws after a failed connect', async (t) => {
|
|
441
|
+
const rejections = [];
|
|
442
|
+
const onRejection = (err) => rejections.push(err);
|
|
443
|
+
process.on('unhandledRejection', onRejection);
|
|
444
|
+
t.after(() => process.removeListener('unhandledRejection', onRejection));
|
|
445
|
+
|
|
446
|
+
let attempts = 0;
|
|
447
|
+
|
|
448
|
+
function openModel() {
|
|
449
|
+
attempts++;
|
|
450
|
+
return Promise.reject(new Error('connect failed'));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
await assert.rejects(
|
|
454
|
+
recovery.connectWithRecoveryPromise(openModel, {
|
|
455
|
+
maxRetries: Infinity,
|
|
456
|
+
calculateDelay() {
|
|
457
|
+
throw new Error('calculateDelay is broken');
|
|
458
|
+
},
|
|
459
|
+
}),
|
|
460
|
+
/calculateDelay is broken/,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
464
|
+
assert.equal(1, attempts);
|
|
465
|
+
assert.deepEqual([], rejections);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('channel operations reject with the failure once recovery has given up', async () => {
|
|
469
|
+
let attempts = 0;
|
|
470
|
+
|
|
471
|
+
function openModel() {
|
|
472
|
+
attempts++;
|
|
473
|
+
return Promise.reject(new Error('connect failed'));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
477
|
+
initialDelay: 1,
|
|
478
|
+
maxDelay: 1,
|
|
479
|
+
jitter: 0,
|
|
480
|
+
maxRetries: 1,
|
|
481
|
+
waitForConnect: false,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const err = await new Promise((resolve) => client.once('reconnect-failed', resolve));
|
|
485
|
+
assert.equal('connect failed', err.message);
|
|
486
|
+
assert.equal(2, attempts);
|
|
487
|
+
|
|
488
|
+
await assert.rejects(client.createChannel(), { message: 'connect failed' });
|
|
489
|
+
await assert.rejects(client.createConfirmChannel(), { message: 'connect failed' });
|
|
490
|
+
await assert.rejects(client.updateSecret(Buffer.from('x'), 'rotate'), { message: 'connect failed' });
|
|
491
|
+
|
|
492
|
+
// Nothing further is attempted and close() is a no-op.
|
|
493
|
+
await client.close();
|
|
494
|
+
assert.equal(2, attempts);
|
|
495
|
+
});
|
|
496
|
+
|
|
202
497
|
it('promise recovery fails after max retries', async () => {
|
|
203
498
|
let attempts = 0;
|
|
204
499
|
|
|
@@ -224,6 +519,108 @@ describe('recovery', () => {
|
|
|
224
519
|
assert.equal(2, attempts);
|
|
225
520
|
});
|
|
226
521
|
|
|
522
|
+
describe('initialMaxRetries', () => {
|
|
523
|
+
it('bounds the initial connection separately from maxRetries', async () => {
|
|
524
|
+
let attempts = 0;
|
|
525
|
+
|
|
526
|
+
function openModel() {
|
|
527
|
+
attempts++;
|
|
528
|
+
return Promise.reject(new Error('connect failed'));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
await assert.rejects(
|
|
532
|
+
recovery.connectWithRecoveryPromise(openModel, {
|
|
533
|
+
initialDelay: 1,
|
|
534
|
+
maxDelay: 1,
|
|
535
|
+
jitter: 0,
|
|
536
|
+
initialMaxRetries: 2,
|
|
537
|
+
maxRetries: Infinity,
|
|
538
|
+
}),
|
|
539
|
+
{ message: 'connect failed' },
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
assert.equal(3, attempts);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('stops applying once a connection has been established', async () => {
|
|
546
|
+
const models = [];
|
|
547
|
+
let attempts = 0;
|
|
548
|
+
|
|
549
|
+
function openModel() {
|
|
550
|
+
attempts++;
|
|
551
|
+
// Succeed at first, fail twice after the disconnect, then succeed again.
|
|
552
|
+
if (attempts === 2 || attempts === 3) return Promise.reject(new Error('connect failed'));
|
|
553
|
+
const model = new FakePromiseModel(attempts);
|
|
554
|
+
models.push(model);
|
|
555
|
+
return Promise.resolve(model);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
559
|
+
initialDelay: 1,
|
|
560
|
+
maxDelay: 1,
|
|
561
|
+
jitter: 0,
|
|
562
|
+
initialMaxRetries: 0,
|
|
563
|
+
maxRetries: 3,
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const failures = [];
|
|
567
|
+
client.on('reconnect-failed', (err) => failures.push(err));
|
|
568
|
+
|
|
569
|
+
const reconnected = new Promise((resolve) => client.once('connect', resolve));
|
|
570
|
+
models[0].emit('close', new Error('socket closed'));
|
|
571
|
+
await reconnected;
|
|
572
|
+
|
|
573
|
+
assert.equal(4, attempts);
|
|
574
|
+
assert.deepEqual([], failures);
|
|
575
|
+
|
|
576
|
+
await client.close();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('accepts Infinity explicitly even when maxRetries is finite', async () => {
|
|
580
|
+
let attempts = 0;
|
|
581
|
+
|
|
582
|
+
function openModel() {
|
|
583
|
+
attempts++;
|
|
584
|
+
if (attempts < 4) return Promise.reject(new Error('connect failed'));
|
|
585
|
+
return Promise.resolve(new FakePromiseModel(attempts));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, {
|
|
589
|
+
initialDelay: 1,
|
|
590
|
+
maxDelay: 1,
|
|
591
|
+
jitter: 0,
|
|
592
|
+
initialMaxRetries: Infinity,
|
|
593
|
+
maxRetries: 1,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
assert.equal(4, attempts);
|
|
597
|
+
await client.close();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('rejects the callback connect once the initial budget is exhausted', (_t, done) => {
|
|
601
|
+
let attempts = 0;
|
|
602
|
+
|
|
603
|
+
function openModel() {
|
|
604
|
+
attempts++;
|
|
605
|
+
return Promise.reject(new Error('connect failed'));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
recovery.connectWithRecoveryCallback(
|
|
609
|
+
openModel,
|
|
610
|
+
{ initialDelay: 1, maxDelay: 1, jitter: 0, initialMaxRetries: 1, maxRetries: Infinity },
|
|
611
|
+
(err) => {
|
|
612
|
+
try {
|
|
613
|
+
assert.equal('connect failed', err.message);
|
|
614
|
+
assert.equal(2, attempts);
|
|
615
|
+
done();
|
|
616
|
+
} catch (e) {
|
|
617
|
+
done(e);
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
227
624
|
describe('waitForConnect: false', () => {
|
|
228
625
|
function watchUnhandledRejections(t) {
|
|
229
626
|
const rejections = [];
|
|
@@ -240,7 +637,7 @@ describe('recovery', () => {
|
|
|
240
637
|
return Promise.resolve(new FakePromiseModel(++opened));
|
|
241
638
|
}
|
|
242
639
|
|
|
243
|
-
const client = await recovery.connectWithRecoveryPromise(openModel, {waitForConnect: false});
|
|
640
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, { waitForConnect: false });
|
|
244
641
|
|
|
245
642
|
assert.equal(0, opened);
|
|
246
643
|
|
|
@@ -288,7 +685,7 @@ describe('recovery', () => {
|
|
|
288
685
|
return Promise.resolve(new FakePromiseModel(1));
|
|
289
686
|
}
|
|
290
687
|
|
|
291
|
-
const client = await recovery.connectWithRecoveryPromise(openModel, {waitForConnect: false});
|
|
688
|
+
const client = await recovery.connectWithRecoveryPromise(openModel, { waitForConnect: false });
|
|
292
689
|
const connected = await client.waitForConnect();
|
|
293
690
|
|
|
294
691
|
assert.strictEqual(client, connected);
|
|
@@ -333,8 +730,8 @@ describe('recovery', () => {
|
|
|
333
730
|
|
|
334
731
|
await client.close();
|
|
335
732
|
|
|
336
|
-
await assert.rejects(pendingChannel, {message: 'Connection closed'});
|
|
337
|
-
await assert.rejects(client.waitForConnect(), {message: 'Connection closed'});
|
|
733
|
+
await assert.rejects(pendingChannel, { message: 'Connection closed' });
|
|
734
|
+
await assert.rejects(client.waitForConnect(), { message: 'Connection closed' });
|
|
338
735
|
|
|
339
736
|
releaseOpen();
|
|
340
737
|
await new Promise((resolve) => setImmediate(resolve));
|
|
@@ -366,7 +763,7 @@ describe('recovery', () => {
|
|
|
366
763
|
assert.equal('connect failed', err.message);
|
|
367
764
|
assert.equal(2, attempts);
|
|
368
765
|
|
|
369
|
-
await assert.rejects(client.waitForConnect(), {message: 'connect failed'});
|
|
766
|
+
await assert.rejects(client.waitForConnect(), { message: 'connect failed' });
|
|
370
767
|
await new Promise((resolve) => setImmediate(resolve));
|
|
371
768
|
assert.deepEqual([], rejections);
|
|
372
769
|
|
|
@@ -381,7 +778,7 @@ describe('recovery', () => {
|
|
|
381
778
|
}
|
|
382
779
|
|
|
383
780
|
const failures = [];
|
|
384
|
-
const client = recovery.connectWithRecoveryCallback(openModel, {waitForConnect: false}, (err, c) => {
|
|
781
|
+
const client = recovery.connectWithRecoveryCallback(openModel, { waitForConnect: false }, (err, c) => {
|
|
385
782
|
if (err) return done(err);
|
|
386
783
|
assert.strictEqual(client, c);
|
|
387
784
|
assert.equal(0, opened);
|