Skip to content

Commit 3b87675

Browse files
authored
Merge pull request #2437 from kuzzleio/improve-node-eviction-prevention
Overall improvement of the Debug Controller
2 parents 3c80d89 + 1783d66 commit 3b87675

File tree

13 files changed

+192
-29
lines changed

13 files changed

+192
-29
lines changed

lib/api/controllers/debugController.ts

+41
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import { KuzzleRequest } from "../request";
2323
import { NativeController } from "./baseController";
2424
import * as kerror from "../../kerror";
25+
import get from "lodash/get";
2526

2627
/**
2728
* @class DebugController
@@ -49,13 +50,29 @@ export class DebugController extends NativeController {
4950
* Connect the debugger
5051
*/
5152
async enable() {
53+
if (!get(global.kuzzle.config, "security.debug.native_debug_protocol")) {
54+
throw kerror.get(
55+
"core",
56+
"debugger",
57+
"native_debug_protocol_usage_denied"
58+
);
59+
}
60+
5261
await global.kuzzle.ask("core:debugger:enable");
5362
}
5463

5564
/**
5665
* Disconnect the debugger and clears all the events listeners
5766
*/
5867
async disable() {
68+
if (!get(global.kuzzle.config, "security.debug.native_debug_protocol")) {
69+
throw kerror.get(
70+
"core",
71+
"debugger",
72+
"native_debug_protocol_usage_denied"
73+
);
74+
}
75+
5976
await global.kuzzle.ask("core:debugger:disable");
6077
}
6178

