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.
- package/dist/bin/start-kuzzle-server +2 -1
- package/dist/lib/cluster/idCardHandler.d.ts +4 -0
- package/dist/lib/cluster/idCardHandler.js +23 -2
- package/dist/lib/cluster/node.d.ts +15 -3
- package/dist/lib/cluster/node.js +139 -72
- package/dist/lib/cluster/workers/IDCardRenewer.d.ts +6 -0
- package/dist/lib/cluster/workers/IDCardRenewer.js +14 -8
- package/dist/lib/core/auth/passportWrapper.js +3 -1
- package/dist/lib/kerror/codes/8-cluster.json +23 -0
- package/dist/lib/kuzzle/exitOnFailure.d.ts +13 -0
- package/dist/lib/kuzzle/exitOnFailure.js +60 -0
- package/dist/lib/kuzzle/kuzzle.d.ts +7 -0
- package/dist/lib/kuzzle/kuzzle.js +45 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
* @
|
|
147
|
-
*
|
|
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
|
-
*
|
|
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
|
/**
|
package/dist/lib/cluster/node.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
* @
|
|
527
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
115
|
-
|
|
116
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|