spice-couchbase 2.0.56 → 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 +56 -9
- package/package.json +3 -2
- package/src/index.count-query.test.js +129 -0
- package/src/index.js +62 -7
|
@@ -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())) {
|
|
@@ -704,10 +737,15 @@ module.exports = class Database {
|
|
|
704
737
|
|
|
705
738
|
if (query) {
|
|
706
739
|
conditions.push(query);
|
|
707
|
-
}
|
|
740
|
+
} // The conditions are folded into `where_clause` itself so that both queries below are built
|
|
741
|
+
// from one string. Previously this held the bare literal " WHERE " and only the data query
|
|
742
|
+
// appended `conditions.join(" AND ")`, which left the count query emitting a dangling
|
|
743
|
+
// `WHERE` — Couchbase answered "syntax error - at end of input" and every `do_count=true`
|
|
744
|
+
// request failed with a 400, on every resource.
|
|
708
745
|
|
|
709
|
-
|
|
710
|
-
|
|
746
|
+
|
|
747
|
+
var where_clause = conditions.length ? " WHERE ".concat(conditions.join(" AND ")) : "";
|
|
748
|
+
query = front_query + where_clause;
|
|
711
749
|
var count_query = "SELECT count(meta(".concat(bucket, ").id) as count FROM ").concat(bucket, " ").concat(join, " ").concat(where_clause); //console.log("Cound Query::", count_query);
|
|
712
750
|
|
|
713
751
|
var order_by_clause = order_by ? " ORDER BY ".concat(order_by) : "";
|
|
@@ -863,6 +901,8 @@ module.exports = class Database {
|
|
|
863
901
|
var _this19 = this;
|
|
864
902
|
|
|
865
903
|
return _asyncToGenerator(function* () {
|
|
904
|
+
optimizeState.inFlight += 1;
|
|
905
|
+
|
|
866
906
|
try {
|
|
867
907
|
var _explainResponse$rows;
|
|
868
908
|
|
|
@@ -889,9 +929,15 @@ module.exports = class Database {
|
|
|
889
929
|
}); // 4. Execute the index creation if we got a valid statement
|
|
890
930
|
|
|
891
931
|
if (indexStatement) {
|
|
892
|
-
|
|
932
|
+
yield exec_scope.query(indexStatement);
|
|
893
933
|
}
|
|
894
|
-
} 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;
|
|
895
941
|
}
|
|
896
942
|
})();
|
|
897
943
|
}
|
|
@@ -924,10 +970,11 @@ module.exports = class Database {
|
|
|
924
970
|
|
|
925
971
|
var endTime = process.hrtime.bigint(); // Calculate duration in microseconds
|
|
926
972
|
|
|
927
|
-
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.
|
|
928
975
|
|
|
929
|
-
if (durationInMilliseconds >
|
|
930
|
-
_this20.optimize(
|
|
976
|
+
if (durationInMilliseconds > SLOW_QUERY_MS && shouldOptimize(sanitizedQuery)) {
|
|
977
|
+
_this20.optimize(sanitizedQuery, exec_scope).catch(() => {});
|
|
931
978
|
}
|
|
932
979
|
|
|
933
980
|
return response;
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spice-couchbase",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.59",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node src/utility.test.js",
|
|
8
|
-
"test:query-consistency": "node src/index.query-consistency.test.js"
|
|
8
|
+
"test:query-consistency": "node src/index.query-consistency.test.js",
|
|
9
|
+
"test:count-query": "node src/index.count-query.test.js"
|
|
9
10
|
},
|
|
10
11
|
"author": "Chad Fraser <chad@sonover.com> (http://sonover.com)",
|
|
11
12
|
"license": "ISC",
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regression tests for the WHERE clause shared by the data and count queries in `search()`.
|
|
5
|
+
*
|
|
6
|
+
* The count query used to be built from a `where_clause` that held the bare literal `" WHERE "`,
|
|
7
|
+
* while the conditions were appended only to the data query. Every `do_count` request therefore
|
|
8
|
+
* emitted `... FROM \`bucket\` WHERE` and Couchbase answered
|
|
9
|
+
* "syntax error - at end of input" (code 3000) with a 400 — on every resource, for every caller.
|
|
10
|
+
*
|
|
11
|
+
* It survived the existing suite because nothing exercised the count statement.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const assert = require("assert");
|
|
15
|
+
const Database = require("./index");
|
|
16
|
+
|
|
17
|
+
let passed = 0;
|
|
18
|
+
let failed = 0;
|
|
19
|
+
|
|
20
|
+
/** Captures the two statements `search()` produces without touching a cluster. */
|
|
21
|
+
function createSearchDb() {
|
|
22
|
+
const statements = { data: null, count: null };
|
|
23
|
+
const db = Object.create(Database.prototype);
|
|
24
|
+
|
|
25
|
+
db.use_bucket_name = false;
|
|
26
|
+
db.bucket = "test";
|
|
27
|
+
db.scope_name = "scope";
|
|
28
|
+
db.collection_name = "platformtask";
|
|
29
|
+
|
|
30
|
+
db.getData = async (statement) => {
|
|
31
|
+
statements.data = statement;
|
|
32
|
+
return [];
|
|
33
|
+
};
|
|
34
|
+
db.getCount = async (statement) => {
|
|
35
|
+
statements.count = statement;
|
|
36
|
+
return 0;
|
|
37
|
+
};
|
|
38
|
+
db.optimize = () => {};
|
|
39
|
+
|
|
40
|
+
return { db, statements };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function test(name, fn) {
|
|
44
|
+
try {
|
|
45
|
+
await fn();
|
|
46
|
+
console.log(`✓ ${name}`);
|
|
47
|
+
passed += 1;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.log(`✗ ${name}`);
|
|
50
|
+
console.log(` Error: ${error.stack || error.message}`);
|
|
51
|
+
failed += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function run() {
|
|
56
|
+
console.log("\n--- count query tests ---");
|
|
57
|
+
|
|
58
|
+
await test("the count query carries the predicates, not a dangling WHERE", async () => {
|
|
59
|
+
const { db, statements } = createSearchDb();
|
|
60
|
+
|
|
61
|
+
await db.search("platformtask", "id", `status = "pending"`, 1, 0, null, "true");
|
|
62
|
+
|
|
63
|
+
assert.ok(
|
|
64
|
+
statements.count.includes(`status = "pending"`),
|
|
65
|
+
`count query lost its predicate: ${statements.count}`
|
|
66
|
+
);
|
|
67
|
+
assert.ok(
|
|
68
|
+
!/WHERE\s*$/.test(statements.count.trim()),
|
|
69
|
+
`count query ends in a dangling WHERE: ${statements.count}`
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await test("multiple conditions are joined with AND in the count query", async () => {
|
|
74
|
+
const { db, statements } = createSearchDb();
|
|
75
|
+
|
|
76
|
+
await db.search(
|
|
77
|
+
"platformtask",
|
|
78
|
+
"id",
|
|
79
|
+
`status = "pending" AND assignee_type = "user"`,
|
|
80
|
+
1,
|
|
81
|
+
0,
|
|
82
|
+
null,
|
|
83
|
+
"true"
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
assert.ok(statements.count.includes(`status = "pending"`));
|
|
87
|
+
assert.ok(statements.count.includes(`assignee_type = "user"`));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await test("an unfiltered count emits no WHERE at all", async () => {
|
|
91
|
+
const { db, statements } = createSearchDb();
|
|
92
|
+
|
|
93
|
+
await db.search("platformtask", "id", "", 1, 0, null, "true");
|
|
94
|
+
|
|
95
|
+
assert.ok(
|
|
96
|
+
!/WHERE/i.test(statements.count),
|
|
97
|
+
`unfiltered count should have no WHERE: ${statements.count}`
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await test("the data and count queries agree on the WHERE clause", async () => {
|
|
102
|
+
const { db, statements } = createSearchDb();
|
|
103
|
+
|
|
104
|
+
await db.search("platformtask", "id", `status = "blocked"`, 1, 0, null, "true");
|
|
105
|
+
|
|
106
|
+
const whereOf = (sql) => {
|
|
107
|
+
const m = sql.match(/WHERE\s+(.*?)(?:\s+ORDER BY|\s+LIMIT|\s+OFFSET|$)/is);
|
|
108
|
+
return m ? m[1].trim() : null;
|
|
109
|
+
};
|
|
110
|
+
assert.strictEqual(whereOf(statements.data), whereOf(statements.count));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await test("no count statement is produced when do_count is off", async () => {
|
|
114
|
+
const { db, statements } = createSearchDb();
|
|
115
|
+
|
|
116
|
+
await db.search("platformtask", "id", `status = "pending"`, 1, 0, null, "false");
|
|
117
|
+
|
|
118
|
+
assert.strictEqual(statements.count, null);
|
|
119
|
+
assert.ok(statements.data.includes(`status = "pending"`));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
console.log("\n========================================");
|
|
123
|
+
console.log(`Tests: ${passed + failed} | Passed: ${passed} | Failed: ${failed}`);
|
|
124
|
+
console.log("========================================");
|
|
125
|
+
|
|
126
|
+
if (failed > 0) process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
run();
|
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 ||
|
|
@@ -671,8 +709,15 @@ module.exports = class Database {
|
|
|
671
709
|
conditions.push(query);
|
|
672
710
|
}
|
|
673
711
|
|
|
674
|
-
|
|
675
|
-
|
|
712
|
+
// The conditions are folded into `where_clause` itself so that both queries below are built
|
|
713
|
+
// from one string. Previously this held the bare literal " WHERE " and only the data query
|
|
714
|
+
// appended `conditions.join(" AND ")`, which left the count query emitting a dangling
|
|
715
|
+
// `WHERE` — Couchbase answered "syntax error - at end of input" and every `do_count=true`
|
|
716
|
+
// request failed with a 400, on every resource.
|
|
717
|
+
const where_clause = conditions.length
|
|
718
|
+
? ` WHERE ${conditions.join(" AND ")}`
|
|
719
|
+
: "";
|
|
720
|
+
query = front_query + where_clause;
|
|
676
721
|
|
|
677
722
|
const count_query = `SELECT count(meta(${bucket}).id) as count FROM ${bucket} ${join} ${where_clause}`;
|
|
678
723
|
//console.log("Cound Query::", count_query);
|
|
@@ -809,6 +854,7 @@ module.exports = class Database {
|
|
|
809
854
|
}
|
|
810
855
|
|
|
811
856
|
async optimize(query, exec_scope) {
|
|
857
|
+
optimizeState.inFlight += 1;
|
|
812
858
|
try {
|
|
813
859
|
const { buildCoveringIndex } = require("./utility");
|
|
814
860
|
|
|
@@ -832,10 +878,15 @@ module.exports = class Database {
|
|
|
832
878
|
|
|
833
879
|
// 4. Execute the index creation if we got a valid statement
|
|
834
880
|
if (indexStatement) {
|
|
835
|
-
|
|
881
|
+
await exec_scope.query(indexStatement);
|
|
836
882
|
}
|
|
837
883
|
} catch (e) {
|
|
838
|
-
//
|
|
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;
|
|
839
890
|
}
|
|
840
891
|
}
|
|
841
892
|
|
|
@@ -866,9 +917,13 @@ module.exports = class Database {
|
|
|
866
917
|
let endTime = process.hrtime.bigint();
|
|
867
918
|
// Calculate duration in microseconds
|
|
868
919
|
let durationInMilliseconds = (endTime - startTime) / BigInt(1000000);
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
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(() => {});
|
|
872
927
|
}
|
|
873
928
|
return response;
|
|
874
929
|
}
|