@@ -64,6 +81,14 @@ export class DebugController extends NativeController {
6481
* See: https://chromedevtools.github.io/devtools-protocol/v8/
6582
*/
6683
async post(request: KuzzleRequest) {
84+
if (!get(global.kuzzle.config, "security.debug.native_debug_protocol")) {
85+
throw kerror.get(
86+
"core",
87+
"debugger",
88+
"native_debug_protocol_usage_denied"
89+
);
90+
}
91+
6792
const method = request.getBodyString("method");
6893
const params = request.getBodyObject("params", {});
6994

@@ -75,6 +100,14 @@ export class DebugController extends NativeController {
75100
* See events from: https://chromedevtools.github.io/devtools-protocol/v8/
76101
*/
77102
async addListener(request: KuzzleRequest) {
103+
if (!get(global.kuzzle.config, "security.debug.native_debug_protocol")) {
104+
throw kerror.get(
105+
"core",
106+
"debugger",
107+
"native_debug_protocol_usage_denied"
108+
);
109+
}
110+
78111
if (request.context.connection.protocol !== "websocket") {
79112
throw kerror.get(
80113
"api",
@@ -98,6 +131,14 @@ export class DebugController extends NativeController {
98131
* Remove the websocket connection from the events' listeners
99132
*/
100133
async removeListener(request: KuzzleRequest) {
134+
if (!get(global.kuzzle.config, "security.debug.native_debug_protocol")) {
135+
throw kerror.get(
136+
"core",
137+
"debugger",
138+
"native_debug_protocol_usage_denied"
139+
);
140+
}
141+
101142
if (request.context.connection.protocol !== "websocket") {
102143
throw kerror.get(
103144
"api",

lib/api/funnel.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const debug = require("../util/debug")("kuzzle:funnel");
5151
const processError = kerror.wrap("api", "process");
5252
const { has } = require("../util/safeObject");
5353
const { HttpStream } = require("../types");
54+
const get = require("lodash/get");
5455

5556
// Actions of the auth controller that does not necessite to verify the token
5657
// when cookie auth is active
@@ -179,6 +180,12 @@ class Funnel {
179180
throw processError.get("not_enough_nodes");
180181
}
181182

183+
const isRequestFromDebugSession = get(
184+
request,
185+
"context.connection.misc.internal.debugSession",
186+
false
187+
);
188+
182189
if (this.overloaded) {
183190
const now = Date.now();
184191

@@ -226,7 +233,8 @@ class Funnel {
226233
*/
227234
if (
228235
this.pendingRequestsQueue.length >=
229-
global.kuzzle.config.limits.requestsBufferSize
236+
global.kuzzle.config.limits.requestsBufferSize &&
237+
!isRequestFromDebugSession
230238
) {
231239
const error = processError.get("overloaded");
232240
global.kuzzle.emit("log:error", error);
@@ -239,7 +247,13 @@ class Funnel {
239247
request.internalId,
240248
new PendingRequest(request, fn, context)
241249
);
242-
this.pendingRequestsQueue.push(request.internalId);
250+
251+
if (isRequestFromDebugSession) {
252+
// Push at the front to prioritize debug requests
253+
this.pendingRequestsQueue.unshift(request.internalId);
254+
} else {
255+
this.pendingRequestsQueue.push(request.internalId);
256+
}
243257

244258
if (!this.overloaded) {
245259
this.overloaded = true;

lib/cluster/node.js

+9
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,15 @@ class ClusterNode {
256256
*/
257257
preventEviction(evictionPrevented) {
258258
this.publisher.sendNodePreventEviction(evictionPrevented);
259+
// This node is subscribed to the other node and might not receive their heartbeat while debugging
260+
// so this node should not have the responsability of evicting others when his own eviction is prevented
261+
// when debugging.
262+
// Otherwise when recovering from a debug session, all the other nodes will be evicted.
263+
for (const subscriber of this.remoteNodes.values()) {
264+
subscriber.handleNodePreventEviction({
265+
evictionPrevented,
266+
});
267+
}
259268
}
260269

261270
/**

lib/cluster/subscriber.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,15 @@ class ClusterSubscriber {
680680
* to recover, otherwise we evict it from the cluster.
681681
*/
682682
async checkHeartbeat() {
683-
if (this.state === stateEnum.EVICTED || this.remoteNodeEvictionPrevented) {
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+
691+
if (this.state === stateEnum.EVICTED) {
684692
return;
685693
}
686694

lib/cluster/workers/IDCardRenewer.js

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class IDCardRenewer {
2929
const redisConf = config.redis || {};
3030
await this.initRedis(redisConf.config, redisConf.name);
3131
} 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+
);
3236
this.parentPort.postMessage({
3337
error: `Failed to connect to redis, could not refresh ID card: ${error.message}`,
3438
});

lib/core/debug/kuzzleDebugger.ts

+82-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Inspector from "inspector";
22
import * as kerror from "../../kerror";
33
import { JSONObject } from "kuzzle-sdk";
4-
import get from "lodash/get";
4+
import HttpWsProtocol from "../../core/network/protocols/httpwsProtocol";
55

66
const DEBUGGER_EVENT = "kuzzle-debugger-event";
77

@@ -15,7 +15,11 @@ export class KuzzleDebugger {
1515
*/
1616
private events = new Map<string, Set<string>>();
1717

18+
private httpWsProtocol?: HttpWsProtocol;
19+
1820
async init() {
21+
this.httpWsProtocol = global.kuzzle.entryPoint.protocols.get("websocket");
22+
1923
this.inspector = new Inspector.Session();
2024

2125
// Remove connection id from the list of listeners for each event
@@ -97,6 +101,20 @@ export class KuzzleDebugger {
97101
this.inspector.disconnect();
98102
this.debuggerStatus = false;
99103
await global.kuzzle.ask("cluster:node:preventEviction", false);
104+
105+
// Disable debug mode for all connected sockets that still have listeners
106+
if (this.httpWsProtocol) {
107+
for (const eventName of this.events.keys()) {
108+
for (const connectionId of this.events.get(eventName)) {
109+
const socket =
110+
this.httpWsProtocol.socketByConnectionId.get(connectionId);
111+
if (socket) {
112+
socket.internal.debugSession = false;
113+
}
114+
}
115+
}
116+
}
117+
100118
this.events.clear();
101119
}
102120

@@ -109,21 +127,33 @@ export class KuzzleDebugger {
109127
throw kerror.get("core", "debugger", "not_enabled");
110128
}
111129

112-
if (!get(global.kuzzle.config, "security.debug.native_debug_protocol")) {
113-
throw kerror.get(
114-
"core",
115-
"debugger",
116-
"native_debug_protocol_usage_denied"
117-
);
118-
}
119-
120-
// Always disable report progress because this params causes a segfault.
130+
// Always disable report progress because this parameter causes a segfault.
121131
// The reason this happens is because the inspector is running inside the same thread
122-
// as the Kuzzle Process and reportProgress forces the inspector to send events
123-
// to the main thread, while it is being inspected by the HeapProfiler, which causes javascript code
124-
// to be executed as the HeapProfiler is running, which causes a segfault.
132+
// as the Kuzzle Process and reportProgress forces the inspector to call function in the JS Heap
133+
// while it is being inspected by the HeapProfiler, which causes a segfault.
125134
// See: https://github.com/nodejs/node/issues/44634
126-
params.reportProgress = false;
135+
if (params.reportProgress) {
136+
// We need to send a fake HeapProfiler.reportHeapSnapshotProgress event
137+
// to the inspector to make Chrome think that the HeapProfiler is done
138+
// otherwise, even though the Chrome Inspector did receive the whole snapshot, it will not be parsed.
139+
//
140+
// Chrome inspector is waiting for a HeapProfiler.reportHeapSnapshotProgress event with the finished property set to true
141+
// The `done` and `total` properties are only used to show a progress bar, so there are not important.
142+
// Sending this event before the HeapProfiler.addHeapSnapshotChunk event will not cause any problem,
143+
// in fact, Chrome always do that when taking a snapshot, it receives the HeapProfiler.reportHeapSnapshotProgress event
144+
// before the HeapProfiler.addHeapSnapshotChunk event.
145+
// So this will have no impact and when receiving the HeapProfiler.addHeapSnapshotChunk event, Chrome will wait to receive
146+
// a complete snapshot before parsing it if it has received the HeapProfiler.reportHeapSnapshotProgress event with the finished property set to true before.
147+
this.inspector.emit("inspectorNotification", {
148+
method: "HeapProfiler.reportHeapSnapshotProgress",
149+
params: {
150+
done: 0,
151+
finished: true,
152+
total: 0,
153+
},
154+
});
155+
params.reportProgress = false;
156+
}
127157

128158
return this.inspectorPost(method, params);
129159
}
@@ -137,6 +167,18 @@ export class KuzzleDebugger {
137167
throw kerror.get("core", "debugger", "not_enabled");
138168
}
139169

170+
if (this.httpWsProtocol) {
171+
const socket = this.httpWsProtocol.socketByConnectionId.get(connectionId);
172+
if (socket) {
173+
/**
174+
* Mark the socket as a debugging socket
175+
* this will bypass some limitations like the max pressure buffer size,
176+
* which could end the connection when the debugger is sending a lot of data.
177+
*/
178+
socket.internal.debugSession = true;
179+
}
180+
}
181+
140182
let listeners = this.events.get(event);
141183
if (!listeners) {
142184
listeners = new Set();
@@ -159,6 +201,32 @@ export class KuzzleDebugger {
159201
if (listeners) {
160202
listeners.delete(connectionId);
161203
}
204+
205+
if (!this.httpWsProtocol) {
206+
return;
207+
}
208+
209+
const socket = this.httpWsProtocol.socketByConnectionId.get(connectionId);
210+
if (!socket) {
211+
return;
212+
}
213+
214+
let removeDebugSessionMarker = true;
215+
/**
216+
* If the connection doesn't listen to any other events
217+
* we can remove the debugSession marker
218+
*/
219+
for (const eventName of this.events.keys()) {
220+
const eventListener = this.events.get(eventName);
221+
if (eventListener && eventListener.has(connectionId)) {
222+
removeDebugSessionMarker = false;
223+
break;
224+
}
225+
}
226+
227+
if (removeDebugSessionMarker) {
228+
socket.internal.debugSession = false;
229+
}
162230
}
163231

164232
/**

lib/core/network/clientConnection.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ const uuid = require("uuid");
3131
* @param {object} [headers] - Optional extra key-value object. I.e., for http, will receive the request headers
3232
*/
3333
class ClientConnection {
34-
constructor(protocol, ips, headers = null) {
34+
constructor(protocol, ips, headers = null, internal = null) {
3535
this.id = uuid.v4();
3636
this.protocol = protocol;
3737
this.headers = {};
38+
this.internal = {};
3839

3940
if (!Array.isArray(ips)) {
4041
throw new TypeError(`Expected ips to be an Array, got ${typeof ips}`);
@@ -45,6 +46,10 @@ class ClientConnection {
4546
this.headers = headers;
4647
}
4748

49+
if (isPlainObject(internal)) {
50+
this.internal = internal;
51+
}
52+
4853
Object.freeze(this);
4954
}
5055
}

lib/core/network/protocols/httpwsProtocol.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ class HttpWsProtocol extends Protocol {
282282
res.upgrade(
283283
{
284284
headers,
285+
internal: {},
285286
},
286287
req.getHeader("sec-websocket-key"),
287288
req.getHeader("sec-websocket-protocol"),
@@ -292,7 +293,12 @@ class HttpWsProtocol extends Protocol {
292293

293294
wsOnOpenHandler(socket) {
294295
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+
);
296302

297303
this.entryPoint.newConnection(connection);
298304
this.connectionBySocket.set(socket, connection);
@@ -457,8 +463,17 @@ class HttpWsProtocol extends Protocol {
457463
const buffer = this.backpressureBuffer.get(socket);
458464
buffer.push(payload);
459465

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+
) {
462477
socket.end(WS_FORCED_TERMINATION_CODE, WS_BACKPRESSURE_MESSAGE);
463478
}
464479
}

lib/kuzzle/kuzzle.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ class Kuzzle extends KuzzleEventEmitter {
249249
seed: this.config.internal.hash.seed,
250250
});
251251

252-
await this.debugger.init();
253252
await new CacheEngine().init();
254253
await new StorageEngine().init();
255254
await new RealtimeModule().init();
@@ -279,6 +278,8 @@ class Kuzzle extends KuzzleEventEmitter {
279278
// before opening connections to external users
280279
await this.entryPoint.init();
281280

281+
await this.debugger.init();
282+
282283
this.pluginsManager.application = application;
283284
const pluginImports = await this.pluginsManager.init(options.plugins);
284285
this.log.info(

0 commit comments

Comments
 (0)