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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-couchbase",
3
- "version": "2.0.63",
3
+ "version": "2.0.64",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -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?.collection) {
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.collection.exists("__spice_health_probe__", { timeout }),
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.waitUntilReady(
330
- settings.reconnectTimeoutMs,
331
- serviceTypes.length ? { serviceTypes } : undefined,
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
- if (cluster.hang) return new Promise(() => {});
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++) {