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.
@@ -0,0 +1,659 @@
1
+ "use strict";
2
+
3
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
4
+
5
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
6
+
7
+ function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
8
+
9
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
10
+
11
+ function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
12
+
13
+ var assert = require("node:assert/strict");
14
+
15
+ var {
16
+ test
17
+ } = require("node:test");
18
+
19
+ var couchbase = require("couchbase");
20
+
21
+ var {
22
+ createHealthMonitor,
23
+ buildConnectOptions,
24
+ isConnectivityError
25
+ } = require("./connectionHealth");
26
+
27
+ var config = {
28
+ nodes: "couchbase://db",
29
+ bucket: "saturn",
30
+ scope: "app",
31
+ username: "u",
32
+ password: "p"
33
+ };
34
+
35
+ var timeoutError = () => new couchbase.UnambiguousTimeoutError(); // A fake cluster whose probe behaviour can be flipped between healthy and
36
+ // wedged. `hang` makes operations never settle, like a stuck SDK.
37
+
38
+
39
+ function fakeCluster(id) {
40
+ var {
41
+ healthy = true,
42
+ hang = false
43
+ } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
44
+ var cluster = {
45
+ id,
46
+ healthy,
47
+ hang,
48
+ closed: 0,
49
+ queries: 0,
50
+
51
+ close() {
52
+ return _asyncToGenerator(function* () {
53
+ cluster.closed += 1;
54
+ })();
55
+ },
56
+
57
+ query() {
58
+ cluster.queries += 1;
59
+ if (cluster.hang) return new Promise(() => {});
60
+ return cluster.healthy ? Promise.resolve({
61
+ rows: [{
62
+ $1: 1
63
+ }]
64
+ }) : Promise.reject(timeoutError());
65
+ },
66
+
67
+ bucket(name) {
68
+ var bucket = {
69
+ name,
70
+ cluster,
71
+
72
+ waitUntilReady() {
73
+ return _asyncToGenerator(function* () {
74
+ if (!cluster.healthy) throw timeoutError();
75
+ })();
76
+ },
77
+
78
+ scope(scopeName) {
79
+ return {
80
+ name: scopeName,
81
+ bucket,
82
+
83
+ collection(collectionName) {
84
+ return {
85
+ name: collectionName,
86
+ cluster,
87
+
88
+ exists() {
89
+ if (cluster.hang) return new Promise(() => {});
90
+ return cluster.healthy ? Promise.resolve({
91
+ exists: false
92
+ }) : Promise.reject(timeoutError());
93
+ }
94
+
95
+ };
96
+ }
97
+
98
+ };
99
+ }
100
+
101
+ };
102
+ return bucket;
103
+ }
104
+
105
+ };
106
+ return cluster;
107
+ }
108
+
109
+ function handlesFor(cluster) {
110
+ var bucket = cluster.bucket(config.bucket);
111
+ var scope = bucket.scope(config.scope);
112
+ return {
113
+ cluster,
114
+ bucket,
115
+ scope,
116
+ collection: scope.collection("_default")
117
+ };
118
+ }
119
+
120
+ function setup() {
121
+ var {
122
+ settings = {},
123
+ connect,
124
+ clock
125
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
126
+ var initial = fakeCluster("initial");
127
+ var handles = handlesFor(initial);
128
+ var globals = {
129
+ database: {
130
+ clusters: {
131
+ default: handles.cluster
132
+ },
133
+ buckets: {
134
+ default: handles.bucket
135
+ },
136
+ scopes: {
137
+ default: handles.scope
138
+ },
139
+ connections: {
140
+ default: handles.collection
141
+ }
142
+ },
143
+ checked_collections: {
144
+ users: true
145
+ },
146
+ db: {
147
+ collections: new Set(["users"])
148
+ }
149
+ };
150
+ var opened = [];
151
+ var time = clock || {
152
+ now: 1000
153
+ };
154
+ var terminated = [];
155
+ var logs = [];
156
+ var log = {
157
+ log: m => logs.push(m),
158
+ warn: m => logs.push(m),
159
+ error: m => logs.push(m)
160
+ };
161
+ var monitor = createHealthMonitor({
162
+ settings: _objectSpread({
163
+ healthIntervalMs: 0,
164
+ probeTimeoutMs: 20,
165
+ probeFailureThreshold: 2,
166
+ failureThreshold: 3,
167
+ failureWindowMs: 1000,
168
+ reconnectTimeoutMs: 50,
169
+ reconnectBackoffMinMs: 100,
170
+ reconnectBackoffMaxMs: 1000,
171
+ retireGraceMs: 5,
172
+ exitAfterMs: 10000
173
+ }, settings),
174
+ connect: connect ||
175
+ /*#__PURE__*/
176
+ _asyncToGenerator(function* () {
177
+ var cluster = fakeCluster("fresh-".concat(opened.length + 1));
178
+ opened.push(cluster);
179
+ return cluster;
180
+ }),
181
+ now: () => time.now,
182
+ random: () => 0.5,
183
+ log,
184
+ getGlobals: () => globals,
185
+ terminate: () => terminated.push(time.now)
186
+ });
187
+ monitor.register("default", config);
188
+ return {
189
+ monitor,
190
+ initial,
191
+ globals,
192
+ opened,
193
+ time,
194
+ terminated,
195
+ logs
196
+ };
197
+ }
198
+
199
+ var sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
200
+
201
+ test("only connectivity errors count", () => {
202
+ assert.equal(isConnectivityError(timeoutError()), true);
203
+ assert.equal(isConnectivityError(new couchbase.AmbiguousTimeoutError()), true);
204
+ assert.equal(isConnectivityError(new couchbase.RequestCanceledError()), true);
205
+ assert.equal(isConnectivityError(new couchbase.ServiceNotAvailableError()), true);
206
+ assert.equal(isConnectivityError(new couchbase.DocumentNotFoundError()), false);
207
+ assert.equal(isConnectivityError(new Error("syntax error")), false);
208
+ assert.equal(isConnectivityError(undefined), false);
209
+ });
210
+ test("connect options carry only the timeouts that are set", () => {
211
+ assert.deepEqual(buildConnectOptions(config), {
212
+ username: "u",
213
+ password: "p"
214
+ });
215
+ assert.deepEqual(buildConnectOptions(_objectSpread({}, config, {
216
+ configProfile: "wanDevelopment",
217
+ timeouts: {
218
+ queryTimeout: "30000",
219
+ kvTimeout: undefined,
220
+ connectTimeout: ""
221
+ }
222
+ })), {
223
+ username: "u",
224
+ password: "p",
225
+ configProfile: "wanDevelopment",
226
+ timeouts: {
227
+ queryTimeout: 30000
228
+ }
229
+ });
230
+ });
231
+ test("a healthy probe leaves the connection alone",
232
+ /*#__PURE__*/
233
+ _asyncToGenerator(function* () {
234
+ var {
235
+ monitor,
236
+ opened,
237
+ globals,
238
+ initial
239
+ } = setup();
240
+ assert.equal((yield monitor.check("default")), true);
241
+ assert.equal(opened.length, 0);
242
+ assert.equal(globals.database.clusters.default, initial);
243
+ assert.equal(monitor.status().default.healthy, true);
244
+ }));
245
+ test("non-connectivity errors never trigger a probe",
246
+ /*#__PURE__*/
247
+ _asyncToGenerator(function* () {
248
+ var {
249
+ monitor,
250
+ initial
251
+ } = setup();
252
+
253
+ for (var i = 0; i < 10; i++) {
254
+ monitor.recordOutcome("default", new couchbase.DocumentNotFoundError());
255
+ }
256
+
257
+ yield sleep(5);
258
+ assert.equal(initial.queries, 0);
259
+ }));
260
+ test("one failed probe is not enough, two trigger a reconnect",
261
+ /*#__PURE__*/
262
+ _asyncToGenerator(function* () {
263
+ var {
264
+ monitor,
265
+ initial,
266
+ opened
267
+ } = setup();
268
+ initial.healthy = false;
269
+ yield monitor.check("default");
270
+ assert.equal(opened.length, 0);
271
+ assert.equal(monitor.status().default.healthy, true);
272
+ yield monitor.check("default");
273
+ assert.equal(opened.length, 1);
274
+ assert.equal(monitor.status().default.healthy, true);
275
+ assert.equal(monitor.status().default.generation, 2);
276
+ }));
277
+ test("a burst of operation timeouts probes and reconnects at once",
278
+ /*#__PURE__*/
279
+ _asyncToGenerator(function* () {
280
+ var {
281
+ monitor,
282
+ initial,
283
+ opened,
284
+ globals
285
+ } = setup();
286
+ initial.healthy = false;
287
+
288
+ for (var i = 0; i < 3; i++) {
289
+ monitor.recordOutcome("default", timeoutError());
290
+ }
291
+
292
+ yield sleep(10);
293
+ assert.equal(opened.length, 1);
294
+ assert.equal(globals.database.clusters.default, opened[0]);
295
+ }));
296
+ test("timeouts outside the window do not add up",
297
+ /*#__PURE__*/
298
+ _asyncToGenerator(function* () {
299
+ var {
300
+ monitor,
301
+ initial,
302
+ opened,
303
+ time
304
+ } = setup();
305
+ initial.healthy = false;
306
+ monitor.recordOutcome("default", timeoutError());
307
+ monitor.recordOutcome("default", timeoutError());
308
+ time.now += 2000;
309
+ monitor.recordOutcome("default", timeoutError());
310
+ yield sleep(10);
311
+ assert.equal(initial.queries, 0);
312
+ assert.equal(opened.length, 0);
313
+ }));
314
+ test("reconnect swaps every global handle and resets collection caches",
315
+ /*#__PURE__*/
316
+ _asyncToGenerator(function* () {
317
+ var {
318
+ monitor,
319
+ initial,
320
+ opened,
321
+ globals
322
+ } = setup();
323
+ initial.healthy = false;
324
+ yield monitor.check("default");
325
+ yield monitor.check("default");
326
+ var fresh = opened[0];
327
+ assert.equal(globals.database.clusters.default, fresh);
328
+ assert.equal(globals.database.buckets.default.cluster, fresh);
329
+ assert.equal(globals.database.scopes.default.bucket.cluster, fresh);
330
+ assert.equal(globals.database.connections.default.cluster, fresh);
331
+ assert.deepEqual(globals.checked_collections, {});
332
+ assert.equal(globals.db.collections.size, 0);
333
+ }));
334
+ test("the replaced cluster is closed after the grace period",
335
+ /*#__PURE__*/
336
+ _asyncToGenerator(function* () {
337
+ var {
338
+ monitor,
339
+ initial
340
+ } = setup();
341
+ initial.healthy = false;
342
+ yield monitor.check("default");
343
+ yield monitor.check("default");
344
+ assert.equal(initial.closed, 0);
345
+ yield sleep(20);
346
+ assert.equal(initial.closed, 1);
347
+ }));
348
+ test("concurrent triggers share a single reconnect",
349
+ /*#__PURE__*/
350
+ _asyncToGenerator(function* () {
351
+ var release;
352
+ var opened = [];
353
+ var {
354
+ monitor,
355
+ initial
356
+ } = setup({
357
+ settings: {
358
+ probeFailureThreshold: 1,
359
+ reconnectTimeoutMs: 1000
360
+ },
361
+ connect: () => new Promise(resolve => {
362
+ release = () => {
363
+ var cluster = fakeCluster("fresh");
364
+ opened.push(cluster);
365
+ resolve(cluster);
366
+ };
367
+ })
368
+ });
369
+ initial.healthy = false;
370
+ var first = monitor.check("default");
371
+ yield sleep(5);
372
+ var others = [monitor.check("default"), monitor.reconnect("default")];
373
+
374
+ for (var i = 0; i < 5; i++) {
375
+ monitor.recordOutcome("default", timeoutError());
376
+ }
377
+
378
+ release();
379
+ yield Promise.all([first, ...others]);
380
+ assert.equal(opened.length, 1);
381
+ assert.equal(monitor.status().default.generation, 2);
382
+ }));
383
+ test("a probe that hangs is cut off by the hard deadline",
384
+ /*#__PURE__*/
385
+ _asyncToGenerator(function* () {
386
+ var {
387
+ monitor,
388
+ initial,
389
+ opened
390
+ } = setup({
391
+ settings: {
392
+ probeFailureThreshold: 1
393
+ }
394
+ });
395
+ initial.hang = true; // The deadline timer is unref'd; keep the loop alive while the probe hangs.
396
+
397
+ var keepAlive = setInterval(() => {}, 1000);
398
+
399
+ try {
400
+ var started = Date.now();
401
+ yield monitor.check("default");
402
+ assert.ok(Date.now() - started < 1500);
403
+ assert.equal(opened.length, 1);
404
+ } finally {
405
+ clearInterval(keepAlive);
406
+ }
407
+ }));
408
+ test("a failed reconnect backs off and keeps the old cluster",
409
+ /*#__PURE__*/
410
+ _asyncToGenerator(function* () {
411
+ var opened = [];
412
+ var {
413
+ monitor,
414
+ initial,
415
+ globals,
416
+ time
417
+ } = setup({
418
+ settings: {
419
+ probeFailureThreshold: 1
420
+ },
421
+ connect: function () {
422
+ var _connect = _asyncToGenerator(function* () {
423
+ var cluster = fakeCluster("bad-".concat(opened.length + 1), {
424
+ healthy: false
425
+ });
426
+ opened.push(cluster);
427
+ return cluster;
428
+ });
429
+
430
+ function connect() {
431
+ return _connect.apply(this, arguments);
432
+ }
433
+
434
+ return connect;
435
+ }()
436
+ });
437
+ initial.healthy = false;
438
+ yield monitor.check("default");
439
+ assert.equal(opened.length, 1);
440
+ assert.equal(opened[0].closed, 1, "unverified cluster is closed");
441
+ assert.equal(globals.database.clusters.default, initial);
442
+ assert.equal(monitor.status().default.reconnectAttempts, 1); // Inside the backoff window: probe only, no new connect.
443
+
444
+ yield monitor.check("default");
445
+ assert.equal(opened.length, 1);
446
+ time.now += 150;
447
+ yield monitor.check("default");
448
+ assert.equal(opened.length, 2);
449
+ assert.equal(monitor.status().default.reconnectAttempts, 2);
450
+ }));
451
+ test("a connection that recovers on its own is marked healthy again",
452
+ /*#__PURE__*/
453
+ _asyncToGenerator(function* () {
454
+ var {
455
+ monitor,
456
+ initial,
457
+ time
458
+ } = setup({
459
+ settings: {
460
+ probeFailureThreshold: 1
461
+ },
462
+ connect: function () {
463
+ var _connect2 = _asyncToGenerator(function* () {
464
+ return fakeCluster("bad", {
465
+ healthy: false
466
+ });
467
+ });
468
+
469
+ function connect() {
470
+ return _connect2.apply(this, arguments);
471
+ }
472
+
473
+ return connect;
474
+ }()
475
+ });
476
+ initial.healthy = false;
477
+ yield monitor.check("default");
478
+ assert.equal(monitor.status().default.healthy, false);
479
+ initial.healthy = true;
480
+ time.now += 5000;
481
+ yield monitor.check("default");
482
+ var status = monitor.status().default;
483
+ assert.equal(status.healthy, true);
484
+ assert.equal(status.reconnectAttempts, 0);
485
+ assert.equal(status.unhealthySince, null);
486
+ }));
487
+ test("staying unhealthy past the limit terminates the process once",
488
+ /*#__PURE__*/
489
+ _asyncToGenerator(function* () {
490
+ var {
491
+ monitor,
492
+ initial,
493
+ time,
494
+ terminated
495
+ } = setup({
496
+ settings: {
497
+ probeFailureThreshold: 1,
498
+ exitAfterMs: 1000
499
+ },
500
+ connect: function () {
501
+ var _connect3 = _asyncToGenerator(function* () {
502
+ return fakeCluster("bad", {
503
+ healthy: false
504
+ });
505
+ });
506
+
507
+ function connect() {
508
+ return _connect3.apply(this, arguments);
509
+ }
510
+
511
+ return connect;
512
+ }()
513
+ });
514
+ initial.healthy = false;
515
+ yield monitor.check("default");
516
+ assert.equal(terminated.length, 0);
517
+ time.now += 1500;
518
+ yield monitor.check("default");
519
+ time.now += 1500;
520
+ yield monitor.check("default");
521
+ assert.equal(terminated.length, 1);
522
+ }));
523
+ test("exitAfterMs of 0 never terminates",
524
+ /*#__PURE__*/
525
+ _asyncToGenerator(function* () {
526
+ var {
527
+ monitor,
528
+ initial,
529
+ time,
530
+ terminated
531
+ } = setup({
532
+ settings: {
533
+ probeFailureThreshold: 1,
534
+ exitAfterMs: 0
535
+ },
536
+ connect: function () {
537
+ var _connect4 = _asyncToGenerator(function* () {
538
+ return fakeCluster("bad", {
539
+ healthy: false
540
+ });
541
+ });
542
+
543
+ function connect() {
544
+ return _connect4.apply(this, arguments);
545
+ }
546
+
547
+ return connect;
548
+ }()
549
+ });
550
+ initial.healthy = false;
551
+
552
+ for (var i = 0; i < 5; i++) {
553
+ time.now += 100000;
554
+ yield monitor.check("default");
555
+ }
556
+
557
+ assert.equal(terminated.length, 0);
558
+ }));
559
+ test("stop() halts reconnects and closes retired clusters",
560
+ /*#__PURE__*/
561
+ _asyncToGenerator(function* () {
562
+ var {
563
+ monitor,
564
+ initial,
565
+ opened
566
+ } = setup({
567
+ settings: {
568
+ probeFailureThreshold: 1,
569
+ retireGraceMs: 60000
570
+ }
571
+ });
572
+ initial.healthy = false;
573
+ yield monitor.check("default");
574
+ assert.equal(opened.length, 1);
575
+ yield monitor.stop();
576
+ assert.equal(initial.closed, 1);
577
+ opened[0].healthy = false;
578
+ yield monitor.check("default");
579
+ yield monitor.reconnect("default");
580
+ assert.equal(opened.length, 1);
581
+ }));
582
+ test("the interval probe runs when enabled",
583
+ /*#__PURE__*/
584
+ _asyncToGenerator(function* () {
585
+ var {
586
+ monitor,
587
+ initial,
588
+ opened
589
+ } = setup({
590
+ settings: {
591
+ healthIntervalMs: 5,
592
+ probeFailureThreshold: 1
593
+ }
594
+ });
595
+ initial.healthy = false;
596
+ yield sleep(40);
597
+ assert.ok(opened.length >= 1);
598
+ yield monitor.stop();
599
+ }));
600
+ test("a Database instance follows the swapped globals",
601
+ /*#__PURE__*/
602
+ _asyncToGenerator(function* () {
603
+ var path = require.resolve("./index");
604
+
605
+ delete require.cache[path];
606
+ var previousSpice = global.spice;
607
+ var {
608
+ monitor,
609
+ initial,
610
+ opened,
611
+ globals
612
+ } = setup({
613
+ settings: {
614
+ probeFailureThreshold: 1
615
+ }
616
+ });
617
+ global.spice = _objectSpread({}, globals, {
618
+ config: {
619
+ database: {
620
+ connections: {
621
+ default: _objectSpread({}, config)
622
+ }
623
+ }
624
+ }
625
+ }); // Point the shared module-level monitor at this test's monitor.
626
+
627
+ var key = Symbol.for("spice-couchbase.health-monitor");
628
+ var previousMonitor = globalThis[key];
629
+ globalThis[key] = monitor;
630
+
631
+ try {
632
+ // Keep a reference to the same globals object the monitor mutates.
633
+ global.spice.database = globals.database; // The constructor reads the bucket name off the boot-time collection.
634
+
635
+ globals.database.connections.default._scope = {
636
+ _bucket: {
637
+ _name: "saturn"
638
+ }
639
+ };
640
+
641
+ var Database = require("./index");
642
+
643
+ var db = new Database("default");
644
+ assert.equal(db.cluster, initial);
645
+ var oldCollection = db.collection;
646
+ assert.equal(oldCollection.cluster, initial);
647
+ initial.healthy = false;
648
+ yield monitor.check("default");
649
+ assert.equal(opened.length, 1);
650
+ assert.equal(db.cluster, opened[0]);
651
+ assert.equal(db.collection.cluster, opened[0]);
652
+ assert.notEqual(db.collection, oldCollection);
653
+ assert.deepEqual(Database.healthStatus().default.generation, 2);
654
+ } finally {
655
+ globalThis[key] = previousMonitor;
656
+ global.spice = previousSpice;
657
+ delete require.cache[path];
658
+ }
659
+ }));