kuzzle 2.60.0-beta.2 → 2.60.0-beta.3

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.
@@ -69,6 +69,7 @@ const helpers_1 = require("yargs/helpers");
69
69
  * own `main`. Keep it that way.
70
70
  */
71
71
  const index_1 = require("../index");
72
+ const exitOnFailure_1 = require("../lib/kuzzle/exitOnFailure");
72
73
  /**
73
74
  * Parses one of the JSON files the command line points at. The type parameter
74
75
  * is a *claim about the file*, not a check: `JSON.parse` answers `any` and this
@@ -226,7 +227,7 @@ const run = async () => {
226
227
  }
227
228
  catch (error) {
228
229
  console.error(`[x] [ERROR] ${error instanceof Error ? error.stack : String(error)}`);
229
- process.exit(1);
230
+ await (0, exitOnFailure_1.exitOnFailure)();
230
231
  }
231
232
  };
232
233
  run();
@@ -87,6 +87,10 @@ export declare class ClusterIdCardHandler {
87
87
  private disposed;
88
88
  private readonly logger;
89
89
  constructor(node: ClusterNode);
90
+ /**
91
+ * How long an ID card outlives the last renewal of its node, in ms
92
+ */
93
+ get ttl(): number;
90
94
  /**
91
95
  * Generates and reserves a unique ID for this node instance.
92
96
  * Makes sure that the ID is not already taken by another node instance.
@@ -94,6 +94,12 @@ class ClusterIdCardHandler {
94
94
  this.ip = node.ip;
95
95
  this.refreshDelay = node.heartbeatDelay;
96
96
  }
97
+ /**
98
+ * How long an ID card outlives the last renewal of its node, in ms
99
+ */
100
+ get ttl() {
101
+ return this.refreshDelay * this.refreshMultiplier;
102
+ }
97
103
  /**
98
104
  * Generates and reserves a unique ID for this node instance.
99
105
  * Makes sure that the ID is not already taken by another node instance.
@@ -139,7 +145,8 @@ class ClusterIdCardHandler {
139
145
  const refreshWorker = this.constructWorker(`${__dirname}/workers/IDCardRenewer${(0, node_path_1.extname)(__filename)}`);
140
146
  this.refreshWorker = refreshWorker;
141
147
  refreshWorker.on("message", async (message) => {
142
- if (message.error) {
148
+ // A renewal racing the disposal finds the card deleted: not a failure
149
+ if (message.error && !this.disposed) {
143
150
  await this.node.evictSelf(message.error);
144
151
  }
145
152
  });
@@ -228,6 +235,20 @@ class ClusterIdCardHandler {
228
235
  // It could happens that the worker has been killed before the dispose causing send to fail
229
236
  }
230
237
  }
238
+ clearInterval(this.refreshTimer ?? undefined);
239
+ this.refreshTimer = null;
240
+ // The worker deletes the card too, but nothing waits for it: the process
241
+ // usually exits first, and the card then outlives it by a TTL, long
242
+ // enough for a node restarted on this IP to take it for a live duplicate
243
+ if (this.nodeIdKey !== null) {
244
+ try {
245
+ await global.kuzzle.ask("core:cache:internal:del", this.nodeIdKey);
246
+ await global.kuzzle.ask("core:cache:internal:execute", "srem", REDIS_ID_CARDS_INDEX, this.nodeIdKey);
247
+ }
248
+ catch (error) {
249
+ this.logger.warn(`Could not delete the ID card ${this.nodeIdKey}: it expires in ${this.ttl}ms (${error instanceof Error ? error.message : String(error)})`);
250
+ }
251
+ }
231
252
  }
232
253
  /**
233
254
  * Retrieves the ID cards from other nodes
@@ -309,7 +330,7 @@ class ClusterIdCardHandler {
309
330
  if (!this.idCard) {
310
331
  return false;
311
332
  }
312
- return global.kuzzle.ask("core:cache:internal:store", this.nodeIdKey, JSON.stringify(this.idCard.serialize()), { onlyIfNew: creation, ttl: this.refreshDelay * this.refreshMultiplier });
333
+ return global.kuzzle.ask("core:cache:internal:store", this.nodeIdKey, JSON.stringify(this.idCard.serialize()), { onlyIfNew: creation, ttl: this.ttl });
313
334
  }
314
335
  }
315
336
  exports.ClusterIdCardHandler = ClusterIdCardHandler;
@@ -143,16 +143,28 @@ declare class ClusterNode {
143
143
  *
144
144
  * Extracted from `handshake()` unchanged.
145
145
  *
146
- * @returns false when the caller must stop — the cluster is split and this
147
- * node is shutting down
146
+ * @throws {cluster.join.full_state_unavailable} on the second failure: the
147
+ * cluster is split
148
148
  */
