kuzzle 2.20.3 → 2.21.1

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.
@@ -270,6 +270,16 @@ class ClusterPublisher {
270
270
  return this.send("NodeShutdown", { nodeId });
271
271
  }
272
272
 
273
+ /**
274
+ * Publishes an event about the node being in debug mode
275
+ * @param {bool} evictionPrevented
276
+ *
277
+ * @returns {Long} ID of the message sent
278
+ */
279
+ sendNodePreventEviction(evictionPrevented) {
280
+ return this.send("NodePreventEviction", { evictionPrevented });
281
+ }
282
+
273
283
  /**
274
284
  * Publishes an event about a node being evicted
275
285
  *
@@ -55,6 +55,8 @@ class ClusterSubscriber {
55
55
  this.remoteNodeIP = remoteNodeIP;
56
56
  this.remoteNodeAddress = `tcp://${remoteNodeIP}:${this.localNode.config.ports.sync}`;
57
57
  this.remoteNodeId = remoteNodeId;
58
+ // Used in debug mode when the node might be slower
59
+ this.remoteNodeEvictionPrevented = false;
58
60
  this.socket = null;
59
61
  this.protoroot = null;
60
62
 
@@ -87,6 +89,7 @@ class ClusterSubscriber {
87
89
  NewAuthStrategy: this.handleNewAuthStrategy,
88
90
  NewRealtimeRoom: this.handleNewRealtimeRoom,
89
91
  NodeEvicted: this.handleNodeEviction,
92
+ NodePreventEviction: this.handleNodePreventEviction,
90
93
  NodeShutdown: this.handleNodeShutdown,
91
94
  RefreshIndexCache: this.handleRefreshIndexCache,
92
95
  RefreshValidators: this.handleRefreshValidators,
@@ -229,6 +232,10 @@ class ClusterSubscriber {
229
232
  }
230
233
  }
231
234
 
235
+ async handleNodePreventEviction(message) {
236
+ this.remoteNodeEvictionPrevented = message.evictionPrevented;
237
+ }
238
+
232
239
  /**
233
240
  * Handles a heartbeat from the remote node
234
241
  *
@@ -673,6 +680,14 @@ class ClusterSubscriber {
673
680
  * to recover, otherwise we evict it from the cluster.
674
681
  */
