kuzzle 2.60.0-beta.2 → 2.60.0-beta.4
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/api/controllers/documentController.d.ts +1 -1
- package/dist/lib/cluster/command.d.ts +7 -0
- package/dist/lib/cluster/command.js +67 -28
- package/dist/lib/cluster/idCardHandler.d.ts +4 -0
- package/dist/lib/cluster/idCardHandler.js +23 -2
- package/dist/lib/cluster/node.d.ts +28 -5
- package/dist/lib/cluster/node.js +159 -76
- package/dist/lib/cluster/subscriber.d.ts +2 -0
- package/dist/lib/cluster/subscriber.js +6 -0
- 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/core/backend/backend.d.ts +1 -1
- package/dist/lib/core/backend/backend.js +15 -15
- package/dist/lib/core/backend/backendCluster.d.ts +1 -1
- package/dist/lib/core/shared/store.js +4 -0
- package/dist/lib/core/storage/clientAdapter.d.ts +6 -0
- package/dist/lib/core/storage/clientAdapter.js +35 -5
- 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/internalIndexHandler.js +24 -6
- package/dist/lib/kuzzle/kuzzle.d.ts +7 -0
- package/dist/lib/kuzzle/kuzzle.js +45 -2
- package/dist/lib/model/security/profile.d.ts +1 -1
- package/dist/lib/model/security/role.d.ts +1 -1
- package/dist/lib/model/security/user.d.ts +1 -1
- package/dist/lib/service/storage/7/elasticsearch.d.ts +12 -0
- package/dist/lib/service/storage/7/elasticsearch.js +32 -1
- package/dist/lib/service/storage/7/esWrapper.js +31 -2
- package/dist/lib/service/storage/8/elasticsearch.d.ts +12 -0
- package/dist/lib/service/storage/8/elasticsearch.js +32 -1
- package/dist/lib/service/storage/8/esWrapper.js +31 -2
- package/dist/lib/types/events/EventGenericDocument.d.ts +2 -1
- package/dist/lib/types/events/EventProtocol.d.ts +1 -1
- package/dist/lib/util/mutex.d.ts +2 -1
- package/dist/lib/util/mutex.js +7 -1
- 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();
|
|
@@ -77,7 +77,7 @@ declare class DocumentController extends NativeController {
|
|
|
77
77
|
errors: any;
|
|
78
78
|
successes: any;
|
|
79
79
|
}>;
|
|
80
|
-
export(request: KuzzleRequest): Promise<import("
|
|
80
|
+
export(request: KuzzleRequest): Promise<import("../../types").HttpStream>;
|
|
81
81
|
/**
|
|
82
82
|
* @param {Request} request
|
|
83
83
|
* @returns {Promise<Object>}
|
|
@@ -21,6 +21,8 @@ type CommandingNode = {
|
|
|
21
21
|
};
|
|
22
22
|
ip: string;
|
|
23
23
|
nodeId: string;
|
|
24
|
+
/** Whether `nodeId` can be read: false until the handshake sets it. */
|
|
25
|
+
hasNodeId: boolean;
|
|
24
26
|
activity: Activity[];
|
|
25
27
|
/** Only `serialize()`: the command layer never mutates the state. */
|
|
26
28
|
fullState: {
|
|
@@ -102,6 +104,11 @@ declare class ClusterCommand {
|
|
|
102
104
|
* This method can (and should) never return a rejected promise.
|
|
103
105
|
*/
|
|
104
106
|
listen(): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Answers one request. Every request gets exactly one reply: REP/REQ
|
|
109
|
+
* requires it, and a request left unanswered wedges the peer's socket.
|
|
110
|
+
*/
|
|
111
|
+
private answer;
|
|
105
112
|
/**
|
|
106
113
|
* Closes opened sockets and marks this server as closed.
|
|
107
114
|
*/
|
|
@@ -159,32 +159,9 @@ class ClusterCommand {
|
|
|
159
159
|
while (this.state === stateEnum.RUNNING) {
|
|
160
160
|
try {
|
|
161
161
|
const [type, data] = await this.server.receive();
|
|
162
|
-
//
|
|
163
|
-
// request
|
|
164
|
-
|
|
165
|
-
switch (type?.toString()) {
|
|
166
|
-
case commandMessages_1.commandTopic.FULLSTATE:
|
|
167
|
-
await this.sendFullState();
|
|
168
|
-
break;
|
|
169
|
-
case commandMessages_1.commandTopic.HANDSHAKE:
|
|
170
|
-
if (data === undefined) {
|
|
171
|
-
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
await this.handleHandshake(data);
|
|
175
|
-
break;
|
|
176
|
-
case commandMessages_1.commandTopic.RETRANSMIT:
|
|
177
|
-
if (data === undefined) {
|
|
178
|
-
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
await this.handleRetransmit(data);
|
|
182
|
-
break;
|
|
183
|
-
default:
|
|
184
|
-
// REP/REQ sockets expect a reply to each request made, so we have
|
|
185
|
-
// to send some kind of response on an invalid request received
|
|
186
|
-
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
187
|
-
}
|
|
162
|
+
// NOSONAR (S9382): a REP socket strictly alternates receive and send,
|
|
163
|
+
// so a request must be answered before the next one can be read.
|
|
164
|
+
await this.answer(type, data); // NOSONAR
|
|
188
165
|
}
|
|
189
166
|
catch (e) {
|
|
190
167
|
if (this.state !== stateEnum.CLOSED) {
|
|
@@ -193,6 +170,47 @@ class ClusterCommand {
|
|
|
193
170
|
}
|
|
194
171
|
}
|
|
195
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Answers one request. Every request gets exactly one reply: REP/REQ
|
|
175
|
+
* requires it, and a request left unanswered wedges the peer's socket.
|
|
176
|
+
*/
|
|
177
|
+
async answer(type, data) {
|
|
178
|
+
// The socket is bound before the handshake sets this node's id, and a
|
|
179
|
+
// peer still talking to a dead node on this IP can reach it (#3031). A
|
|
180
|
+
// node with no id yet has no state to serve and cannot take a peer in:
|
|
181
|
+
// every request is answered DISCARDED, which the requester treats as no
|
|
182
|
+
// answer.
|
|
183
|
+
if (!this.node.hasNodeId) {
|
|
184
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// A REQ frame carries a topic and, for a handshake, a payload. A request
|
|
188
|
+
// missing either is answered with DISCARDED, which is what this socket
|
|
189
|
+
// owes every REQ — the two frames were read unchecked.
|
|
190
|
+
switch (type?.toString()) {
|
|
191
|
+
case commandMessages_1.commandTopic.FULLSTATE:
|
|
192
|
+
await this.sendFullState();
|
|
193
|
+
break;
|
|
194
|
+
case commandMessages_1.commandTopic.HANDSHAKE:
|
|
195
|
+
if (data === undefined) {
|
|
196
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
await this.handleHandshake(data);
|
|
200
|
+
break;
|
|
201
|
+
case commandMessages_1.commandTopic.RETRANSMIT:
|
|
202
|
+
if (data === undefined) {
|
|
203
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
await this.handleRetransmit(data);
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
// REP/REQ sockets expect a reply to each request made, so we have
|
|
210
|
+
// to send some kind of response on an invalid request received
|
|
211
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
196
214
|
/**
|
|
197
215
|
* Closes opened sockets and marks this server as closed.
|
|
198
216
|
*/
|
|
@@ -339,7 +357,19 @@ class ClusterCommand {
|
|
|
339
357
|
// No payload message for a full state request
|
|
340
358
|
await req.send([commandMessages_1.commandTopic.FULLSTATE, null]);
|
|
341
359
|
try {
|
|
342
|
-
|
|
360
|
+
// NOSONAR (S9382): one node at a time by design, the loop stops at
|
|
361
|
+
// the first one that serves a full state.
|
|
362
|
+
const [topic, payload] = await req.receive(); // NOSONAR
|
|
363
|
+
// DISCARDED carries an empty frame, which would decode as an empty
|
|
364
|
+
// full state: a node that cannot serve one (#3031) is skipped like
|
|
365
|
+
// one that does not answer.
|
|
366
|
+
if (topic?.toString() === commandMessages_1.commandTopic.FULLSTATE && payload) {
|
|
367
|
+
fullState = payload;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
this.logger.warn(`Unable to fetch a full state from node ${id} (request discarded)`);
|
|
371
|
+
idx = (idx + 1) % nodes.length;
|
|
372
|
+
}
|
|
343
373
|
}
|
|
344
374
|
catch {
|
|
345
375
|
// no response from the remote node in a timely fashion... retrying
|
|
@@ -403,7 +433,16 @@ class ClusterCommand {
|
|
|
403
433
|
await req.send([commandMessages_1.commandTopic.HANDSHAKE, payload]);
|
|
404
434
|
let response = null;
|
|
405
435
|
try {
|
|
406
|
-
[,
|
|
436
|
+
const [topic, answer] = await req.receive();
|
|
437
|
+
// DISCARDED carries an empty frame, which would decode as a handshake
|
|
438
|
+
// answer with nothing in it: a node that refused the handshake (#3031)
|
|
439
|
+
// is one that did not answer.
|
|
440
|
+
if (topic?.toString() === commandMessages_1.commandTopic.HANDSHAKE && answer) {
|
|
441
|
+
response = answer;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
this.logger.warn(`Couldn't complete handshake with node ${id}: request discarded`);
|
|
445
|
+
}
|
|
407
446
|
}
|
|
408
447
|
catch {
|
|
409
448
|
this.logger.warn(`Couldn't complete handshake with node ${id}: no response received`);
|
|
@@ -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;
|
|
@@ -41,10 +41,21 @@ declare class ClusterNode {
|
|
|
41
41
|
* The id of the ID card this node reserved during `handshake()`.
|
|
42
42
|
*
|
|
43
43
|
* Null until the handshake gets that far, and read as a `string` at
|
|
44
|
-
* seventeen sites here plus `subscriber.ts` and `command.ts
|
|
45
|
-
*
|
|
44
|
+
* seventeen sites here plus `subscriber.ts` and `command.ts`. The accessor
|
|
45
|
+
* says once where that can fail: an internal read before the handshake is a
|
|
46
|
+
* bug, and it throws.
|
|
47
|
+
*
|
|
48
|
+
* Network input is not internal: the command socket is bound before the
|
|
49
|
+
* handshake, and a node restarted on the IP of a dead one can be reached by
|
|
50
|
+
* peers still talking to that dead node (#3031). The paths network input
|
|
51
|
+
* reaches check `hasNodeId` first.
|
|
46
52
|
*/
|
|
47
53
|
get nodeId(): string;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the handshake has set this node's id yet: the non-throwing check
|
|
56
|
+
* for the paths network input reaches before it has (#3031).
|
|
57
|
+
*/
|
|
58
|
+
get hasNodeId(): boolean;
|
|
48
59
|
set nodeId(id: string);
|
|
49
60
|
private heartbeatTimer;
|
|
50
61
|
readonly idCardHandler: ClusterIdCardHandler;
|
|
@@ -143,16 +154,28 @@ declare class ClusterNode {
|
|
|
143
154
|
*
|
|
144
155
|
* Extracted from `handshake()` unchanged.
|
|
145
156
|
*
|
|
146
|
-
* @
|
|
147
|
-
*
|
|
157
|
+
* @throws {cluster.join.full_state_unavailable} on the second failure: the
|
|
158
|
+
* cluster is split
|
|
148
159
|
*/
|
|
149
160
|
private retryFullState;
|
|
161
|
+
/**
|
|
162
|
+
* The ID cards of the other nodes, none of which has this node's IP.
|
|
163
|
+
*
|
|
164
|
+
* A card with this IP may be the stale one of a previous run of this node
|
|
165
|
+
* (a process killed, a container restarted at once): it expires within a
|
|
166
|
+
* TTL, so it is only a real duplicate if it is still there after that.
|
|
167
|
+
*
|
|
168
|
+
* @throws {cluster.join.duplicate_ip} if it is
|
|
169
|
+
*/
|
|
170
|
+
private discoverNodes;
|
|
150
171
|
/**
|
|
151
172
|
* Discovers other active nodes from the cluster and, if other nodes exist,
|
|
152
173
|
* starts a handshake procedure to sync this node and to make it able to
|
|
153
174
|
* handle new client requests
|
|
154
175
|
*
|
|
155
|
-
*
|
|
176
|
+
* Rejects with a `cluster.join.*` error, and shuts the node down, when it
|
|
177
|
+
* cannot join: the node must not go on starting as if it had (a duplicate
|
|
178
|
+
* IP, a split cluster, `joinTimeout` reached).
|
|
156
179
|
*/
|
|
157
180
|
handshake(): Promise<void>;
|
|
158
181
|
/**
|
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";
|
|
@@ -164,8 +200,14 @@ class ClusterNode {
|
|
|
164
200
|
* The id of the ID card this node reserved during `handshake()`.
|
|
165
201
|
*
|
|
166
202
|
* Null until the handshake gets that far, and read as a `string` at
|
|
167
|
-
* seventeen sites here plus `subscriber.ts` and `command.ts
|
|
168
|
-
*
|
|
203
|
+
* seventeen sites here plus `subscriber.ts` and `command.ts`. The accessor
|
|
204
|
+
* says once where that can fail: an internal read before the handshake is a
|
|
205
|
+
* bug, and it throws.
|
|
206
|
+
*
|
|
207
|
+
* Network input is not internal: the command socket is bound before the
|
|
208
|
+
* handshake, and a node restarted on the IP of a dead one can be reached by
|
|
209
|
+
* peers still talking to that dead node (#3031). The paths network input
|
|
210
|
+
* reaches check `hasNodeId` first.
|
|
169
211
|
*/
|
|
170
212
|
get nodeId() {
|
|
171
213
|
if (this._nodeId === null) {
|
|
@@ -173,6 +215,13 @@ class ClusterNode {
|
|
|
173
215
|
}
|
|
174
216
|
return this._nodeId;
|
|
175
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether the handshake has set this node's id yet: the non-throwing check
|
|
220
|
+
* for the paths network input reaches before it has (#3031).
|
|
221
|
+
*/
|
|
222
|
+
get hasNodeId() {
|
|
223
|
+
return this._nodeId !== null;
|
|
224
|
+
}
|
|
176
225
|
set nodeId(id) {
|
|
177
226
|
this._nodeId = id;
|
|
178
227
|
}
|
|
@@ -332,7 +381,7 @@ class ClusterNode {
|
|
|
332
381
|
//
|
|
333
382
|
// Exit code 1: this is a failure, and a node that exits 0 is not restarted
|
|
334
383
|
// by an `on-failure` policy (#2785).
|
|
335
|
-
global.kuzzle.shutdown(1);
|
|
384
|
+
void global.kuzzle.shutdown(1);
|
|
336
385
|
}
|
|
337
386
|
/**
|
|
338
387
|
* Evicts a remote from the list
|
|
@@ -353,8 +402,11 @@ class ClusterNode {
|
|
|
353
402
|
this.remoteNodes.delete(nodeId);
|
|
354
403
|
this.fullState.removeNode(nodeId);
|
|
355
404
|
subscriber.dispose();
|
|
356
|
-
|
|
357
|
-
|
|
405
|
+
// A subscriber can evict its remote node (heartbeat lost, gap, invalid
|
|
406
|
+
// message) before this node's handshake: there is no evictor id to
|
|
407
|
+
// broadcast then, and the eviction stays local (#3031).
|
|
408
|
+
if (broadcast && this._nodeId !== null) {
|
|
409
|
+
this.publisher.sendNodeEvicted(this._nodeId, nodeId, reason);
|
|
358
410
|
}
|
|
359
411
|
if (this.countActiveNodes() < this.config.minimumNodes) {
|
|
360
412
|
global.kuzzle.state = kuzzleStateEnum_1.default.NOT_ENOUGH_NODES;
|
|
@@ -388,14 +440,15 @@ class ClusterNode {
|
|
|
388
440
|
const candidates = this.electShutdownCandidates(splits, idCards);
|
|
389
441
|
if (candidates.includes(this.nodeId)) {
|
|
390
442
|
this.logger.error("[CLUSTER] Network split detected. This node is outside the cluster: shutting down.");
|
|
391
|
-
|
|
443
|
+
// A failure, like `evictSelf`'s: exit non-zero (#2785)
|
|
444
|
+
void global.kuzzle.shutdown(1);
|
|
392
445
|
return;
|
|
393
446
|
}
|
|
394
447
|
}
|
|
395
448
|
catch (err) {
|
|
396
449
|
this.logger.error("[CLUSTER] Unexpected exception caught during a cluster consistency check. Shutting down...");
|
|
397
450
|
this.logger.error(err instanceof Error ? err.stack : (0, util_1.inspect)(err));
|
|
398
|
-
global.kuzzle.shutdown();
|
|
451
|
+
void global.kuzzle.shutdown(1);
|
|
399
452
|
}
|
|
400
453
|
}
|
|
401
454
|
/**
|
|
@@ -523,8 +576,8 @@ class ClusterNode {
|
|
|
523
576
|
*
|
|
524
577
|
* Extracted from `handshake()` unchanged.
|
|
525
578
|
*
|
|
526
|
-
* @
|
|
527
|
-
*
|
|
579
|
+
* @throws {cluster.join.full_state_unavailable} on the second failure: the
|
|
580
|
+
* cluster is split
|
|
528
581
|
*/
|
|
529
582
|
async retryFullState(retried) {
|
|
530
583
|
// Uh oh... no node was able to give us the full state.
|
|
@@ -533,9 +586,7 @@ class ClusterNode {
|
|
|
533
586
|
// means we're probably facing a network split, and we must then shut
|
|
534
587
|
// down.
|
|
535
588
|
if (retried) {
|
|
536
|
-
|
|
537
|
-
global.kuzzle.shutdown();
|
|
538
|
-
return false;
|
|
589
|
+
throw joinError.get("full_state_unavailable");
|
|
539
590
|
}
|
|
540
591
|
// Disposes all subscribers
|
|
541
592
|
for (const subscriber of this.remoteNodes.values()) {
|
|
@@ -546,81 +597,110 @@ class ClusterNode {
|
|
|
546
597
|
const retryDelay = this.heartbeatDelay * 1.5;
|
|
547
598
|
this.logger.warn(`[CLUSTER] Unable to connect to discovered cluster nodes. Retrying in ${retryDelay}ms...`);
|
|
548
599
|
await bluebird_1.default.delay(retryDelay);
|
|
549
|
-
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* The ID cards of the other nodes, none of which has this node's IP.
|
|
603
|
+
*
|
|
604
|
+
* A card with this IP may be the stale one of a previous run of this node
|
|
605
|
+
* (a process killed, a container restarted at once): it expires within a
|
|
606
|
+
* TTL, so it is only a real duplicate if it is still there after that.
|
|
607
|
+
*
|
|
608
|
+
* @throws {cluster.join.duplicate_ip} if it is
|
|
609
|
+
*/
|
|
610
|
+
async discoverNodes() {
|
|
611
|
+
const nodes = await this.idCardHandler.getRemoteIdCards();
|
|
612
|
+
if (!nodes.some((node) => node.ip === this.ip)) {
|
|
613
|
+
return nodes;
|
|
614
|
+
}
|
|
615
|
+
// A heartbeat more than the TTL: the card's last renewal may be in flight
|
|
616
|
+
const delay = this.idCardHandler.ttl + this.heartbeatDelay;
|
|
617
|
+
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...`);
|
|
618
|
+
await bluebird_1.default.delay(delay);
|
|
619
|
+
const remaining = await this.idCardHandler.getRemoteIdCards();
|
|
620
|
+
const duplicate = remaining.find((node) => node.ip === this.ip);
|
|
621
|
+
if (duplicate) {
|
|
622
|
+
throw joinError.get("duplicate_ip", this.ip, duplicate.id);
|
|
623
|
+
}
|
|
624
|
+
return remaining;
|
|
550
625
|
}
|
|
551
626
|
/**
|
|
552
627
|
* Discovers other active nodes from the cluster and, if other nodes exist,
|
|
553
628
|
* starts a handshake procedure to sync this node and to make it able to
|
|
554
629
|
* handle new client requests
|
|
555
630
|
*
|
|
556
|
-
*
|
|
631
|
+
* Rejects with a `cluster.join.*` error, and shuts the node down, when it
|
|
632
|
+
* cannot join: the node must not go on starting as if it had (a duplicate
|
|
633
|
+
* IP, a split cluster, `joinTimeout` reached).
|
|
557
634
|
*/
|
|
558
635
|
async handshake() {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
636
|
+
let timedOut = false;
|
|
637
|
+
let handshakeTimeout;
|
|
638
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
639
|
+
handshakeTimeout = setTimeout(() => {
|
|
640
|
+
timedOut = true;
|
|
641
|
+
reject(joinError.get("timeout", this.config.joinTimeout));
|
|
642
|
+
}, this.config.joinTimeout);
|
|
643
|
+
});
|
|
563
644
|
// No retry limit: the handshakes are taken one at a time, and the
|
|
564
645
|
// `joinTimeout` above is what gives up on a cluster that never lets this
|
|
565
646
|
// node in.
|
|
647
|
+
const join = (0, distributedLock_1.withLock)("clusterHandshake", async () => {
|
|
648
|
+
// Given up on while waiting for the lock: do not join after all
|
|
649
|
+
if (timedOut) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
// Create the ID Key AFTER the handshake lock is actually taken,
|
|
653
|
+
// to prevent race conditions (other nodes attempting to connect to this
|
|
654
|
+
// node while it's still initializing)
|
|
655
|
+
await this.idCardHandler.createIdCard();
|
|
656
|
+
debug("[CLUSTER] ID Card created");
|
|
657
|
+
const nodeId = this.idCardHandler.nodeId;
|
|
658
|
+
(0, assert_1.default)(nodeId !== null, "[CLUSTER] createIdCard() returned without reserving a node id");
|
|
659
|
+
this.nodeId = nodeId;
|
|
660
|
+
await this.startHeartbeat(); // NOSONAR: TD-26
|
|
661
|
+
debug("[CLUSTER] Start heartbeat");
|
|
662
|
+
let retried = false;
|
|
663
|
+
let fullState;
|
|
664
|
+
let nodes;
|
|
665
|
+
debug("[CLUSTER] Start retrieving full state..");
|
|
666
|
+
do {
|
|
667
|
+
nodes = await this.discoverNodes();
|
|
668
|
+
debug("[CLUSTER] %s remote nodes discovered", nodes.length);
|
|
669
|
+
// No other nodes detected = no handshake required
|
|
670
|
+
if (nodes.length === 0) {
|
|
671
|
+
this.trackActivity(this.nodeId, this.ip, nodeActivityEnum.ADDED);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
await this.subscribeToNodes(nodes);
|
|
675
|
+
debug("[CLUSTER] Successfully subscribed to nodes");
|
|
676
|
+
fullState = await this.command.getFullState(nodes);
|
|
677
|
+
if (fullState === null) {
|
|
678
|
+
// Sequential by design: the retry waits for a heartbeat round
|
|
679
|
+
await this.retryFullState(retried); // NOSONAR
|
|
680
|
+
retried = true;
|
|
681
|
+
}
|
|
682
|
+
} while (fullState === null);
|
|
683
|
+
debug("[CLUSTER] Fullstate retrieved, loading into node..");
|
|
684
|
+
await this.fullState.loadFullState(fullState); // NOSONAR: TD-26
|
|
685
|
+
this.activity = fullState.activity ?? this.activity;
|
|
686
|
+
debug("[CLUSTER] Fullstate loaded.");
|
|
687
|
+
const handshakeResponses = await this.command.broadcastHandshake(nodes);
|
|
688
|
+
debug("[CLUSTER] Successful handshakes with other nodes.");
|
|
689
|
+
await this.syncWithPeers(handshakeResponses, fullState);
|
|
690
|
+
// `createIdCard()` adopts `global.nodeId`, so these are normally the same
|
|
691
|
+
// string and the line reads as one name (TD-59, #2764). They still differ
|
|
692
|
+
// in the two cases the adoption cannot cover — a stale IdCard holding the
|
|
693
|
+
// key, and a Kuzzle running without a Backend — and those are exactly the
|
|
694
|
+
// cases where a reader needs to be told, so the line says both.
|
|
695
|
+
this.logger.info(global.nodeId && global.nodeId !== this.nodeId
|
|
696
|
+
? `[CLUSTER] Successfully joined the cluster as "${this.nodeId}" (process node id: ${global.nodeId}).`
|
|
697
|
+
: `[CLUSTER] Successfully joined the cluster as "${this.nodeId}".`);
|
|
698
|
+
this.trackActivity(this.nodeId, this.ip, nodeActivityEnum.ADDED);
|
|
699
|
+
}, { retryAttempts: Number.POSITIVE_INFINITY });
|
|
566
700
|
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 });
|
|
701
|
+
// A timeout does not stop `join`: the node it settles later is shutting
|
|
702
|
+
// down already
|
|
703
|
+
await Promise.race([join, timeout]);
|
|
624
704
|
}
|
|
625
705
|
catch (error) {
|
|
626
706
|
// The handshake cannot be stopped where it is: it may still introduce
|
|
@@ -628,7 +708,10 @@ class ClusterNode {
|
|
|
628
708
|
// half-joined, whatever the application does with the failed startup.
|
|
629
709
|
if (error instanceof distributedLock_1.MutexLockLostError) {
|
|
630
710
|
this.logger.error("[CLUSTER] Lost the handshake lock while joining the cluster. Shutting down.");
|
|
631
|
-
|
|
711
|
+
}
|
|
712
|
+
if (error instanceof distributedLock_1.MutexLockLostError ||
|
|
713
|
+
(error instanceof kuzzleError_1.KuzzleError && error.id.startsWith("cluster.join."))) {
|
|
714
|
+
void global.kuzzle.shutdown(1);
|
|
632
715
|
}
|
|
633
716
|
throw error;
|
|
634
717
|
}
|
|
@@ -15,6 +15,8 @@ type SubscribingNode = {
|
|
|
15
15
|
config: IKuzzleConfiguration["cluster"];
|
|
16
16
|
heartbeatDelay: number;
|
|
17
17
|
nodeId: string;
|
|
18
|
+
/** Whether `nodeId` can be read: false until the handshake sets it. */
|
|
19
|
+
hasNodeId: boolean;
|
|
18
20
|
fullState: State;
|
|
19
21
|
eventEmitter: {
|
|
20
22
|
emit(event: string, payload: unknown): void;
|
|
@@ -383,6 +383,12 @@ class ClusterSubscriber {
|
|
|
383
383
|
* @param message - decoded NodeEvicted protobuf message
|
|
384
384
|
*/
|
|
385
385
|
async handleNodeEviction(message) {
|
|
386
|
+
// Before the handshake this node has no id to compare, and is not part of
|
|
387
|
+
// the cluster the eviction is about: the message is ignored (#3031).
|
|
388
|
+
if (!this.localNode.hasNodeId) {
|
|
389
|
+
debug("[CLUSTER] Eviction of node %s by %s ignored: the handshake has not run", message.nodeId, message.evictor);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
386
392
|
if (message.nodeId === this.localNode.nodeId) {
|
|
387
393
|
this.logger.error(`[CLUSTER] Node evicted by ${message.evictor}. Reason: ${message.reason}`);
|
|
388
394
|
// A failure, like `evictSelf`'s: exit non-zero (#2785).
|
|
@@ -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 };
|