kuzzle 2.60.0-beta.5 → 2.60.0-beta.7
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/dist/lib/api/controllers/adminController.js +2 -1
- package/dist/lib/api/funnel.js +12 -6
- package/dist/lib/cluster/command.d.ts +2 -1
- package/dist/lib/cluster/command.js +8 -2
- package/dist/lib/cluster/subscriber.js +15 -5
- package/dist/lib/kuzzle/event/KuzzleEventEmitter.js +16 -6
- package/dist/lib/kuzzle/kuzzle.js +18 -12
- package/dist/lib/service/storage/7/elasticsearch.d.ts +6 -0
- package/dist/lib/service/storage/7/elasticsearch.js +19 -2
- package/dist/lib/service/storage/8/elasticsearch.d.ts +6 -0
- package/dist/lib/service/storage/8/elasticsearch.js +14 -1
- package/dist/lib/util/dump-collection.js +3 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -162,7 +162,8 @@ class AdminController extends baseController_1.NativeController {
|
|
|
162
162
|
throw kerror.get("api", "process", "action_locked", "Kuzzle is already shutting down.");
|
|
163
163
|
}
|
|
164
164
|
this.shuttingDown = true;
|
|
165
|
-
|
|
165
|
+
// Not awaited, so that this request is answered first. It never rejects.
|
|
166
|
+
void global.kuzzle.shutdown();
|
|
166
167
|
return { acknowledge: true };
|
|
167
168
|
}
|
|
168
169
|
/**
|
package/dist/lib/api/funnel.js
CHANGED
|
@@ -1011,16 +1011,22 @@ function doAction(controller, request) {
|
|
|
1011
1011
|
const ret = Reflect.apply(Reflect.get(controller, action), controller, [
|
|
1012
1012
|
request,
|
|
1013
1013
|
]);
|
|
1014
|
-
|
|
1015
|
-
// used to reach `ret.then` and read `undefined`, which took this branch too.
|
|
1016
|
-
if (!ret ||
|
|
1017
|
-
typeof ret !== "object" ||
|
|
1018
|
-
!("then" in ret) ||
|
|
1019
|
-
typeof ret.then !== "function") {
|
|
1014
|
+
if (!isThenable(ret)) {
|
|
1020
1015
|
return kerror.reject("plugin", "controller", "invalid_action_response", request.input.controller, request.input.action);
|
|
1021
1016
|
}
|
|
1022
1017
|
return ret;
|
|
1023
1018
|
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Same duck-type check as before, spelled as a guard so that it narrows to
|
|
1021
|
+
* what `await` takes: a truthy non-object used to reach `ret.then` and read
|
|
1022
|
+
* `undefined`, which failed the check too.
|
|
1023
|
+
*/
|
|
1024
|
+
function isThenable(value) {
|
|
1025
|
+
return (typeof value === "object" &&
|
|
1026
|
+
value !== null &&
|
|
1027
|
+
"then" in value &&
|
|
1028
|
+
typeof value.then === "function");
|
|
1029
|
+
}
|
|
1024
1030
|
/**
|
|
1025
1031
|
* Very straightforward function to check only if the version satisfies
|
|
1026
1032
|
* the major version requirements
|
|
@@ -101,7 +101,8 @@ declare class ClusterCommand {
|
|
|
101
101
|
* Listens to incoming requests and answer to them.
|
|
102
102
|
* Do NOT await this method, it's meant to run indefinitely until the close()
|
|
103
103
|
* method is invoked.
|
|
104
|
-
*
|
|
104
|
+
* It rejects when a receive or a reply fails while it is running, which
|
|
105
|
+
* ends the loop: `init()` logs it.
|
|
105
106
|
*/
|
|
106
107
|
listen(): Promise<void>;
|
|
107
108
|
/**
|
|
@@ -55,6 +55,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
55
55
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
56
56
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
57
57
|
};
|
|
58
|
+
const node_util_1 = require("node:util");
|
|
58
59
|
const bluebird_1 = __importDefault(require("bluebird"));
|
|
59
60
|
const long_1 = __importDefault(require("long"));
|
|
60
61
|
const protobuf = __importStar(require("protobufjs"));
|
|
@@ -142,7 +143,11 @@ class ClusterCommand {
|
|
|
142
143
|
async init() {
|
|
143
144
|
await this.loadProtobuf();
|
|
144
145
|
await this.serve();
|
|
145
|
-
|
|
146
|
+
// Never awaited (it loops until `dispose()`), and it rethrows a failed
|
|
147
|
+
// receive or reply: logged rather than left unhandled.
|
|
148
|
+
this.listen().catch((error) => {
|
|
149
|
+
this.logger.error(`[CLUSTER] Command layer stopped listening: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
|
|
150
|
+
});
|
|
146
151
|
}
|
|
147
152
|
async serve() {
|
|
148
153
|
this.server = new zeromq_1.Reply();
|
|
@@ -152,7 +157,8 @@ class ClusterCommand {
|
|
|
152
157
|
* Listens to incoming requests and answer to them.
|
|
153
158
|
* Do NOT await this method, it's meant to run indefinitely until the close()
|
|
154
159
|
* method is invoked.
|
|
155
|
-
*
|
|
160
|
+
* It rejects when a receive or a reply fails while it is running, which
|
|
161
|
+
* ends the loop: `init()` logs it.
|
|
156
162
|
*/
|
|
157
163
|
async listen() {
|
|
158
164
|
this.state = stateEnum.RUNNING;
|
|
@@ -184,7 +184,12 @@ class ClusterSubscriber {
|
|
|
184
184
|
this.socket.connect(this.remoteNodeAddress);
|
|
185
185
|
this.socket.subscribe();
|
|
186
186
|
this.heartbeatTimer = setInterval(() => this.checkHeartbeat(), this.heartbeatDelay);
|
|
187
|
-
|
|
187
|
+
// Never awaited (it loops until the remote node is evicted), and it can
|
|
188
|
+
// still reject: a failed read evicts the remote node, which removes its
|
|
189
|
+
// ID card from Redis.
|
|
190
|
+
this.listen().catch((error) => {
|
|
191
|
+
this.logger.error(`[CLUSTER] Stopped listening to node ${this.remoteNodeId}: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
|
|
192
|
+
});
|
|
188
193
|
}
|
|
189
194
|
/**
|
|
190
195
|
* Starts subscribing to other nodes
|
|
@@ -353,7 +358,8 @@ class ClusterSubscriber {
|
|
|
353
358
|
await Reflect.apply(this.handlers[topic], this, [message]);
|
|
354
359
|
}
|
|
355
360
|
catch (e) {
|
|
356
|
-
|
|
361
|
+
// Cannot reject: it logs, broadcasts, and shuts down, which never rejects
|
|
362
|
+
void this.localNode.evictSelf(`Unable to process sync message (topic: ${topic}, message: ${JSON.stringify(message)}`, e instanceof Error ? e : new Error((0, node_util_1.inspect)(e)));
|
|
357
363
|
}
|
|
358
364
|
}
|
|
359
365
|
/**
|
|
@@ -392,7 +398,7 @@ class ClusterSubscriber {
|
|
|
392
398
|
if (message.nodeId === this.localNode.nodeId) {
|
|
393
399
|
this.logger.error(`[CLUSTER] Node evicted by ${message.evictor}. Reason: ${message.reason}`);
|
|
394
400
|
// A failure, like `evictSelf`'s: exit non-zero (#2785).
|
|
395
|
-
global.kuzzle.shutdown(1);
|
|
401
|
+
void global.kuzzle.shutdown(1);
|
|
396
402
|
return;
|
|
397
403
|
}
|
|
398
404
|
await this.localNode.evictNode(message.nodeId, {
|
|
@@ -563,7 +569,11 @@ class ClusterSubscriber {
|
|
|
563
569
|
*/
|
|
564
570
|
handleDumpRequest(message) {
|
|
565
571
|
debug("Dump generation request received from node %s", this.remoteNodeId);
|
|
566
|
-
|
|
572
|
+
// Nothing awaits it: a failure (another dump running, an unwritable dump
|
|
573
|
+
// path) is logged rather than left unhandled, as on SIGTRAP.
|
|
574
|
+
global.kuzzle.dump(message.suffix).catch((error) => {
|
|
575
|
+
this.logger.error(`[CLUSTER] Unable to dump on request from node ${this.remoteNodeId}: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
|
|
576
|
+
});
|
|
567
577
|
}
|
|
568
578
|
/**
|
|
569
579
|
* Handles cluster-wide shutdown
|
|
@@ -571,7 +581,7 @@ class ClusterSubscriber {
|
|
|
571
581
|
handleShutdown() {
|
|
572
582
|
debug("Cluster-wide shutdown request received from node %s", this.remoteNodeId);
|
|
573
583
|
this.logger.error(`[CLUSTER] Cluster wide shutdown from ${this.remoteNodeId}`);
|
|
574
|
-
global.kuzzle.shutdown();
|
|
584
|
+
void global.kuzzle.shutdown();
|
|
575
585
|
}
|
|
576
586
|
/**
|
|
577
587
|
* Handles changes on document validators
|
|
@@ -193,7 +193,8 @@ class KuzzleEventEmitter extends eventemitter3_1.default {
|
|
|
193
193
|
targetEvent: eventName,
|
|
194
194
|
};
|
|
195
195
|
if (funcs.length === 0) {
|
|
196
|
-
|
|
196
|
+
// Never rejects: it settles `promback` instead
|
|
197
|
+
void pipeCallback.call(callbackContext, null, ...payload);
|
|
197
198
|
}
|
|
198
199
|
else {
|
|
199
200
|
this.pipeRunner.run(funcs, payload, pipeCallback, callbackContext);
|
|
@@ -326,12 +327,21 @@ async function pipeCallback(error, ...updated) {
|
|
|
326
327
|
this.promback.reject(error);
|
|
327
328
|
return;
|
|
328
329
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
// Nothing awaits this function: a core pipe or a listener that throws
|
|
331
|
+
// settles the pipe, rather than leaving it pending forever and the error
|
|
332
|
+
// unhandled.
|
|
333
|
+
try {
|
|
334
|
+
const corePipes = this.corePipes.get(this.targetEvent);
|
|
335
|
+
if (corePipes) {
|
|
336
|
+
await bluebird_1.default.map(corePipes, (fn) => fn(...updated));
|
|
337
|
+
}
|
|
338
|
+
for (const element of this.events) {
|
|
339
|
+
this.superEmit(element, ...updated);
|
|
340
|
+
}
|
|
332
341
|
}
|
|
333
|
-
|
|
334
|
-
this.
|
|
342
|
+
catch (e) {
|
|
343
|
+
this.promback.reject(e);
|
|
344
|
+
return;
|
|
335
345
|
}
|
|
336
346
|
this.promback.resolve(updated[0]);
|
|
337
347
|
/* eslint-enable no-invalid-this */
|
|
@@ -239,13 +239,9 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
239
239
|
// A failed start cannot be retried (a second Kuzzle instance cannot be
|
|
240
240
|
// built), and what it did start keeps the process alive: in production,
|
|
241
241
|
// a node with no listener and an ID card still renewed (#3020). Not
|
|
242
|
-
// awaited, so the caller still gets the error before the process exits
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
? shutdownError.message
|
|
246
|
-
: (0, node_util_1.inspect)(shutdownError)}`);
|
|
247
|
-
process.exit(1);
|
|
248
|
-
});
|
|
242
|
+
// awaited, so the caller still gets the error before the process exits;
|
|
243
|
+
// `shutdown` never rejects, and exits even when it fails.
|
|
244
|
+
void this.shutdown(1);
|
|
249
245
|
throw error;
|
|
250
246
|
}
|
|
251
247
|
}
|
|
@@ -322,7 +318,17 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
322
318
|
process.exit(this.shutdownExitCode);
|
|
323
319
|
}, FAILURE_SHUTDOWN_TIMEOUT));
|
|
324
320
|
}
|
|
325
|
-
|
|
321
|
+
// Never rejects: every caller but one fires it and forgets it (signal
|
|
322
|
+
// handlers, cluster eviction, admin:shutdown), so a failure here used to
|
|
323
|
+
// be an unhandled rejection, and the process stayed up with its network
|
|
324
|
+
// layer already closed. It exits non-zero instead.
|
|
325
|
+
this.shutdownPromise ?? (this.shutdownPromise = this.runShutdown().catch((error) => {
|
|
326
|
+
this.log.error(`[shutdown] Unable to shut down gracefully: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
|
|
327
|
+
if (this.shutdownWatchdog !== null) {
|
|
328
|
+
clearTimeout(this.shutdownWatchdog);
|
|
329
|
+
}
|
|
330
|
+
process.exit(Math.max(this.shutdownExitCode, 1));
|
|
331
|
+
}));
|
|
326
332
|
return this.shutdownPromise;
|
|
327
333
|
}
|
|
328
334
|
async runShutdown() {
|
|
@@ -646,7 +652,7 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
646
652
|
registerSignalHandlers() {
|
|
647
653
|
process.removeAllListeners("unhandledRejection");
|
|
648
654
|
process.on("exit", () => {
|
|
649
|
-
this.shutdown();
|
|
655
|
+
void this.shutdown();
|
|
650
656
|
});
|
|
651
657
|
process.on("unhandledRejection", (reason, promise) => {
|
|
652
658
|
if (reason !== undefined) {
|
|
@@ -672,14 +678,14 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
672
678
|
process.removeAllListeners("uncaughtException");
|
|
673
679
|
process.on("uncaughtException", (err) => {
|
|
674
680
|
this.log.error(`ERROR: uncaughtException: ${err.message}\n${err.stack}`);
|
|
675
|
-
this.dumpAndExit("uncaught-exception");
|
|
681
|
+
void this.dumpAndExit("uncaught-exception");
|
|
676
682
|
});
|
|
677
683
|
// abnormal termination signals => generate a core dump
|
|
678
684
|
for (const signal of ["SIGQUIT", "SIGABRT"]) {
|
|
679
685
|
process.removeAllListeners(signal);
|
|
680
686
|
process.on(signal, () => {
|
|
681
687
|
this.log.error(`ERROR: Caught signal: ${signal}`);
|
|
682
|
-
this.dumpAndExit("signal-".concat(signal.toLowerCase()));
|
|
688
|
+
void this.dumpAndExit("signal-".concat(signal.toLowerCase()));
|
|
683
689
|
});
|
|
684
690
|
}
|
|
685
691
|
// signal SIGTRAP is used to generate a kuzzle dump without stopping it
|
|
@@ -697,7 +703,7 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
697
703
|
process.removeAllListeners(signal);
|
|
698
704
|
process.on(signal, () => {
|
|
699
705
|
this.log.info(`Caught signal ${signal} => gracefully exit`);
|
|
700
|
-
this.shutdown();
|
|
706
|
+
void this.shutdown();
|
|
701
707
|
});
|
|
702
708
|
}
|
|
703
709
|
}
|
|
@@ -468,6 +468,12 @@ export declare class ES7 {
|
|
|
468
468
|
* collection waits for its shards (`wait_for_active_shards`); a node that
|
|
469
469
|
* finds it already there calls this instead. It answers at once when the
|
|
470
470
|
* shards are already active.
|
|
471
|
+
*
|
|
472
|
+
* A `yellow` status alone does not wait: Elasticsearch reports a new index
|
|
473
|
+
* whose primaries are still initializing as `yellow` (#3074). Asking for as
|
|
474
|
+
* many active shard copies as the index has primaries does, and, unlike
|
|
475
|
+
* `wait_for_no_initializing_shards`, does not also wait for replicas that
|
|
476
|
+
* are recovering, which a collection with data may take longer to do.
|
|
471
477
|
*/
|
|
472
478
|
waitForCollectionShards(index: string, collection: string, timeout?: string): Promise<void>;
|
|
473
479
|
/**
|
|
@@ -1081,7 +1081,11 @@ class ES7 {
|
|
|
1081
1081
|
});
|
|
1082
1082
|
}
|
|
1083
1083
|
finally {
|
|
1084
|
-
|
|
1084
|
+
// Not awaited, but not left unhandled either: a scroll that cannot be
|
|
1085
|
+
// cleared expires on its own.
|
|
1086
|
+
this.clearScroll(scrollId).catch((error) => {
|
|
1087
|
+
this.logger.warn(`Unable to clear scroll ${scrollId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1088
|
+
});
|
|
1085
1089
|
}
|
|
1086
1090
|
return results;
|
|
1087
1091
|
}
|
|
@@ -1214,12 +1218,25 @@ class ES7 {
|
|
|
1214
1218
|
* collection waits for its shards (`wait_for_active_shards`); a node that
|
|
1215
1219
|
* finds it already there calls this instead. It answers at once when the
|
|
1216
1220
|
* shards are already active.
|
|
1221
|
+
*
|
|
1222
|
+
* A `yellow` status alone does not wait: Elasticsearch reports a new index
|
|
1223
|
+
* whose primaries are still initializing as `yellow` (#3074). Asking for as
|
|
1224
|
+
* many active shard copies as the index has primaries does, and, unlike
|
|
1225
|
+
* `wait_for_no_initializing_shards`, does not also wait for replicas that
|
|
1226
|
+
* are recovering, which a collection with data may take longer to do.
|
|
1217
1227
|
*/
|
|
1218
1228
|
async waitForCollectionShards(index, collection, timeout = COLLECTION_SHARDS_TIMEOUT) {
|
|
1229
|
+
const alias = this._getAlias(index, collection);
|
|
1219
1230
|
try {
|
|
1231
|
+
const { body } = await this._client.indices.getSettings({
|
|
1232
|
+
index: alias,
|
|
1233
|
+
name: "index.number_of_shards",
|
|
1234
|
+
});
|
|
1235
|
+
const primaries = Object.values(body)[0]?.settings?.index?.number_of_shards;
|
|
1220
1236
|
await this._client.cluster.health({
|
|
1221
|
-
index:
|
|
1237
|
+
index: alias,
|
|
1222
1238
|
timeout,
|
|
1239
|
+
wait_for_active_shards: String(primaries ?? 1),
|
|
1223
1240
|
wait_for_status: "yellow",
|
|
1224
1241
|
});
|
|
1225
1242
|
}
|
|
@@ -459,6 +459,12 @@ export declare class ES8 {
|
|
|
459
459
|
* collection waits for its shards (`wait_for_active_shards`); a node that
|
|
460
460
|
* finds it already there calls this instead. It answers at once when the
|
|
461
461
|
* shards are already active.
|
|
462
|
+
*
|
|
463
|
+
* A `yellow` status alone does not wait: Elasticsearch reports a new index
|
|
464
|
+
* whose primaries are still initializing as `yellow` (#3074). Asking for as
|
|
465
|
+
* many active shard copies as the index has primaries does, and, unlike
|
|
466
|
+
* `wait_for_no_initializing_shards`, does not also wait for replicas that
|
|
467
|
+
* are recovering, which a collection with data may take longer to do.
|
|
462
468
|
*/
|
|
463
469
|
waitForCollectionShards(index: string, collection: string, timeout?: string): Promise<void>;
|
|
464
470
|
/**
|
|
@@ -1236,12 +1236,25 @@ class ES8 {
|
|
|
1236
1236
|
* collection waits for its shards (`wait_for_active_shards`); a node that
|
|
1237
1237
|
* finds it already there calls this instead. It answers at once when the
|
|
1238
1238
|
* shards are already active.
|
|
1239
|
+
*
|
|
1240
|
+
* A `yellow` status alone does not wait: Elasticsearch reports a new index
|
|
1241
|
+
* whose primaries are still initializing as `yellow` (#3074). Asking for as
|
|
1242
|
+
* many active shard copies as the index has primaries does, and, unlike
|
|
1243
|
+
* `wait_for_no_initializing_shards`, does not also wait for replicas that
|
|
1244
|
+
* are recovering, which a collection with data may take longer to do.
|
|
1239
1245
|
*/
|
|
1240
1246
|
async waitForCollectionShards(index, collection, timeout = COLLECTION_SHARDS_TIMEOUT) {
|
|
1247
|
+
const alias = this._getAlias(index, collection);
|
|
1241
1248
|
try {
|
|
1249
|
+
const body = await this._client.indices.getSettings({
|
|
1250
|
+
index: alias,
|
|
1251
|
+
name: "index.number_of_shards",
|
|
1252
|
+
});
|
|
1253
|
+
const primaries = Object.values(body)[0]?.settings?.index?.number_of_shards;
|
|
1242
1254
|
await this._client.cluster.health({
|
|
1243
|
-
index:
|
|
1255
|
+
index: alias,
|
|
1244
1256
|
timeout,
|
|
1257
|
+
wait_for_active_shards: Number(primaries ?? 1),
|
|
1245
1258
|
wait_for_status: "yellow",
|
|
1246
1259
|
});
|
|
1247
1260
|
}
|
|
@@ -299,11 +299,12 @@ function dumpCollectionDocuments(index, collection, query = {}, format = "jsonl"
|
|
|
299
299
|
switch (format.toLowerCase()) {
|
|
300
300
|
case "csv":
|
|
301
301
|
dumper = new CSVDumper(index, collection, query, writableStream, options, fields);
|
|
302
|
-
|
|
302
|
+
// Errors are written to the stream, which is what the client reads
|
|
303
|
+
void dumper.dump();
|
|
303
304
|
break;
|
|
304
305
|
default:
|
|
305
306
|
dumper = new JSONLDumper(index, collection, query, writableStream, options);
|
|
306
|
-
dumper.dump();
|
|
307
|
+
void dumper.dump();
|
|
307
308
|
break;
|
|
308
309
|
}
|
|
309
310
|
return new types_1.HttpStream(writableStream);
|
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kuzzle",
|
|
3
3
|
"author": "The Kuzzle Team <support@kuzzle.io>",
|
|
4
|
-
"version": "2.60.0-beta.
|
|
4
|
+
"version": "2.60.0-beta.7",
|
|
5
5
|
"description": "Kuzzle is an open-source solution that handles all the data management through a secured API, with a large choice of protocols.",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "rm -Rf ./dist && tsc && node -r ts-node/register/transpile-only ./bin/copy-binaries.ts",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kuzzle",
|
|
3
3
|
"author": "The Kuzzle Team <support@kuzzle.io>",
|
|
4
|
-
"version": "2.60.0-beta.
|
|
4
|
+
"version": "2.60.0-beta.7",
|
|
5
5
|
"description": "Kuzzle is an open-source solution that handles all the data management through a secured API, with a large choice of protocols.",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "rm -Rf ./dist && tsc && node -r ts-node/register/transpile-only ./bin/copy-binaries.ts",
|