149
149
  private retryFullState;
150
+ /**
151
+ * The ID cards of the other nodes, none of which has this node's IP.
152
+ *
153
+ * A card with this IP may be the stale one of a previous run of this node
154
+ * (a process killed, a container restarted at once): it expires within a
155
+ * TTL, so it is only a real duplicate if it is still there after that.
156
+ *
157
+ * @throws {cluster.join.duplicate_ip} if it is
158
+ */
159
+ private discoverNodes;
150
160
  /**
151
161
  * Discovers other active nodes from the cluster and, if other nodes exist,
152
162
  * starts a handshake procedure to sync this node and to make it able to
153
163
  * handle new client requests
154
164
  *
155
- * @return {void}
165
+ * Rejects with a `cluster.join.*` error, and shuts the node down, when it
166
+ * cannot join: the node must not go on starting as if it had (a duplicate
167
+ * IP, a split cluster, `joinTimeout` reached).
156
168
  */
157
169
  handshake(): Promise<void>;
158
170
  /**
@@ -19,6 +19,39 @@
19
19
  * See the License for the specific language governing permissions and
20
20
  * limitations under the License.
21
21
  */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
22
55
  var __importDefault = (this && this.__importDefault) || function (mod) {
23
56
  return (mod && mod.__esModule) ? mod : { "default": mod };
24
57
  };
@@ -37,6 +70,8 @@ const eventemitter3_1 = __importDefault(require("eventemitter3"));
37
70
  const intersection_1 = __importDefault(require("lodash/intersection"));
38
71
  const xor_1 = __importDefault(require("lodash/xor"));
39
72
  const long_1 = __importDefault(require("long"));
73
+ const kerror = __importStar(require("../kerror"));
74
+ const kuzzleError_1 = require("../kerror/errors/kuzzleError");
40
75
  const kuzzleStateEnum_1 = __importDefault(require("../kuzzle/kuzzleStateEnum"));
41
76
  const debug_1 = __importDefault(require("../util/debug"));
42
77
  const koncordeCompat_1 = require("../util/koncordeCompat");
@@ -47,6 +82,7 @@ const publisher_1 = __importDefault(require("./publisher"));
47
82
  const state_1 = __importDefault(require("./state"));
48
83
  const subscriber_1 = __importDefault(require("./subscriber"));
49
84
  const debug = (0, debug_1.default)("kuzzle:cluster:sync");
85
+ const joinError = kerror.wrap("cluster", "join");
50
86
  /** Cluster-wide events carrying the collection write locks (see registerEvents) */
51
87
  const COLLECTION_LOCK_EVENT = "core:storage:collection:lock";
52
88
  const COLLECTION_UNLOCK_EVENT = "core:storage:collection:unlock";
@@ -332,7 +368,7 @@ class ClusterNode {
332
368
  //
333
369
  // Exit code 1: this is a failure, and a node that exits 0 is not restarted
334
370
  // by an `on-failure` policy (#2785).
335
- global.kuzzle.shutdown(1);
371
+ void global.kuzzle.shutdown(1);
336
372
  }
