spice-couchbase 2.0.62 → 2.0.63
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 +573 -0
- package/build/connectionHealth.test.js +659 -0
- package/build/index.js +219 -41
- package/package.json +3 -2
- package/src/connectionHealth.js +529 -0
- package/src/connectionHealth.test.js +412 -0
- package/src/index.js +198 -55
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ let couchbase = require("couchbase");
|
|
|
5
5
|
const { ConflictResolutionType } = require("couchbase");
|
|
6
6
|
const { QueryScanConsistency } = require("couchbase");
|
|
7
7
|
const { parseResourceList, extractStatementResources } = require("./utility");
|
|
8
|
+
const { createHealthMonitor, openHandles } = require("./connectionHealth");
|
|
8
9
|
|
|
9
10
|
if (!Promise.allSettled) {
|
|
10
11
|
Promise.allSettled = (promises) =>
|
|
@@ -71,6 +72,27 @@ const OPTIMIZE_ENABLED_RESOURCES = parseResourceList(
|
|
|
71
72
|
);
|
|
72
73
|
let autoOptimizePolicy = null;
|
|
73
74
|
|
|
75
|
+
// One health monitor per process, shared by every copy of this module that
|
|
76
|
+
// ends up loaded. See connectionHealth.js.
|
|
77
|
+
const HEALTH_MONITOR_KEY = Symbol.for("spice-couchbase.health-monitor");
|
|
78
|
+
const health =
|
|
79
|
+
globalThis[HEALTH_MONITOR_KEY] ||
|
|
80
|
+
(globalThis[HEALTH_MONITOR_KEY] = createHealthMonitor());
|
|
81
|
+
|
|
82
|
+
// Cluster handles live on the `spice.database` globals, which a reconnect
|
|
83
|
+
// replaces. Instances read them at call time instead of copying them.
|
|
84
|
+
const HANDLE_GLOBALS = {
|
|
85
|
+
cluster: "clusters",
|
|
86
|
+
connection: "connections",
|
|
87
|
+
scope: "scopes",
|
|
88
|
+
bucket_handle: "buckets",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function globalHandle(kind, connection_name) {
|
|
92
|
+
if (typeof spice === "undefined") return undefined;
|
|
93
|
+
return spice?.database?.[HANDLE_GLOBALS[kind]]?.[connection_name];
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
function requestedAutoOptimize(query_options) {
|
|
75
97
|
if (
|
|
76
98
|
!query_options ||
|
|
@@ -164,13 +186,9 @@ module.exports = class Database {
|
|
|
164
186
|
) {
|
|
165
187
|
if (connection) {
|
|
166
188
|
this.connection_name = connection;
|
|
167
|
-
this.connection = spice.database.connections[connection];
|
|
168
189
|
this.query_scope = query_scope;
|
|
169
|
-
this.cluster = spice.database.clusters[connection];
|
|
170
190
|
this.bucket = this.connection._scope._bucket._name;
|
|
171
191
|
this.db_config = spice.config.database.connections[connection];
|
|
172
|
-
this.collection_manager =
|
|
173
|
-
spice.database.buckets[connection].collections();
|
|
174
192
|
this.auto_create_collection =
|
|
175
193
|
auto_create_collection ||
|
|
176
194
|
((collection ||
|
|
@@ -196,14 +214,11 @@ module.exports = class Database {
|
|
|
196
214
|
scope ||
|
|
197
215
|
spice.config.database.connections[connection].scope ||
|
|
198
216
|
"_default";
|
|
199
|
-
this.scope = spice.database.scopes[connection];
|
|
200
217
|
this.full_text_index =
|
|
201
218
|
spice.config.database.connections[connection].full_text_index ||
|
|
202
219
|
full_text_index;
|
|
203
220
|
this.N1qlQuery = couchbase.N1qlQuery;
|
|
204
221
|
this.SearchQuery = couchbase.SearchQuery;
|
|
205
|
-
this.collection = this.scope.collection(this.collection_name);
|
|
206
|
-
this.userManager = this.getUserManager(connection);
|
|
207
222
|
if (!spice?.db?.collections) {
|
|
208
223
|
spice.db = spice.db || {};
|
|
209
224
|
spice.db.collections = new Set();
|
|
@@ -230,6 +245,137 @@ module.exports = class Database {
|
|
|
230
245
|
}
|
|
231
246
|
}
|
|
232
247
|
|
|
248
|
+
// Handles resolve from the globals on every access, so an instance created
|
|
249
|
+
// before a reconnect uses the new cluster afterwards. Assigning one pins it
|
|
250
|
+
// on that instance instead (tests, custom wiring).
|
|
251
|
+
pinHandle(kind, value) {
|
|
252
|
+
if (!Object.prototype.hasOwnProperty.call(this, "_pinned_handles")) {
|
|
253
|
+
Object.defineProperty(this, "_pinned_handles", {
|
|
254
|
+
value: {},
|
|
255
|
+
enumerable: false,
|
|
256
|
+
writable: true,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
this._pinned_handles[kind] = value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
pinnedHandle(kind) {
|
|
263
|
+
const pinned = this._pinned_handles;
|
|
264
|
+
return pinned && kind in pinned ? { value: pinned[kind] } : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
handle(kind) {
|
|
268
|
+
const pinned = this.pinnedHandle(kind);
|
|
269
|
+
return pinned ? pinned.value : globalHandle(kind, this.connection_name);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
get cluster() {
|
|
273
|
+
return this.handle("cluster");
|
|
274
|
+
}
|
|
275
|
+
set cluster(value) {
|
|
276
|
+
this.pinHandle("cluster", value);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get connection() {
|
|
280
|
+
return this.handle("connection");
|
|
281
|
+
}
|
|
282
|
+
set connection(value) {
|
|
283
|
+
this.pinHandle("connection", value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
get scope() {
|
|
287
|
+
return this.handle("scope");
|
|
288
|
+
}
|
|
289
|
+
set scope(value) {
|
|
290
|
+
this.pinHandle("scope", value);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
get collection() {
|
|
294
|
+
const pinned = this.pinnedHandle("collection");
|
|
295
|
+
if (pinned) return pinned.value;
|
|
296
|
+
const scope = this.scope;
|
|
297
|
+
if (!scope) return undefined;
|
|
298
|
+
// Collection handles are cheap but not free; cache one per scope object,
|
|
299
|
+
// which changes exactly when the connection is replaced.
|
|
300
|
+
if (this._collection_cache?.scope !== scope) {
|
|
301
|
+
Object.defineProperty(this, "_collection_cache", {
|
|
302
|
+
value: { scope, collection: scope.collection(this.collection_name) },
|
|
303
|
+
enumerable: false,
|
|
304
|
+
writable: true,
|
|
305
|
+
configurable: true,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return this._collection_cache.collection;
|
|
309
|
+
}
|
|
310
|
+
set collection(value) {
|
|
311
|
+
this.pinHandle("collection", value);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
get collection_manager() {
|
|
315
|
+
const pinned = this.pinnedHandle("collection_manager");
|
|
316
|
+
if (pinned) return pinned.value;
|
|
317
|
+
return this.handle("bucket_handle")?.collections();
|
|
318
|
+
}
|
|
319
|
+
set collection_manager(value) {
|
|
320
|
+
this.pinHandle("collection_manager", value);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
get userManager() {
|
|
324
|
+
const pinned = this.pinnedHandle("userManager");
|
|
325
|
+
if (pinned) return pinned.value;
|
|
326
|
+
return this.cluster?.users();
|
|
327
|
+
}
|
|
328
|
+
set userManager(value) {
|
|
329
|
+
this.pinHandle("userManager", value);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Reports a finished operation to the health monitor. Only connectivity
|
|
334
|
+
* errors (timeouts, cancelled requests, service not available) count.
|
|
335
|
+
*/
|
|
336
|
+
recordOutcome(error) {
|
|
337
|
+
health.recordOutcome(this.connection_name, error);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
recordSettled(results) {
|
|
341
|
+
for (const result of results) {
|
|
342
|
+
if (result?.status === "rejected") this.recordOutcome(result.reason);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async observe(operation) {
|
|
347
|
+
try {
|
|
348
|
+
return await operation;
|
|
349
|
+
} catch (error) {
|
|
350
|
+
this.recordOutcome(error);
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Per-connection health: healthy, reconnecting, generation, last error. */
|
|
356
|
+
static healthStatus() {
|
|
357
|
+
return health.status();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Probes a connection now and reconnects it if the probe fails. */
|
|
361
|
+
static checkHealth(connection_name = "default") {
|
|
362
|
+
return health.check(connection_name);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Opens a fresh cluster for a connection and swaps it in. */
|
|
366
|
+
static forceReconnect(connection_name = "default") {
|
|
367
|
+
return health.reconnect(connection_name);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Stops probing and reconnecting and closes replaced clusters still in
|
|
372
|
+
* their grace period. Call before closing `spice.database.clusters` on
|
|
373
|
+
* shutdown so a reconnect cannot race it.
|
|
374
|
+
*/
|
|
375
|
+
static stopHealthMonitor() {
|
|
376
|
+
return health.stop();
|
|
377
|
+
}
|
|
378
|
+
|
|
233
379
|
/**
|
|
234
380
|
* Lets the application decide per resource whether slow statements may
|
|
235
381
|
* create covering indexes, e.g. from a per-resource setting an operator
|
|
@@ -324,26 +470,9 @@ module.exports = class Database {
|
|
|
324
470
|
}
|
|
325
471
|
|
|
326
472
|
async connect(connection_name, config) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
};
|
|
331
|
-
if (config.configProfile) {
|
|
332
|
-
connectOptions.configProfile = config.configProfile;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
let cluster = await couchbase.connect(config.nodes, connectOptions);
|
|
336
|
-
let bucket = cluster.bucket(config.bucket);
|
|
337
|
-
let collection;
|
|
338
|
-
let scope_name = this.scope_name || config.scope || "_default";
|
|
339
|
-
let collection_name =
|
|
340
|
-
this.collection_name || config.collection || "_default";
|
|
341
|
-
let scope = bucket.scope(scope_name);
|
|
342
|
-
|
|
343
|
-
collection = bucket
|
|
344
|
-
.scope(scope_name)
|
|
345
|
-
.collection(collection_name || "_default");
|
|
346
|
-
|
|
473
|
+
const { cluster, bucket, scope, collection } = await openHandles(config);
|
|
474
|
+
// Watch the connection from here on and replace it if it wedges.
|
|
475
|
+
health.register(connection_name, config);
|
|
347
476
|
return {
|
|
348
477
|
connection: connection_name,
|
|
349
478
|
bucket,
|
|
@@ -354,7 +483,7 @@ module.exports = class Database {
|
|
|
354
483
|
}
|
|
355
484
|
|
|
356
485
|
getUserManager(connection) {
|
|
357
|
-
return
|
|
486
|
+
return globalHandle("cluster", connection).users();
|
|
358
487
|
}
|
|
359
488
|
|
|
360
489
|
async assignUserPermission() {
|
|
@@ -534,7 +663,7 @@ module.exports = class Database {
|
|
|
534
663
|
this.use_bucket_name ? this.connection : this.collection;
|
|
535
664
|
|
|
536
665
|
if (!useLookupIn) {
|
|
537
|
-
let response = await collection.get(doc_id);
|
|
666
|
+
let response = await this.observe(collection.get(doc_id));
|
|
538
667
|
return { id: doc_id, ...response.content };
|
|
539
668
|
}
|
|
540
669
|
|
|
@@ -549,6 +678,7 @@ module.exports = class Database {
|
|
|
549
678
|
});
|
|
550
679
|
|
|
551
680
|
const batchResults = await Promise.allSettled(batchPromises);
|
|
681
|
+
this.recordSettled(batchResults);
|
|
552
682
|
|
|
553
683
|
// Merge results from all batches
|
|
554
684
|
batchResults.forEach((result, batchIndex) => {
|
|
@@ -570,12 +700,14 @@ module.exports = class Database {
|
|
|
570
700
|
|
|
571
701
|
async insert(doc_id, document, expiry) {
|
|
572
702
|
try {
|
|
573
|
-
await
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
703
|
+
await this.observe(
|
|
704
|
+
(this.use_bucket_name ? this.connection : this.collection).insert(
|
|
705
|
+
doc_id,
|
|
706
|
+
document,
|
|
707
|
+
{
|
|
708
|
+
expiry: expiry,
|
|
709
|
+
},
|
|
710
|
+
),
|
|
579
711
|
);
|
|
580
712
|
} catch (e) {
|
|
581
713
|
console.log(e.message, e, doc_id);
|
|
@@ -584,38 +716,45 @@ module.exports = class Database {
|
|
|
584
716
|
}
|
|
585
717
|
|
|
586
718
|
async touch(doc_id, expiry = 0) {
|
|
587
|
-
await
|
|
588
|
-
|
|
589
|
-
|
|
719
|
+
await this.observe(
|
|
720
|
+
(this.use_bucket_name ? this.connection : this.collection).touch(
|
|
721
|
+
doc_id,
|
|
722
|
+
expiry,
|
|
723
|
+
),
|
|
590
724
|
);
|
|
591
725
|
return await this.get(doc_id);
|
|
592
726
|
}
|
|
593
727
|
|
|
594
728
|
async count(doc_id, adjustment, initial_value) {
|
|
595
|
-
let res = await (
|
|
596
|
-
this.use_bucket_name ?
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
729
|
+
let res = await this.observe(
|
|
730
|
+
(this.use_bucket_name ? this.connection : this.collection).counter(
|
|
731
|
+
doc_id,
|
|
732
|
+
adjustment,
|
|
733
|
+
{
|
|
734
|
+
initial: initial_value,
|
|
735
|
+
},
|
|
736
|
+
),
|
|
737
|
+
);
|
|
601
738
|
return res.value;
|
|
602
739
|
}
|
|
603
740
|
|
|
604
741
|
async update(doc_id, document, expiry) {
|
|
605
|
-
let res = await (
|
|
606
|
-
this.use_bucket_name ?
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
742
|
+
let res = await this.observe(
|
|
743
|
+
(this.use_bucket_name ? this.connection : this.collection).replace(
|
|
744
|
+
doc_id,
|
|
745
|
+
document,
|
|
746
|
+
{
|
|
747
|
+
expiry: expiry,
|
|
748
|
+
},
|
|
749
|
+
),
|
|
750
|
+
);
|
|
611
751
|
return res.value;
|
|
612
752
|
}
|
|
613
753
|
|
|
614
754
|
async delete(doc_id) {
|
|
615
|
-
let res = await (
|
|
616
|
-
this.use_bucket_name ?
|
|
617
|
-
|
|
618
|
-
: this.collection).remove(doc_id);
|
|
755
|
+
let res = await this.observe(
|
|
756
|
+
(this.use_bucket_name ? this.connection : this.collection).remove(doc_id),
|
|
757
|
+
);
|
|
619
758
|
return res.value;
|
|
620
759
|
}
|
|
621
760
|
|
|
@@ -632,6 +771,7 @@ module.exports = class Database {
|
|
|
632
771
|
// Regular get - no batching needed
|
|
633
772
|
let functions = _.map(keys, (key) => collection.get(key));
|
|
634
773
|
let returned_all = await Promise.allSettled(functions);
|
|
774
|
+
this.recordSettled(returned_all);
|
|
635
775
|
|
|
636
776
|
let responses = _.flatten(
|
|
637
777
|
_.map(returned_all, (returned_obj) => returned_obj?.value),
|
|
@@ -670,6 +810,7 @@ module.exports = class Database {
|
|
|
670
810
|
});
|
|
671
811
|
|
|
672
812
|
const batchResults = await Promise.allSettled(batchPromises);
|
|
813
|
+
this.recordSettled(batchResults);
|
|
673
814
|
|
|
674
815
|
// Merge results from all batches
|
|
675
816
|
batchResults.forEach((result, batchIndex) => {
|
|
@@ -1068,7 +1209,9 @@ module.exports = class Database {
|
|
|
1068
1209
|
// Start time
|
|
1069
1210
|
let startTime = process.hrtime.bigint();
|
|
1070
1211
|
const options = buildQueryOptions(query_options);
|
|
1071
|
-
let response = await
|
|
1212
|
+
let response = await this.observe(
|
|
1213
|
+
exec_scope.query(sanitizedQuery, options),
|
|
1214
|
+
);
|
|
1072
1215
|
// End time
|
|
1073
1216
|
let endTime = process.hrtime.bigint();
|
|
1074
1217
|
// Calculate duration in microseconds
|