675
682
  async checkHeartbeat() {
683
+ if (this.remoteNodeEvictionPrevented) {
684
+ // Fake the heartbeat while the node eviction prevention is enabled
685
+ // otherwise when the node eviction prevention is disabled
686
+ // the node will be evicted if it did not send a heartbeat before disabling the protection.
687
+ this.lastHeartbeat = Date.now();
688
+ return;
689
+ }
690
+
676
691
  if (this.state === stateEnum.EVICTED) {
677
692
  return;
678
693
  }
@@ -1,11 +1,9 @@
1
1
  "use strict";
2
2
 
3
- const { isMainThread, parentPort } = require("worker_threads");
4
3
  const Redis = require("../../service/cache/redis");
5
4
 
6
5
  class IDCardRenewer {
7
6
  constructor() {
8
- this.parentPort = parentPort;
9
7
  this.redis = null;
10
8
  this.refreshTimer = null;
11
9
  this.nodeIdKey = null;
@@ -31,6 +29,10 @@ class IDCardRenewer {
31
29
  const redisConf = config.redis || {};
32
30
  await this.initRedis(redisConf.config, redisConf.name);
33
31
  } catch (error) {
32
+ // eslint-disable-next-line no-console
33
+ console.error(
34
+ `Failed to connect to redis, could not refresh ID card: ${error.message}`
35
+ );
34
36
  this.parentPort.postMessage({
35
37
  error: `Failed to connect to redis, could not refresh ID card: ${error.message}`,
36
38
  });
@@ -50,7 +52,7 @@ class IDCardRenewer {
50
52
  }
51
53
 
52
54
  // Notify that the worker is running and updating the ID Card
53
- this.parentPort.postMessage({ initialized: true });
55
+ process.send({ initialized: true });
54
56
  }
55
57
 
56
58
  async initRedis(config, name) {
@@ -73,13 +75,13 @@ class IDCardRenewer {
73
75
  // => this node is too slow, we need to remove it from the cluster
74
76
  if (refreshed === 0) {
75
77
  await this.dispose();
76
- this.parentPort.postMessage({
78
+ process.send({
77
79
  error: "Node too slow: ID card expired",
78
80
  });
79
81
  }
80
82
  } catch (error) {
81
83
  await this.dispose();
82
- this.parentPort.postMessage({
84
+ process.send({
83
85
  error: `Failed to refresh ID Card: ${error.message}`,
84
86
  });
85
87
  }
@@ -112,26 +114,25 @@ class IDCardRenewer {
112
114
  }
113
115
  }
114
116
 
115
- if (!isMainThread) {
116
- const idCardRenewer = new IDCardRenewer();
117
-
118
- parentPort.on("message", async (message) => {
119
- if (message.action === "start") {
120
- // Simulate basic global Kuzzle Context
121
- global.kuzzle = { ...message.kuzzle };
122
- global.kuzzle.log = {
123
- debug: console.debug, // eslint-disable-line no-console
124
- error: console.error, // eslint-disable-line no-console
125
- info: console.info, // eslint-disable-line no-console
126
- warn: console.warn, // eslint-disable-line no-console
127
- };
128
- // Should never throw
129
- await idCardRenewer.init(message);
130
- } else if (message.action === "dispose") {
131
- // Should never throw
132
- await idCardRenewer.dispose();
133
- }
134
- });
135
- }
117
+ const idCardRenewer = new IDCardRenewer();
118
+
119
+ process.on("message", async (message) => {
120
+ if (message.action === "start") {
121
+ // Simulate basic global Kuzzle Context
122
+ global.kuzzle = { ...message.kuzzle };
123
+ global.kuzzle.log = {
124
+ debug: console.debug, // eslint-disable-line no-console
125
+ error: console.error, // eslint-disable-line no-console
126
+ info: console.info, // eslint-disable-line no-console
127
+ warn: console.warn, // eslint-disable-line no-console
128
+ };
129
+ // Should never throw
130
+ await idCardRenewer.init(message);
131
+ } else if (message.action === "dispose") {
132
+ // Should never throw
133
+ await idCardRenewer.dispose();
134
+ process.exit(0);
135
+ }
136
+ });
136
137
 
137
138
  module.exports = { IDCardRenewer };
@@ -0,0 +1,46 @@
1
+ import { JSONObject } from "kuzzle-sdk";
2
+ export declare class KuzzleDebugger {
3
+ private inspector;
4
+ private debuggerStatus;
5
+ /**
6
+ * Map<eventName, Set<connectionId>>
7
+ */
8
+ private events;
9
+ private httpWsProtocol?;
10
+ init(): Promise<void>;
11
+ registerAsks(): Promise<void>;
12
+ /**
13
+ * Connect the debugger
14
+ */
15
+ enable(): Promise<void>;
16
+ /**
17
+ * Disconnect the debugger and clears all the events listeners
18
+ */
19
+ disable(): Promise<void>;
20
+ /**
21
+ * Trigger action from debugger directly following the Chrome Debug Protocol
22
+ * See: https://chromedevtools.github.io/devtools-protocol/v8/
23
+ */
24
+ post(method: string, params?: JSONObject): Promise<JSONObject>;
25
+ /**
26
+ * Make the websocket connection listen and receive events from Chrome Debug Protocol
27
+ * See events from: https://chromedevtools.github.io/devtools-protocol/v8/
28
+ */
29
+ addListener(event: string, connectionId: string): Promise<void>;
30
+ /**
31
+ * Remove the websocket connection from the events" listeners
32
+ */
33
+ removeListener(event: string, connectionId: string): Promise<void>;
34
+ /**
35
+ * Execute a method using the Chrome Debug Protocol
36
+ * @param method Chrome Debug Protocol method to execute
37
+ * @param params
38
+ * @returns
39
+ */
40
+ private inspectorPost;
41
+ /**
42
+ * Sends a direct notification to a websocket connection without having to listen to a specific room
43
+ */
44
+ private notifyConnection;
45
+ private notifyGlobalListeners;
46
+ }
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.KuzzleDebugger = void 0;
30
+ const inspector_1 = __importDefault(require("inspector"));
31
+ const kerror = __importStar(require("../../kerror"));
32
+ const DEBUGGER_EVENT = "kuzzle-debugger-event";
33
+ class KuzzleDebugger {
34
+ constructor() {
35
+ this.debuggerStatus = false;
36
+ /**
37
+ * Map<eventName, Set<connectionId>>
38
+ */
39
+ this.events = new Map();
40
+ }
41
+ async init() {
42
+ this.httpWsProtocol = global.kuzzle.entryPoint.protocols.get("websocket");
43
+ this.inspector = new inspector_1.default.Session();
44
+ // Remove connection id from the list of listeners for each event
45
+ global.kuzzle.on("connection:remove", (connectionId) => {
46
+ if (!this.debuggerStatus) {
47
+ return;
48
+ }
49
+ for (const listener of this.events.values()) {
50
+ listener.delete(connectionId);
51
+ }
52
+ });
53
+ this.inspector.on("inspectorNotification", async (payload) => {
54
+ if (!this.debuggerStatus) {
55
+ return;
56
+ }
57
+ await this.notifyGlobalListeners(payload.method, payload);
58
+ const listeners = this.events.get(payload.method);
59
+ if (!listeners) {
60
+ return;
61
+ }
62
+ const promises = [];
63
+ for (const connectionId of listeners) {
64
+ promises.push(this.notifyConnection(connectionId, DEBUGGER_EVENT, {
65
+ event: payload.method,
66
+ result: payload,
67
+ }));
68
+ }
69
+ // No need to catch, notify is already try-catched
70
+ await Promise.all(promises);
71
+ });
72
+ await this.registerAsks();
73
+ }
74
+ async registerAsks() {
75
+ global.kuzzle.onAsk("core:debugger:enable", () => this.enable());
76
+ global.kuzzle.onAsk("core:debugger:disable", () => this.disable());
77
+ global.kuzzle.onAsk("core:debugger:post", (method, params) => this.post(method, params));
78
+ global.kuzzle.onAsk("core:debugger:isEnabled", () => this.debuggerStatus);
79
+ global.kuzzle.onAsk("core:debugger:removeListener", (event, connectionId) => this.removeListener(event, connectionId));
80
+ global.kuzzle.onAsk("core:debugger:addListener", (event, connectionId) => this.addListener(event, connectionId));
81
+ }
82
+ /**
83
+ * Connect the debugger
84
+ */
85
+ async enable() {
86
+ if (this.debuggerStatus) {
87
+ return;
88
+ }
89
+ this.inspector.connect();
90
+ this.debuggerStatus = true;
91
+ await global.kuzzle.ask("cluster:node:preventEviction", true);
92
+ }
93
+ /**
94
+ * Disconnect the debugger and clears all the events listeners
95
+ */
96
+ async disable() {
97
+ if (!this.debuggerStatus) {
98
+ return;
99
+ }
100
+ this.inspector.disconnect();
101
+ this.debuggerStatus = false;
102
+ await global.kuzzle.ask("cluster:node:preventEviction", false);
103
+ // Disable debug mode for all connected sockets that still have listeners
104
+ if (this.httpWsProtocol) {
105
+ for (const eventName of this.events.keys()) {
106
+ for (const connectionId of this.events.get(eventName)) {
107
+ const socket = this.httpWsProtocol.socketByConnectionId.get(connectionId);
108
+ if (socket) {
109
+ socket.internal.debugSession = false;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ this.events.clear();
115
+ }
116
+ /**
117
+ * Trigger action from debugger directly following the Chrome Debug Protocol
118
+ * See: https://chromedevtools.github.io/devtools-protocol/v8/
119
+ */
120
+ async post(method, params = {}) {
121
+ if (!this.debuggerStatus) {
122
+ throw kerror.get("core", "debugger", "not_enabled");
123
+ }
124
+ // Always disable report progress because this parameter causes a segfault.
125
+ // The reason this happens is because the inspector is running inside the same thread
126
+ // as the Kuzzle Process and reportProgress forces the inspector to call function in the JS Heap
127
+ // while it is being inspected by the HeapProfiler, which causes a segfault.
128
+ // See: https://github.com/nodejs/node/issues/44634
129
+ if (params.reportProgress) {
130
+ // We need to send a fake HeapProfiler.reportHeapSnapshotProgress event
131
+ // to the inspector to make Chrome think that the HeapProfiler is done
132
+ // otherwise, even though the Chrome Inspector did receive the whole snapshot, it will not be parsed.
133
+ //
134
+ // Chrome inspector is waiting for a HeapProfiler.reportHeapSnapshotProgress event with the finished property set to true
135
+ // The `done` and `total` properties are only used to show a progress bar, so there are not important.
136
+ // Sending this event before the HeapProfiler.addHeapSnapshotChunk event will not cause any problem,
137
+ // in fact, Chrome always do that when taking a snapshot, it receives the HeapProfiler.reportHeapSnapshotProgress event
138
+ // before the HeapProfiler.addHeapSnapshotChunk event.
139
+ // So this will have no impact and when receiving the HeapProfiler.addHeapSnapshotChunk event, Chrome will wait to receive
140
+ // a complete snapshot before parsing it if it has received the HeapProfiler.reportHeapSnapshotProgress event with the finished property set to true before.
141
+ this.inspector.emit("inspectorNotification", {
142
+ method: "HeapProfiler.reportHeapSnapshotProgress",
143
+ params: {
144
+ done: 0,
145
+ finished: true,
146
+ total: 0,
147
+ },
148
+ });
149
+ params.reportProgress = false;
150
+ }
151
+ return this.inspectorPost(method, params);
152
+ }
153
+ /**
154
+ * Make the websocket connection listen and receive events from Chrome Debug Protocol
155
+ * See events from: https://chromedevtools.github.io/devtools-protocol/v8/
156
+ */
157
+ async addListener(event, connectionId) {
158
+ if (!this.debuggerStatus) {
159
+ throw kerror.get("core", "debugger", "not_enabled");
160
+ }
161
+ if (this.httpWsProtocol) {
162
+ const socket = this.httpWsProtocol.socketByConnectionId.get(connectionId);
163
+ if (socket) {
164
+ /**
165
+ * Mark the socket as a debugging socket
166
+ * this will bypass some limitations like the max pressure buffer size,
167
+ * which could end the connection when the debugger is sending a lot of data.
168
+ */
169
+ socket.internal.debugSession = true;
170
+ }
171
+ }
172
+ let listeners = this.events.get(event);
173
+ if (!listeners) {
174
+ listeners = new Set();
175
+ this.events.set(event, listeners);
176
+ }
177
+ listeners.add(connectionId);
178
+ }
179
+ /**
180
+ * Remove the websocket connection from the events" listeners
181
+ */
182
+ async removeListener(event, connectionId) {
183
+ if (!this.debuggerStatus) {
184
+ throw kerror.get("core", "debugger", "not_enabled");
185
+ }
186
+ const listeners = this.events.get(event);
187
+ if (listeners) {
188
+ listeners.delete(connectionId);
189
+ }
190
+ if (!this.httpWsProtocol) {
191
+ return;
192
+ }
193
+ const socket = this.httpWsProtocol.socketByConnectionId.get(connectionId);
194
+ if (!socket) {
195
+ return;
196
+ }
197
+ let removeDebugSessionMarker = true;
198
+ /**
199
+ * If the connection doesn't listen to any other events
200
+ * we can remove the debugSession marker
201
+ */
202
+ for (const eventName of this.events.keys()) {
203
+ const eventListener = this.events.get(eventName);
204
+ if (eventListener && eventListener.has(connectionId)) {
205
+ removeDebugSessionMarker = false;
206
+ break;
207
+ }
208
+ }
209
+ if (removeDebugSessionMarker) {
210
+ socket.internal.debugSession = false;
211
+ }
212
+ }
213
+ /**
214
+ * Execute a method using the Chrome Debug Protocol
215
+ * @param method Chrome Debug Protocol method to execute
216
+ * @param params
217
+ * @returns
218
+ */
219
+ async inspectorPost(method, params) {
220
+ if (!this.debuggerStatus) {
221
+ throw kerror.get("core", "debugger", "not_enabled");
222
+ }
223
+ let resolve;
224
+ const promise = new Promise((res) => {
225
+ resolve = res;
226
+ });
227
+ this.inspector.post(method, params, (err, res) => {
228
+ if (err) {
229
+ resolve({
230
+ error: JSON.stringify(Object.getOwnPropertyDescriptors(err)),
231
+ });
232
+ }
233
+ else {
234
+ resolve(res);
235
+ }
236
+ });
237
+ return promise;
238
+ }
239
+ /**
240
+ * Sends a direct notification to a websocket connection without having to listen to a specific room
241
+ */
242
+ async notifyConnection(connectionId, event, payload) {
243
+ global.kuzzle.entryPoint._notify({
244
+ channels: [event],
245
+ connectionId,
246
+ payload,
247
+ });
248
+ }
249
+ async notifyGlobalListeners(event, payload) {
250
+ const listeners = this.events.get("*");
251
+ if (!listeners) {
252
+ return;
253
+ }
254
+ const promises = [];
255
+ for (const connectionId of listeners) {
256
+ promises.push(this.notifyConnection(connectionId, DEBUGGER_EVENT, {
257
+ event,
258
+ result: payload,
259
+ }));
260
+ }
261
+ // No need to catch, notify is already try-catched
262
+ await Promise.all(promises);
263
+ }
264
+ }
265
+ exports.KuzzleDebugger = KuzzleDebugger;
266
+ //# sourceMappingURL=kuzzleDebugger.js.map
@@ -31,10 +31,11 @@ const uuid = require("uuid");
31
31
  * @param {object} [headers] - Optional extra key-value object. I.e., for http, will receive the request headers
32
32
  */
33
33
  class ClientConnection {
34
- constructor(protocol, ips, headers = null) {
34
+ constructor(protocol, ips, headers = null, internal = null) {
35
35
  this.id = uuid.v4();
36
36
  this.protocol = protocol;
37
37
  this.headers = {};
38
+ this.internal = {};
38
39
 
39
40
  if (!Array.isArray(ips)) {
40
41
  throw new TypeError(`Expected ips to be an Array, got ${typeof ips}`);
@@ -45,6 +46,10 @@ class ClientConnection {
45
46
  this.headers = headers;
46
47
  }
47
48
 
49
+ if (isPlainObject(internal)) {
50
+ this.internal = internal;
51
+ }
52
+
48
53
  Object.freeze(this);
49
54
  }
50
55
  }
@@ -282,6 +282,7 @@ class HttpWsProtocol extends Protocol {
282
282
  res.upgrade(
283
283
  {
284
284
  headers,
285
+ internal: {},
285
286
  },
286
287
  req.getHeader("sec-websocket-key"),
287
288
  req.getHeader("sec-websocket-protocol"),
@@ -292,7 +293,12 @@ class HttpWsProtocol extends Protocol {
292
293
 
293
294
  wsOnOpenHandler(socket) {
294
295
  const ip = Buffer.from(socket.getRemoteAddressAsText()).toString();
295
- const connection = new ClientConnection(this.name, [ip], socket.headers);
296
+ const connection = new ClientConnection(
297
+ this.name,
298
+ [ip],
299
+ socket.headers,
300
+ socket.internal
301
+ );
296
302
 
297
303
  this.entryPoint.newConnection(connection);
298
304
  this.connectionBySocket.set(socket, connection);
@@ -457,8 +463,17 @@ class HttpWsProtocol extends Protocol {
457
463
  const buffer = this.backpressureBuffer.get(socket);
458
464
  buffer.push(payload);
459
465
 
460
- // Client socket too slow: we need to close it
461
- if (buffer.length > WS_BACKPRESSURE_BUFFER_MAX_LENGTH) {
466
+ /**
467
+ * Client socket too slow: we need to close it
468
+ *
469
+ * If the socket is marked as a debugSession, we don't close it
470
+ * the debugger might send a lot of messages and we don't want to
471
+ * loose the connection while debugging and loose important information.
472
+ */
473
+ if (
474
+ !socket.internal.debugSession &&
475
+ buffer.length > WS_BACKPRESSURE_BUFFER_MAX_LENGTH
476
+ ) {
462
477
  socket.end(WS_FORCED_TERMINATION_CODE, WS_BACKPRESSURE_MESSAGE);
463
478
  }
464
479
  }
@@ -228,7 +228,7 @@ class DumpGenerator {
228
228
  while (dumps.length >= config.history.reports) {
229
229
  const dir = dumps.shift().path;
230
230
 
231
- fs.removeSync(dir);
231
+ fs.rmdirSync(dir, { recursive: true });
232
232
  }
233
233
 
234
234
  for (let i = 0; i < dumps.length - config.history.coredump; i++) {
@@ -80,6 +80,7 @@ const package_json_1 = require("../../package.json");
80
80
  const name_generator_1 = require("../util/name-generator");
81
81
  const openapi_1 = require("../api/openapi");
82
82
  const crypto_1 = require("../util/crypto");
83
+ const kuzzleDebugger_1 = require("../core/debug/kuzzleDebugger");
83
84
  exports.BACKEND_IMPORT_KEY = "backend:init:import";
84
85
  let _kuzzle = null;
85
86
  Reflect.defineProperty(global, "kuzzle", {
@@ -119,6 +120,7 @@ class Kuzzle extends kuzzleEventEmitter_1.default {
119
120
  this.dumpGenerator = new dumpGenerator_1.default();
120
121
  this.vault = null;
121
122
  this.asyncStore = new asyncStore_1.default();
123
+ this.debugger = new kuzzleDebugger_1.KuzzleDebugger();
122
124
  this.version = package_json_1.version;
123
125
  this.importTypes = {
124
126
  fixtures: this.importFixtures.bind(this),
@@ -164,6 +166,7 @@ class Kuzzle extends kuzzleEventEmitter_1.default {
164
166
  // must be initialized before plugins to allow API requests from plugins
165
167
  // before opening connections to external users
166
168
  await this.entryPoint.init();
169
+ await this.debugger.init();
167
170
  this.pluginsManager.application = application;
168
171
  const pluginImports = await this.pluginsManager.init(options.plugins);
169
172
  this.log.info(`[✔] Successfully loaded ${this.pluginsManager.loadedPlugins.length} plugins: ${this.pluginsManager.loadedPlugins.join(", ")}`);