337
373
  /**
338
374
  * Evicts a remote from the list
@@ -388,14 +424,15 @@ class ClusterNode {
388
424
  const candidates = this.electShutdownCandidates(splits, idCards);
389
425
  if (candidates.includes(this.nodeId)) {
390
426
  this.logger.error("[CLUSTER] Network split detected. This node is outside the cluster: shutting down.");
391
- global.kuzzle.shutdown();
427
+ // A failure, like `evictSelf`'s: exit non-zero (#2785)
428
+ void global.kuzzle.shutdown(1);
392
429
  return;
393
430
  }
394
431
  }
395
432
  catch (err) {
396
433
  this.logger.error("[CLUSTER] Unexpected exception caught during a cluster consistency check. Shutting down...");
397
434
  this.logger.error(err instanceof Error ? err.stack : (0, util_1.inspect)(err));
398
- global.kuzzle.shutdown();
435
+ void global.kuzzle.shutdown(1);
399
436
  }
400
437
  }
401
438
  /**
@@ -523,8 +560,8 @@ class ClusterNode {
523
560
  *
524
561
  * Extracted from `handshake()` unchanged.
525
562
  *
526
- * @returns false when the caller must stop — the cluster is split and this
527
- * node is shutting down
563
+ * @throws {cluster.join.full_state_unavailable} on the second failure: the
564
+ * cluster is split
528
565
  */
