kuzzle 2.60.0-beta.6 → 2.60.0

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.
@@ -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
- global.kuzzle.shutdown();
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
  /**
@@ -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
- // Same duck-type check as before, spelled so it narrows: a truthy non-object
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
- * This method can (and should) never return a rejected promise.
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
- this.listen();
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
- * This method can (and should) never return a rejected promise.
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
- this.listen();
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
- 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)));
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
- global.kuzzle.dump(message.suffix);
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
- pipeCallback.call(callbackContext, null, ...payload);
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
- const corePipes = this.corePipes.get(this.targetEvent);
330
- if (corePipes) {
331
- await bluebird_1.default.map(corePipes, (fn) => fn(...updated));
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
- for (const element of this.events) {
334
- this.superEmit(element, ...updated);
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
- this.shutdown(1).catch((shutdownError) => {
244
- this.log.error(`Unable to shut down after a failed start: ${shutdownError instanceof Error
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
- this.shutdownPromise ?? (this.shutdownPromise = this.runShutdown());
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
  }
@@ -1081,7 +1081,11 @@ class ES7 {
1081
1081
  });
1082
1082
  }
1083
1083
  finally {
1084
- this.clearScroll(scrollId);
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
  }
@@ -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
- dumper.dump();
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.6",
4
+ "version": "2.60.0",
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.6",
4
+ "version": "2.60.0",
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",