spice-couchbase 2.0.63 → 2.0.64
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/build/connectionHealth.js +114 -149
- package/build/connectionHealth.test.js +301 -316
- package/package.json +1 -1
- package/src/connectionHealth.js +48 -7
- package/src/connectionHealth.test.js +53 -4
package/package.json
CHANGED
package/src/connectionHealth.js
CHANGED
|
@@ -215,8 +215,43 @@ function createHealthMonitor({
|
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// KV is checked with a bucket ping, not a document operation: the boot-time
|
|
219
|
+
// collection handle can name a collection that does not exist (a named
|
|
220
|
+
// scope has no `_default` collection), and KV operations against it retry
|
|
221
|
+
// until they time out even though every real request succeeds.
|
|
222
|
+
async function probeKv(bucket, timeout) {
|
|
223
|
+
let report;
|
|
224
|
+
try {
|
|
225
|
+
report = await bucket.ping({
|
|
226
|
+
serviceTypes: [couchbase.ServiceType.KeyValue],
|
|
227
|
+
timeout,
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
throw labelled(error, "KV ping");
|
|
231
|
+
}
|
|
232
|
+
const endpoints = Object.values(report?.services || {}).flat();
|
|
233
|
+
// One node answering is enough. A single node being down is the cluster's
|
|
234
|
+
// problem, and a fresh connection would not fix it.
|
|
235
|
+
if (!endpoints.some((endpoint) => endpoint?.state === couchbase.PingState.Ok)) {
|
|
236
|
+
const states = endpoints
|
|
237
|
+
.map((endpoint) => couchbase.PingState[endpoint?.state] ?? endpoint?.state)
|
|
238
|
+
.join(", ");
|
|
239
|
+
throw labelled(
|
|
240
|
+
new Error(`no endpoint responded (${states || "no endpoints"})`),
|
|
241
|
+
"KV ping",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function labelled(error, label) {
|
|
247
|
+
const wrapped = new Error(`${label} failed: ${describe(error)}`);
|
|
248
|
+
wrapped.cause = error;
|
|
249
|
+
wrapped.spiceConnectivity = true;
|
|
250
|
+
return wrapped;
|
|
251
|
+
}
|
|
252
|
+
|
|
218
253
|
async function probe(handles) {
|
|
219
|
-
if (!handles?.cluster || !handles?.
|
|
254
|
+
if (!handles?.cluster || !handles?.bucket) {
|
|
220
255
|
const error = new Error("No cluster handles to probe");
|
|
221
256
|
error.spiceConnectivity = true;
|
|
222
257
|
throw error;
|
|
@@ -225,8 +260,10 @@ function createHealthMonitor({
|
|
|
225
260
|
// The hard deadline is the point: a wedged SDK may never honour its own.
|
|
226
261
|
await deadline(
|
|
227
262
|
Promise.all([
|
|
228
|
-
handles.
|
|
229
|
-
handles.cluster.query("SELECT 1", { timeout })
|
|
263
|
+
probeKv(handles.bucket, timeout),
|
|
264
|
+
handles.cluster.query("SELECT 1", { timeout }).catch((error) => {
|
|
265
|
+
throw labelled(error, "Query probe");
|
|
266
|
+
}),
|
|
230
267
|
]),
|
|
231
268
|
timeout + 1000,
|
|
232
269
|
"Couchbase health probe",
|
|
@@ -326,10 +363,14 @@ function createHealthMonitor({
|
|
|
326
363
|
couchbase.ServiceType?.Query,
|
|
327
364
|
].filter(Boolean);
|
|
328
365
|
await deadline(
|
|
329
|
-
handles.bucket
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
366
|
+
handles.bucket
|
|
367
|
+
.waitUntilReady(
|
|
368
|
+
settings.reconnectTimeoutMs,
|
|
369
|
+
serviceTypes.length ? { serviceTypes } : undefined,
|
|
370
|
+
)
|
|
371
|
+
.catch((error) => {
|
|
372
|
+
throw labelled(error, "waitUntilReady");
|
|
373
|
+
}),
|
|
333
374
|
settings.reconnectTimeoutMs + 1000,
|
|
334
375
|
"Couchbase waitUntilReady",
|
|
335
376
|
);
|
|
@@ -28,6 +28,7 @@ function fakeCluster(id, { healthy = true, hang = false } = {}) {
|
|
|
28
28
|
hang,
|
|
29
29
|
closed: 0,
|
|
30
30
|
queries: 0,
|
|
31
|
+
kvStates: [couchbase.PingState.Ok],
|
|
31
32
|
async close() {
|
|
32
33
|
cluster.closed += 1;
|
|
33
34
|
},
|
|
@@ -45,6 +46,13 @@ function fakeCluster(id, { healthy = true, hang = false } = {}) {
|
|
|
45
46
|
async waitUntilReady() {
|
|
46
47
|
if (!cluster.healthy) throw timeoutError();
|
|
47
48
|
},
|
|
49
|
+
ping() {
|
|
50
|
+
if (cluster.hang) return new Promise(() => {});
|
|
51
|
+
if (!cluster.healthy) return Promise.reject(timeoutError());
|
|
52
|
+
return Promise.resolve({
|
|
53
|
+
services: { kv: cluster.kvStates.map((state) => ({ state })) },
|
|
54
|
+
});
|
|
55
|
+
},
|
|
48
56
|
scope(scopeName) {
|
|
49
57
|
return {
|
|
50
58
|
name: scopeName,
|
|
@@ -53,11 +61,10 @@ function fakeCluster(id, { healthy = true, hang = false } = {}) {
|
|
|
53
61
|
return {
|
|
54
62
|
name: collectionName,
|
|
55
63
|
cluster,
|
|
64
|
+
// Like `_default` inside a named scope: the collection does
|
|
65
|
+
// not exist, so KV operations on it always time out.
|
|
56
66
|
exists() {
|
|
57
|
-
|
|
58
|
-
return cluster.healthy ?
|
|
59
|
-
Promise.resolve({ exists: false })
|
|
60
|
-
: Promise.reject(timeoutError());
|
|
67
|
+
return Promise.reject(timeoutError());
|
|
61
68
|
},
|
|
62
69
|
};
|
|
63
70
|
},
|
|
@@ -166,6 +173,48 @@ test("a healthy probe leaves the connection alone", async () => {
|
|
|
166
173
|
assert.equal(monitor.status().default.healthy, true);
|
|
167
174
|
});
|
|
168
175
|
|
|
176
|
+
test("the probe does not depend on the boot-time collection existing", async () => {
|
|
177
|
+
// The fake collection's KV operations always time out; ping still succeeds.
|
|
178
|
+
const { monitor, opened } = setup({ settings: { probeFailureThreshold: 1 } });
|
|
179
|
+
assert.equal(await monitor.check("default"), true);
|
|
180
|
+
assert.equal(opened.length, 0);
|
|
181
|
+
assert.equal(monitor.status().default.healthy, true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("one answering KV node is enough, none is unhealthy", async () => {
|
|
185
|
+
const { monitor, initial, opened } = setup({
|
|
186
|
+
settings: { probeFailureThreshold: 1 },
|
|
187
|
+
});
|
|
188
|
+
const { Ok, Timeout, Error: Failed } = couchbase.PingState;
|
|
189
|
+
initial.kvStates = [Timeout, Ok, Failed];
|
|
190
|
+
assert.equal(await monitor.check("default"), true);
|
|
191
|
+
assert.equal(opened.length, 0);
|
|
192
|
+
|
|
193
|
+
initial.kvStates = [Timeout, Failed];
|
|
194
|
+
await monitor.check("default");
|
|
195
|
+
assert.equal(opened.length, 1);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("probe failures say which check failed", async () => {
|
|
199
|
+
const { monitor, initial } = setup({
|
|
200
|
+
settings: { probeFailureThreshold: 1 },
|
|
201
|
+
connect: async () => fakeCluster("bad", { healthy: false }),
|
|
202
|
+
});
|
|
203
|
+
initial.kvStates = [];
|
|
204
|
+
await monitor.check("default");
|
|
205
|
+
assert.match(monitor.status().default.lastError, /waitUntilReady failed/);
|
|
206
|
+
|
|
207
|
+
const query = setup({ settings: { probeFailureThreshold: 1 } });
|
|
208
|
+
query.initial.query = () => Promise.reject(timeoutError());
|
|
209
|
+
await query.monitor.check("default");
|
|
210
|
+
assert.match(query.logs.join("\n"), /unhealthy: Query probe failed/);
|
|
211
|
+
|
|
212
|
+
const kv = setup({ settings: { probeFailureThreshold: 1 } });
|
|
213
|
+
kv.initial.kvStates = [];
|
|
214
|
+
await kv.monitor.check("default");
|
|
215
|
+
assert.match(kv.logs.join("\n"), /unhealthy: KV ping failed: no endpoint responded/);
|
|
216
|
+
});
|
|
217
|
+
|
|
169
218
|
test("non-connectivity errors never trigger a probe", async () => {
|
|
170
219
|
const { monitor, initial } = setup();
|
|
171
220
|
for (let i = 0; i < 10; i++) {
|