529
566
  async retryFullState(retried) {
530
567
  // Uh oh... no node was able to give us the full state.
@@ -533,9 +570,7 @@ class ClusterNode {
533
570
  // means we're probably facing a network split, and we must then shut
534
571
  // down.
535
572
  if (retried) {
536
- this.logger.error("[CLUSTER] Could not connect to discovered cluster nodes (network split detected). Shutting down.");
537
- global.kuzzle.shutdown();
538
- return false;
573
+ throw joinError.get("full_state_unavailable");
539
574
  }
540
575
  // Disposes all subscribers
541
576
  for (const subscriber of this.remoteNodes.values()) {
@@ -546,81 +581,110 @@ class ClusterNode {
546
581
  const retryDelay = this.heartbeatDelay * 1.5;
547
582
  this.logger.warn(`[CLUSTER] Unable to connect to discovered cluster nodes. Retrying in ${retryDelay}ms...`);
548
583
  await bluebird_1.default.delay(retryDelay);
549
- return true;
584
+ }
585
+ /**
586
+ * The ID cards of the other nodes, none of which has this node's IP.
587
+ *
588
+ * A card with this IP may be the stale one of a previous run of this node
589
+ * (a process killed, a container restarted at once): it expires within a
590
+ * TTL, so it is only a real duplicate if it is still there after that.
591
+ *
592
+ * @throws {cluster.join.duplicate_ip} if it is
593
+ */
594
+ async discoverNodes() {
595
+ const nodes = await this.idCardHandler.getRemoteIdCards();
596
+ if (!nodes.some((node) => node.ip === this.ip)) {
597
+ return nodes;
598
+ }
599
+ // A heartbeat more than the TTL: the card's last renewal may be in flight
600
+ const delay = this.idCardHandler.ttl + this.heartbeatDelay;
601
+ this.logger.warn(`[CLUSTER] Another node has the same IP address as this one (${this.ip}). Waiting ${delay}ms for it to expire, in case it is a stale ID card...`);
602
+ await bluebird_1.default.delay(delay);
603
+ const remaining = await this.idCardHandler.getRemoteIdCards();
604
+ const duplicate = remaining.find((node) => node.ip === this.ip);
605
+ if (duplicate) {
606
+ throw joinError.get("duplicate_ip", this.ip, duplicate.id);
607
+ }
608
+ return remaining;
550
609
  }
551
610
  /**
552
611
  * Discovers other active nodes from the cluster and, if other nodes exist,
553
612
  * starts a handshake procedure to sync this node and to make it able to
554
613
  * handle new client requests
555
614
  *
556
- * @return {void}
615
+ * Rejects with a `cluster.join.*` error, and shuts the node down, when it
616
+ * cannot join: the node must not go on starting as if it had (a duplicate
617
+ * IP, a split cluster, `joinTimeout` reached).
557
618
  */
558
619
  async handshake() {
559
- const handshakeTimeout = setTimeout(() => {
560
- this.logger.error(`[CLUSTER] Failed to join the cluster: timed out (joinTimeout: ${this.config.joinTimeout}ms)`);
561
- global.kuzzle.shutdown();
562
- }, this.config.joinTimeout);
620
+ let timedOut = false;
621
+ let handshakeTimeout;
622
+ const timeout = new Promise((_resolve, reject) => {
623
+ handshakeTimeout = setTimeout(() => {
624
+ timedOut = true;
625
+ reject(joinError.get("timeout", this.config.joinTimeout));
626
+ }, this.config.joinTimeout);
627
+ });
563
628
  // No retry limit: the handshakes are taken one at a time, and the
564
629
  // `joinTimeout` above is what gives up on a cluster that never lets this
565
630
  // node in.
631
+ const join = (0, distributedLock_1.withLock)("clusterHandshake", async () => {
632
+ // Given up on while waiting for the lock: do not join after all
633
+ if (timedOut) {
634
+ return;
635
+ }
636
+ // Create the ID Key AFTER the handshake lock is actually taken,
637
+ // to prevent race conditions (other nodes attempting to connect to this
638
+ // node while it's still initializing)
639
+ await this.idCardHandler.createIdCard();
640
+ debug("[CLUSTER] ID Card created");
641
+ const nodeId = this.idCardHandler.nodeId;
642
+ (0, assert_1.default)(nodeId !== null, "[CLUSTER] createIdCard() returned without reserving a node id");
643
+ this.nodeId = nodeId;
644
+ await this.startHeartbeat(); // NOSONAR: TD-26
645
+ debug("[CLUSTER] Start heartbeat");
646
+ let retried = false;
647
+ let fullState;
648
+ let nodes;
649
+ debug("[CLUSTER] Start retrieving full state..");
650
+ do {
651
+ nodes = await this.discoverNodes();
652
+ debug("[CLUSTER] %s remote nodes discovered", nodes.length);
653
+ // No other nodes detected = no handshake required
654
+ if (nodes.length === 0) {
655
+ this.trackActivity(this.nodeId, this.ip, nodeActivityEnum.ADDED);
656
+ return;
657
+ }
658
+ await this.subscribeToNodes(nodes);
659
+ debug("[CLUSTER] Successfully subscribed to nodes");
660
+ fullState = await this.command.getFullState(nodes);
661
+ if (fullState === null) {
662
+ // Sequential by design: the retry waits for a heartbeat round
663
+ await this.retryFullState(retried); // NOSONAR
664
+ retried = true;
665
+ }
666
+ } while (fullState === null);
667
+ debug("[CLUSTER] Fullstate retrieved, loading into node..");
668
+ await this.fullState.loadFullState(fullState); // NOSONAR: TD-26
669
+ this.activity = fullState.activity ?? this.activity;
670
+ debug("[CLUSTER] Fullstate loaded.");
671
+ const handshakeResponses = await this.command.broadcastHandshake(nodes);
672
+ debug("[CLUSTER] Successful handshakes with other nodes.");
673
+ await this.syncWithPeers(handshakeResponses, fullState);
674
+ // `createIdCard()` adopts `global.nodeId`, so these are normally the same
675
+ // string and the line reads as one name (TD-59, #2764). They still differ
676
+ // in the two cases the adoption cannot cover — a stale IdCard holding the
677
+ // key, and a Kuzzle running without a Backend — and those are exactly the
678
+ // cases where a reader needs to be told, so the line says both.
679
+ this.logger.info(global.nodeId && global.nodeId !== this.nodeId
680
+ ? `[CLUSTER] Successfully joined the cluster as "${this.nodeId}" (process node id: ${global.nodeId}).`
681
+ : `[CLUSTER] Successfully joined the cluster as "${this.nodeId}".`);
682
+ this.trackActivity(this.nodeId, this.ip, nodeActivityEnum.ADDED);
683
+ }, { retryAttempts: Number.POSITIVE_INFINITY });
566
684
  try {
567
- await (0, distributedLock_1.withLock)("clusterHandshake", async () => {
568
- // Create the ID Key AFTER the handshake lock is actually taken,
569
- // to prevent race conditions (other nodes attempting to connect to this
570
- // node while it's still initializing)
571
- await this.idCardHandler.createIdCard();
572
- debug("[CLUSTER] ID Card created");
573
- const nodeId = this.idCardHandler.nodeId;
574
- (0, assert_1.default)(nodeId !== null, "[CLUSTER] createIdCard() returned without reserving a node id");
575
- this.nodeId = nodeId;
576
- await this.startHeartbeat(); // NOSONAR: TD-26
577
- debug("[CLUSTER] Start heartbeat");
578
- let retried = false;
579
- let fullState;
580
- let nodes;
581
- debug("[CLUSTER] Start retrieving full state..");
582
- do {
583
- nodes = await this.idCardHandler.getRemoteIdCards();
584
- debug("[CLUSTER] %s remote nodes discovered", nodes.length);
585
- // No other nodes detected = no handshake required
586
- if (nodes.length === 0) {
587
- this.trackActivity(this.nodeId, this.ip, nodeActivityEnum.ADDED);
588
- return;
589
- }
590
- // Verify that no other node share the same IP address as this one
591
- const duplicate = nodes.filter((node) => node.ip === this.ip);
592
- if (duplicate.length > 0) {
593
- this.logger.error(`[CLUSTER] Another node share the same IP address as this one (${this.ip}): ${duplicate[0]?.id}. Shutting down.`);
594
- global.kuzzle.shutdown();
595
- return;
596
- }
597
- await this.subscribeToNodes(nodes);
598
- debug("[CLUSTER] Successfully subscribed to nodes");
599
- fullState = await this.command.getFullState(nodes);
600
- if (fullState === null) {
601
- if (!(await this.retryFullState(retried))) {
602
- return;
603
- }
604
- retried = true;
605
- }
606
- } while (fullState === null);
607
- debug("[CLUSTER] Fullstate retrieved, loading into node..");
608
- await this.fullState.loadFullState(fullState); // NOSONAR: TD-26
609
- this.activity = fullState.activity ?? this.activity;
610
- debug("[CLUSTER] Fullstate loaded.");
611
- const handshakeResponses = await this.command.broadcastHandshake(nodes);
612
- debug("[CLUSTER] Successful handshakes with other nodes.");
613
- await this.syncWithPeers(handshakeResponses, fullState);
614
- // `createIdCard()` adopts `global.nodeId`, so these are normally the same
615
- // string and the line reads as one name (TD-59, #2764). They still differ
616
- // in the two cases the adoption cannot cover — a stale IdCard holding the
617
- // key, and a Kuzzle running without a Backend — and those are exactly the
618
- // cases where a reader needs to be told, so the line says both.
619
- this.logger.info(global.nodeId && global.nodeId !== this.nodeId
620
- ? `[CLUSTER] Successfully joined the cluster as "${this.nodeId}" (process node id: ${global.nodeId}).`
621
- : `[CLUSTER] Successfully joined the cluster as "${this.nodeId}".`);
622
- this.trackActivity(this.nodeId, this.ip, nodeActivityEnum.ADDED);
623
- }, { retryAttempts: Number.POSITIVE_INFINITY });
685
+ // A timeout does not stop `join`: the node it settles later is shutting
686
+ // down already
687
+ await Promise.race([join, timeout]);
624
688
  }
625
689
  catch (error) {
626
690
  // The handshake cannot be stopped where it is: it may still introduce
@@ -628,7 +692,10 @@ class ClusterNode {
628
692
  // half-joined, whatever the application does with the failed startup.
629
693
  if (error instanceof distributedLock_1.MutexLockLostError) {
630
694
  this.logger.error("[CLUSTER] Lost the handshake lock while joining the cluster. Shutting down.");
631
- void global.kuzzle.shutdown();
695
+ }
696
+ if (error instanceof distributedLock_1.MutexLockLostError ||
697
+ (error instanceof kuzzleError_1.KuzzleError && error.id.startsWith("cluster.join."))) {
698
+ void global.kuzzle.shutdown(1);
632
699
  }
633
700
  throw error;
634
701
  }
@@ -38,6 +38,12 @@ declare class IDCardRenewer {
38
38
  init(config: IDCardRenewerConfig): Promise<void>;
39
39
  initRedis(config?: RedisServiceConfig, name?: string): Promise<void>;
40
40
  renewIDCard(): Promise<void>;
41
+ /**
42
+ * Disposes the worker and tells the parent why, once: the renewals queued
43
+ * behind a stalled redis all fail together, and each report evicts the node
44
+ * again (#3017)
45
+ */
46
+ private reportFailure;
41
47
  dispose(): Promise<void>;
42
48
  }
43
49
  export { IDCardRenewer };
@@ -104,18 +104,24 @@ class IDCardRenewer {
104
104
  // Unable to refresh the key in time before it expires
105
105
  // => this node is too slow, we need to remove it from the cluster
106
106
  if (refreshed === 0) {
107
- await this.dispose();
108
- process.send?.({
109
- error: "Node too slow: ID card expired",
110
- });
107
+ await this.reportFailure("Node too slow: ID card expired");
111
108
  }
112
109
  }
113
110
  catch (error) {
114
- await this.dispose();
115
- process.send?.({
116
- error: `Failed to refresh ID Card: ${messageOf(error)}`,
117
- });
111
+ await this.reportFailure(`Failed to refresh ID Card: ${messageOf(error)}`);
112
+ }
113
+ }
114
+ /**
115
+ * Disposes the worker and tells the parent why, once: the renewals queued
116
+ * behind a stalled redis all fail together, and each report evicts the node
117
+ * again (#3017)
118
+ */
119
+ async reportFailure(error) {
120
+ if (this.disposed) {
121
+ return;
118
122
  }
123
+ await this.dispose();
124
+ process.send?.({ error });
119
125
  }
120
126
  async dispose() {
121
127
  if (this.disposed) {
@@ -99,7 +99,9 @@ class PassportWrapper {
99
99
  fail(err);
100
100
  }
101
101
  else if (!user) {
102
- const message = typeof info === "object" && "message" in info
102
+ const message =
103
+ // `typeof null` is "object" too: a strategy may fail with no info
104
+ typeof info === "object" && info !== null && "message" in info
103
105
  ? info.message
104
106
  : undefined;
105
107
  reject(kerror.get("plugin", "strategy", "missing_user", message));
@@ -11,6 +11,29 @@
11
11
  "class": "InternalError"
12
12
  }
13
13
  }
14
+ },
15
+ "join": {
16
+ "code": 1,
17
+ "errors": {
18
+ "duplicate_ip": {
19
+ "description": "Another node of the cluster has the same IP address as the one joining it. The joining node does not start.",
20
+ "code": 1,
21
+ "message": "Another node shares the same IP address as this one (%s): %s.",
22
+ "class": "InternalError"
23
+ },
24
+ "full_state_unavailable": {
25
+ "description": "None of the cluster nodes discovered could give the joining node the cluster state, twice in a row (network split). The joining node does not start.",
26
+ "code": 2,
27
+ "message": "Could not connect to the discovered cluster nodes (network split detected).",
28
+ "class": "InternalError"
29
+ },
30
+ "timeout": {
31
+ "description": "The node did not join the cluster within the configured cluster.joinTimeout. It does not start.",
32
+ "code": 3,
33
+ "message": "Failed to join the cluster: timed out (joinTimeout: %sms).",
34
+ "class": "InternalError"
35
+ }
36
+ }
14
37
  }
15
38
  }
16
39
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ends the process with exit code 1, for a start script whose Kuzzle failed
3
+ * to start or to run.
4
+ *
5
+ * A bare `process.exit(1)` cuts Kuzzle's graceful shutdown short: the locks
6
+ * the node holds and its ID card stay in Redis until they expire (#3022).
7
+ * Once a Kuzzle instance exists, this goes through `shutdown(1)` instead,
8
+ * which joins the one a failed start already began (#3020). Before that,
9
+ * there is nothing to release.
10
+ *
11
+ * Internal to Kuzzle: not exported by the package.
12
+ */
13
+ export declare function exitOnFailure(): Promise<void>;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /*
3
+ * Kuzzle, a backend software, self-hostable and ready to use
4
+ * to power modern apps
5
+ *
6
+ * Copyright 2015-2022 Kuzzle
7
+ * mailto: support AT kuzzle.io
8
+ * website: http://kuzzle.io
9
+ *
10
+ * Licensed under the Apache License, Version 2.0 (the "License");
11
+ * you may not use this file except in compliance with the License.
12
+ * You may obtain a copy of the License at
13
+ *
14
+ * https://www.apache.org/licenses/LICENSE-2.0
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.exitOnFailure = exitOnFailure;
24
+ const node_util_1 = require("node:util");
25
+ /**
26
+ * Ends the process with exit code 1, for a start script whose Kuzzle failed
27
+ * to start or to run.
28
+ *
29
+ * A bare `process.exit(1)` cuts Kuzzle's graceful shutdown short: the locks
30
+ * the node holds and its ID card stay in Redis until they expire (#3022).
31
+ * Once a Kuzzle instance exists, this goes through `shutdown(1)` instead,
32
+ * which joins the one a failed start already began (#3020). Before that,
33
+ * there is nothing to release.
34
+ *
35
+ * Internal to Kuzzle: not exported by the package.
36
+ */
37
+ async function exitOnFailure() {
38
+ const kuzzle = currentKuzzle();
39
+ if (kuzzle) {
40
+ try {
41
+ // Ends with `process.exit`
42
+ await kuzzle.shutdown(1);
43
+ return;
44
+ }
45
+ catch (error) {
46
+ kuzzle.log.error(`Unable to shut down: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
47
+ }
48
+ }
49
+ process.exit(1);
50
+ }
51
+ function currentKuzzle() {
52
+ try {
53
+ return global.kuzzle;
54
+ }
55
+ catch {
56
+ // The `global.kuzzle` getter throws until an instance is built
57
+ return undefined;
58
+ }
59
+ }
60
+ //# sourceMappingURL=exitOnFailure.js.map
@@ -17,6 +17,12 @@ import { Logger } from "./Logger";
17
17
  declare class Kuzzle extends KuzzleEventEmitter {
18
18
  config: IKuzzleConfiguration;
19
19
  private _state;
20
+ /** The running shutdown, which later `shutdown()` calls wait for (#3017) */
21
+ private shutdownPromise;
22
+ /** The highest exit code asked for by the `shutdown()` calls so far */
23
+ private shutdownExitCode;
24
+ /** Ends a failure's shutdown that does not finish in time */
25
+ private shutdownWatchdog;
20
26
  log: Logger;
21
27
  /** Read by `PluginsManager` to locate the plugins directories. */
22
28
  rootPath: string;
@@ -117,6 +123,7 @@ declare class Kuzzle extends KuzzleEventEmitter {
117
123
  * @returns {Promise}
118
124
  */
119
125
  shutdown(exitCode?: number): Promise<void>;
126
+ private runShutdown;
120
127
  /**
121
128
  * Execute multiple handlers only once on any given environment
122
129
  *
@@ -93,6 +93,12 @@ const Logger_1 = require("./Logger");
93
93
  const vault_1 = __importDefault(require("./vault"));
94
94
  const name_generator_1 = require("../util/name-generator");
95
95
  let _kuzzle = null;
96
+ /**
97
+ * How long a shutdown caused by a failure may run before the process exits
98
+ * anyway: a `kuzzle:shutdown` pipe or a lock release that never settles must
99
+ * not keep a failed node alive, which a bare `process.exit(1)` never did
100
+ */
101
+ const FAILURE_SHUTDOWN_TIMEOUT = 30000;
96
102
  Reflect.defineProperty(global, "kuzzle", {
97
103
  configurable: true,
98
104
  enumerable: false,
@@ -113,6 +119,12 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
113
119
  constructor(config) {
114
120
  super(config.plugins.common.maxConcurrentPipes, config.plugins.common.pipesBufferSize);
115
121
  this._state = kuzzleStateEnum_1.default.STARTING;
122
+ /** The running shutdown, which later `shutdown()` calls wait for (#3017) */
123
+ this.shutdownPromise = null;
124
+ /** The highest exit code asked for by the `shutdown()` calls so far */
125
+ this.shutdownExitCode = 0;
126
+ /** Ends a failure's shutdown that does not finish in time */
127
+ this.shutdownWatchdog = null;
116
128
  global.kuzzle = this;
117
129
  this._state = kuzzleStateEnum_1.default.STARTING;
118
130
  this.config = config;
@@ -204,6 +216,16 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
204
216
  }
205
217
  catch (error) {
206
218
  this.log.error(`[X] Cannot start Kuzzle ${this.version}: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
219
+ // A failed start cannot be retried (a second Kuzzle instance cannot be
220
+ // built), and what it did start keeps the process alive: in production,
221
+ // a node with no listener and an ID card still renewed (#3020). Not
222
+ // awaited, so the caller still gets the error before the process exits.
223
+ this.shutdown(1).catch((shutdownError) => {
224
+ this.log.error(`Unable to shut down after a failed start: ${shutdownError instanceof Error
225
+ ? shutdownError.message
226
+ : (0, node_util_1.inspect)(shutdownError)}`);
227
+ process.exit(1);
228
+ });
207
229
  throw error;
208
230
  }
209
231
  }
@@ -231,6 +253,22 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
231
253
  * @returns {Promise}
232
254
  */
233
255
  async shutdown(exitCode = 0) {
256
+ // Several sources can ask for it at once (a signal, a cluster eviction,
257
+ // the `exit` handler that `process.exit` below fires): they share the one
258
+ // shutdown, and a failure reported by any of them keeps the exit non-zero
259
+ this.shutdownExitCode = Math.max(this.shutdownExitCode, exitCode);
260
+ // Not for a requested shutdown: it waits for the requests still running,
261
+ // however long they take
262
+ if (exitCode !== 0) {
263
+ this.shutdownWatchdog ?? (this.shutdownWatchdog = setTimeout(() => {
264
+ this.log.error(`[shutdown] Not finished after ${FAILURE_SHUTDOWN_TIMEOUT}ms: exiting now`);
265
+ process.exit(this.shutdownExitCode);
266
+ }, FAILURE_SHUTDOWN_TIMEOUT));
267
+ }
268
+ this.shutdownPromise ?? (this.shutdownPromise = this.runShutdown());
269
+ return this.shutdownPromise;
270
+ }
271
+ async runShutdown() {
234
272
  this._state = kuzzleStateEnum_1.default.SHUTTING_DOWN;
235
273
  this.log.info("Initiating shutdown...");
236
274
  // Ask the network layer to stop accepting new request
@@ -257,7 +295,10 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
257
295
  // `pluginsManager?.application?.log?.flush?.()` was a silent no-op and the
258
296
  // application's buffered logs were dropped on shutdown (TD-51).
259
297
  await this?.pluginsManager?.application?.instance?.log?.flush?.();
260
- process.exit(exitCode);
298
+ if (this.shutdownWatchdog !== null) {
299
+ clearTimeout(this.shutdownWatchdog);
300
+ }
301
+ process.exit(this.shutdownExitCode);
261
302
  }
262
303
  /**
263
304
  * Execute multiple handlers only once on any given environment
@@ -608,7 +649,9 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
608
649
  // nothing to do with that error
609
650
  }
610
651
  }
611
- await this.shutdown();
652
+ // Only abnormal terminations get here: exit non-zero, so that an
653
+ // `on-failure` restart policy brings the node back (#3020)
654
+ await this.shutdown(1);
612
655
  }
613
656
  }
614
657
  exports.Kuzzle = Kuzzle;
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.2",
4
+ "version": "2.60.0-beta.3",
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.2",
4
+ "version": "2.60.0-beta.3",
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",