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.
- package/build/index.count-query.test.js +163 -0
- package/build/index.js +48 -6
- package/package.json +1 -1
- package/src/index.js +53 -5
|
@@ -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
|
-
|
|
932
|
+
yield exec_scope.query(indexStatement);
|
|
898
933
|
}
|
|
899
|
-
} catch (e) {
|
|
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); //
|
|
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 >
|
|
935
|
-
_this20.optimize(
|
|
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
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
|
-
|
|
881
|
+
await exec_scope.query(indexStatement);
|
|
843
882
|
}
|
|
844
883
|
} catch (e) {
|
|
845
|
-
//
|
|
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
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
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
|
}
|