spice-couchbase 2.0.57 → 2.0.59

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,163 @@
1
+ "use strict";
2
+ /**
3
+ * Regression tests for the WHERE clause shared by the data and count queries in `search()`.
4
+ *
5
+ * The count query used to be built from a `where_clause` that held the bare literal `" WHERE "`,
6
+ * while the conditions were appended only to the data query. Every `do_count` request therefore
7
+ * emitted `... FROM \`bucket\` WHERE` and Couchbase answered
8
+ * "syntax error - at end of input" (code 3000) with a 400 — on every resource, for every caller.
9
+ *
10
+ * It survived the existing suite because nothing exercised the count statement.
11
+ */
12
+
13
+ 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); } }
14
+
15
+ 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); }); }; }
16
+
17
+ var assert = require("assert");
18
+
19
+ var Database = require("./index");
20
+
21
+ var passed = 0;
22
+ var failed = 0;
23
+ /** Captures the two statements `search()` produces without touching a cluster. */
24
+
25
+ function createSearchDb() {
26
+ var statements = {
27
+ data: null,
28
+ count: null
29
+ };
30
+ var db = Object.create(Database.prototype);
31
+ db.use_bucket_name = false;
32
+ db.bucket = "test";
33
+ db.scope_name = "scope";
34
+ db.collection_name = "platformtask";
35
+
36
+ db.getData =
37
+ /*#__PURE__*/
38
+ function () {
39
+ var _ref = _asyncToGenerator(function* (statement) {
40
+ statements.data = statement;
41
+ return [];
42
+ });
43
+
44
+ return function (_x) {
45
+ return _ref.apply(this, arguments);
46
+ };
47
+ }();
48
+
49
+ db.getCount =
50
+ /*#__PURE__*/
51
+ function () {
52
+ var _ref2 = _asyncToGenerator(function* (statement) {
53
+ statements.count = statement;
54
+ return 0;
55
+ });
56
+
57
+ return function (_x2) {
58
+ return _ref2.apply(this, arguments);
59
+ };
60
+ }();
61
+
62
+ db.optimize = () => {};
63
+
64
+ return {
65
+ db,
66
+ statements
67
+ };
68
+ }
69
+
70
+ function test(_x3, _x4) {
71
+ return _test.apply(this, arguments);
72
+ }
73
+
74
+ function _test() {
75
+ _test = _asyncToGenerator(function* (name, fn) {
76
+ try {
77
+ yield fn();
78
+ console.log("\u2713 ".concat(name));
79
+ passed += 1;
80
+ } catch (error) {
81
+ console.log("\u2717 ".concat(name));
82
+ console.log(" Error: ".concat(error.stack || error.message));
83
+ failed += 1;
84
+ }
85
+ });
86
+ return _test.apply(this, arguments);
87
+ }
88
+
89
+ function run() {
90
+ return _run.apply(this, arguments);
91
+ }
92
+
93
+ function _run() {
94
+ _run = _asyncToGenerator(function* () {
95
+ console.log("\n--- count query tests ---");
96
+ yield test("the count query carries the predicates, not a dangling WHERE",
97
+ /*#__PURE__*/
98
+ _asyncToGenerator(function* () {
99
+ var {
100
+ db,
101
+ statements
102
+ } = createSearchDb();
103
+ yield db.search("platformtask", "id", "status = \"pending\"", 1, 0, null, "true");
104
+ assert.ok(statements.count.includes("status = \"pending\""), "count query lost its predicate: ".concat(statements.count));
105
+ assert.ok(!/WHERE\s*$/.test(statements.count.trim()), "count query ends in a dangling WHERE: ".concat(statements.count));
106
+ }));
107
+ yield test("multiple conditions are joined with AND in the count query",
108
+ /*#__PURE__*/
109
+ _asyncToGenerator(function* () {
110
+ var {
111
+ db,
112
+ statements
113
+ } = createSearchDb();
114
+ yield db.search("platformtask", "id", "status = \"pending\" AND assignee_type = \"user\"", 1, 0, null, "true");
115
+ assert.ok(statements.count.includes("status = \"pending\""));
116
+ assert.ok(statements.count.includes("assignee_type = \"user\""));
117
+ }));
118
+ yield test("an unfiltered count emits no WHERE at all",
119
+ /*#__PURE__*/
120
+ _asyncToGenerator(function* () {
121
+ var {
122
+ db,
123
+ statements
124
+ } = createSearchDb();
125
+ yield db.search("platformtask", "id", "", 1, 0, null, "true");
126
+ assert.ok(!/WHERE/i.test(statements.count), "unfiltered count should have no WHERE: ".concat(statements.count));
127
+ }));
128
+ yield test("the data and count queries agree on the WHERE clause",
129
+ /*#__PURE__*/
130
+ _asyncToGenerator(function* () {
131
+ var {
132
+ db,
133
+ statements
134
+ } = createSearchDb();
135
+ yield db.search("platformtask", "id", "status = \"blocked\"", 1, 0, null, "true");
136
+
137
+ var whereOf = sql => {
138
+ var m = sql.match(/WHERE[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+([\s\S]*?)(?:[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+ORDER BY|[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+LIMIT|[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+OFFSET|$)/i);
139
+ return m ? m[1].trim() : null;
140
+ };
141
+
142
+ assert.strictEqual(whereOf(statements.data), whereOf(statements.count));
143
+ }));
144
+ yield test("no count statement is produced when do_count is off",
145
+ /*#__PURE__*/
146
+ _asyncToGenerator(function* () {
147
+ var {
148
+ db,
149
+ statements
150
+ } = createSearchDb();
151
+ yield db.search("platformtask", "id", "status = \"pending\"", 1, 0, null, "false");
152
+ assert.strictEqual(statements.count, null);
153
+ assert.ok(statements.data.includes("status = \"pending\""));
154
+ }));
155
+ console.log("\n========================================");
156
+ console.log("Tests: ".concat(passed + failed, " | Passed: ").concat(passed, " | Failed: ").concat(failed));
157
+ console.log("========================================");
158
+ if (failed > 0) process.exit(1);
159
+ });
160
+ return _run.apply(this, arguments);
161
+ }
162
+
163
+ run();
package/build/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
 
3
+ var _process$env$SPICE_CO;
4
+
3
5
  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
6
 
5
7
  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; }
@@ -33,7 +35,38 @@ if (!Promise.allSettled) {
33
35
  } // Couchbase lookupIn has a limit of 16 operations per call
34
36
 
35
37
 
36
- var LOOKUP_IN_MAX_SPECS = 16;
38
+ var LOOKUP_IN_MAX_SPECS = 16; // Auto-optimisation guard rails. `query()` fires `optimize()` (an EXPLAIN plus
39
+ // a possible CREATE INDEX) for any statement slower than SLOW_QUERY_MS. When
40
+ // the cluster is degraded every statement is slow, so without limits each
41
+ // query spawned two more heavy ones and deepened the outage. These caps make it
42
+ // a bounded, once-per-statement background task that backs off on failure.
43
+
44
+ var SLOW_QUERY_MS = Number(process.env.SPICE_COUCHBASE_SLOW_QUERY_MS || 1000);
45
+ var AUTO_OPTIMIZE_ENABLED = !["0", "false", "no", "off"].includes(String((_process$env$SPICE_CO = process.env.SPICE_COUCHBASE_AUTO_OPTIMIZE) !== null && _process$env$SPICE_CO !== void 0 ? _process$env$SPICE_CO : "true").trim().toLowerCase());
46
+ var OPTIMIZE_MAX_INFLIGHT = Number(process.env.SPICE_COUCHBASE_OPTIMIZE_MAX_INFLIGHT || 1);
47
+ var OPTIMIZE_FAILURE_COOLDOWN_MS = Number(process.env.SPICE_COUCHBASE_OPTIMIZE_COOLDOWN_MS || 5 * 60 * 1000); // Statements already optimised in this process. Bounded so it cannot grow
48
+ // without limit on a workload with many distinct literals.
49
+
50
+ var OPTIMIZE_SEEN_LIMIT = 1000;
51
+ var optimizeState = {
52
+ inFlight: 0,
53
+ seen: new Set(),
54
+ cooldownUntil: 0
55
+ };
56
+
57
+ function shouldOptimize(query) {
58
+ if (!AUTO_OPTIMIZE_ENABLED) return false;
59
+ if (optimizeState.inFlight >= OPTIMIZE_MAX_INFLIGHT) return false;
60
+ if (Date.now() < optimizeState.cooldownUntil) return false;
61
+ if (optimizeState.seen.has(query)) return false;
62
+
63
+ if (optimizeState.seen.size >= OPTIMIZE_SEEN_LIMIT) {
64
+ optimizeState.seen.delete(optimizeState.seen.values().next().value);
65
+ }
66
+
67
+ optimizeState.seen.add(query);
68
+ return true;
69
+ }
37
70
 
38
71
  function normalizeScanConsistency(value) {
39
72
  if (value === true || value === QueryScanConsistency.RequestPlus || typeof value === "string" && ["true", "request_plus"].includes(value.toLowerCase())) {
@@ -868,6 +901,8 @@ module.exports = class Database {
868
901
  var _this19 = this;
869
902
 
870
903
  return _asyncToGenerator(function* () {
904
+ optimizeState.inFlight += 1;
905
+
871
906
  try {
872
907
  var _explainResponse$rows;
873
908
 
@@ -894,9 +929,15 @@ module.exports = class Database {
894
929
  }); // 4. Execute the index creation if we got a valid statement
895
930
 
896
931
  if (indexStatement) {
897
- var response = yield exec_scope.query(indexStatement);
932
+ yield exec_scope.query(indexStatement);
898
933
  }
899
- } catch (e) {// Silently handle errors (index may already exist, etc.)
934
+ } catch (e) {
935
+ // A failure here (timeout, index already exists, cluster busy) almost
936
+ // always means the cluster is struggling. Stop adding EXPLAIN and
937
+ // CREATE INDEX load for a while rather than piling on.
938
+ optimizeState.cooldownUntil = Date.now() + OPTIMIZE_FAILURE_COOLDOWN_MS;
939
+ } finally {
940
+ optimizeState.inFlight -= 1;
900
941
  }
901
942
  })();
902
943
  }
@@ -929,10 +970,11 @@ module.exports = class Database {
929
970
 
930
971
  var endTime = process.hrtime.bigint(); // Calculate duration in microseconds
931
972
 
932
- var durationInMilliseconds = (endTime - startTime) / BigInt(1000000); // optimize if response time is greater than 1 second
973
+ var durationInMilliseconds = (endTime - startTime) / BigInt(1000000); // Optimise slow statements in the background, subject to the guard rails
974
+ // declared at the top of this file. Never awaited, never allowed to reject.
933
975
 
934
- if (durationInMilliseconds > 1000) {
935
- _this20.optimize(query, exec_scope);
976
+ if (durationInMilliseconds > SLOW_QUERY_MS && shouldOptimize(sanitizedQuery)) {
977
+ _this20.optimize(sanitizedQuery, exec_scope).catch(() => {});
936
978
  }
937
979
 
938
980
  return response;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-couchbase",
3
- "version": "2.0.57",
3
+ "version": "2.0.59",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -25,6 +25,44 @@ if (!Promise.allSettled) {
25
25
  // Couchbase lookupIn has a limit of 16 operations per call
26
26
  const LOOKUP_IN_MAX_SPECS = 16;
27
27
 
28
+ // Auto-optimisation guard rails. `query()` fires `optimize()` (an EXPLAIN plus
29
+ // a possible CREATE INDEX) for any statement slower than SLOW_QUERY_MS. When
30
+ // the cluster is degraded every statement is slow, so without limits each
31
+ // query spawned two more heavy ones and deepened the outage. These caps make it
32
+ // a bounded, once-per-statement background task that backs off on failure.
33
+ const SLOW_QUERY_MS = Number(process.env.SPICE_COUCHBASE_SLOW_QUERY_MS || 1000);
34
+ const AUTO_OPTIMIZE_ENABLED = !["0", "false", "no", "off"].includes(
35
+ String(process.env.SPICE_COUCHBASE_AUTO_OPTIMIZE ?? "true")
36
+ .trim()
37
+ .toLowerCase(),
38
+ );
39
+ const OPTIMIZE_MAX_INFLIGHT = Number(
40
+ process.env.SPICE_COUCHBASE_OPTIMIZE_MAX_INFLIGHT || 1,
41
+ );
42
+ const OPTIMIZE_FAILURE_COOLDOWN_MS = Number(
43
+ process.env.SPICE_COUCHBASE_OPTIMIZE_COOLDOWN_MS || 5 * 60 * 1000,
44
+ );
45
+ // Statements already optimised in this process. Bounded so it cannot grow
46
+ // without limit on a workload with many distinct literals.
47
+ const OPTIMIZE_SEEN_LIMIT = 1000;
48
+ const optimizeState = {
49
+ inFlight: 0,
50
+ seen: new Set(),
51
+ cooldownUntil: 0,
52
+ };
53
+
54
+ function shouldOptimize(query) {
55
+ if (!AUTO_OPTIMIZE_ENABLED) return false;
56
+ if (optimizeState.inFlight >= OPTIMIZE_MAX_INFLIGHT) return false;
57
+ if (Date.now() < optimizeState.cooldownUntil) return false;
58
+ if (optimizeState.seen.has(query)) return false;
59
+ if (optimizeState.seen.size >= OPTIMIZE_SEEN_LIMIT) {
60
+ optimizeState.seen.delete(optimizeState.seen.values().next().value);
61
+ }
62
+ optimizeState.seen.add(query);
63
+ return true;
64
+ }
65
+
28
66
  function normalizeScanConsistency(value) {
29
67
  if (
30
68
  value === true ||
@@ -816,6 +854,7 @@ module.exports = class Database {
816
854
  }
817
855
 
818
856
  async optimize(query, exec_scope) {
857
+ optimizeState.inFlight += 1;
819
858
  try {
820
859
  const { buildCoveringIndex } = require("./utility");
821
860
 
@@ -839,10 +878,15 @@ module.exports = class Database {
839
878
 
840
879
  // 4. Execute the index creation if we got a valid statement
841
880
  if (indexStatement) {
842
- const response = await exec_scope.query(indexStatement);
881
+ await exec_scope.query(indexStatement);
843
882
  }
844
883
  } catch (e) {
845
- // Silently handle errors (index may already exist, etc.)
884
+ // A failure here (timeout, index already exists, cluster busy) almost
885
+ // always means the cluster is struggling. Stop adding EXPLAIN and
886
+ // CREATE INDEX load for a while rather than piling on.
887
+ optimizeState.cooldownUntil = Date.now() + OPTIMIZE_FAILURE_COOLDOWN_MS;
888
+ } finally {
889
+ optimizeState.inFlight -= 1;
846
890
  }
847
891
  }
848
892
 
@@ -873,9 +917,13 @@ module.exports = class Database {
873
917
  let endTime = process.hrtime.bigint();
874
918
  // Calculate duration in microseconds
875
919
  let durationInMilliseconds = (endTime - startTime) / BigInt(1000000);
876
- // optimize if response time is greater than 1 second
877
- if (durationInMilliseconds > 1000) {
878
- this.optimize(query, exec_scope);
920
+ // Optimise slow statements in the background, subject to the guard rails
921
+ // declared at the top of this file. Never awaited, never allowed to reject.
922
+ if (
923
+ durationInMilliseconds > SLOW_QUERY_MS &&
924
+ shouldOptimize(sanitizedQuery)
925
+ ) {
926
+ this.optimize(sanitizedQuery, exec_scope).catch(() => {});
879
927
  }
880
928
  return response;
881
929
  }