fix(bbb-html5): customHeartbeat would not close stale sessions, + (#19017)

* fix(bbb-html5): customHeartbeat would not close stale sessions, +

The [disabled by default] custom heartbeat included in Meteor's server
does not end connections when they are considered unhealthy/stale, which
deviates a bit from the default implementation. See:
https://github.com/bigbluebutton/bigbluebutton/pull/11486.

This commit includes a call to the default heartbeat termination timeout
so sockets are correctly cleaned up when the custom heartbeat is
activated. It also adds a customHeartbeatUseDataFrames config to allow
controlling whether the custom heartbeat should use WS data frames as
valid heartbeats as well - this should only be useful for
testing/debugging purposes and the default behavior (true) is
maintained.

As a side note: this change spun off from an investigation where some
problematic networks were triggering periodic client re-connects due to
the default heartbeat failing. Investigation points to the control
frames being put alongside fragmented WS data frames and the server side
failing to recognize the former - which means pong frames would be missed and
the health check would fail. Since the default heartbeat _does not_
account for data frame traffic (eg DDP payloads), it would shut down the
client's WS even though it was healthy.
The custom heartbeat _does_ account for data frames, which mitigates
that scenario and prevents unecessary reconnections.

* fix(bbb-html5): frontend crash due to undefined vars in customHeartbeat 

Meteor frontends may crash when customHeartbeat is enabled
due to an undefined access in the heartbeat`s logger.

Add optional chaining to the session props access so it won`t crash and tune down some log levels around that area.
This commit is contained in:
Paulo Lanzarin 2024-01-22 13:10:41 -03:00 committed by GitHub
parent 262cd0b735
commit e1dc4b55e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 44 additions and 30 deletions

View File

@ -66,40 +66,51 @@ Meteor.startup(() => {
}, healthCheckInterval);
}
const { customHeartbeat } = APP_CONFIG;
const { customHeartbeat, customHeartbeatUseDataFrames } = APP_CONFIG;
if (customHeartbeat) {
Logger.warn('Custom heartbeat functions are enabled');
// https://github.com/sockjs/sockjs-node/blob/1ef08901f045aae7b4df0f91ef598d7a11e82897/lib/transport/websocket.js#L74-L82
const newHeartbeat = function heartbeat() {
const currentTime = new Date().getTime();
const heartbeatFactory = function ({ heartbeatTimeoutCallback }) {
return function () {
const currentTime = new Date().getTime();
// Skipping heartbeat, because websocket is sending data
if (currentTime - this.ws.lastSentFrameTimestamp < 10000) {
try {
Logger.info('Skipping heartbeat, because websocket is sending data', {
currentTime,
lastSentFrameTimestamp: this.ws.lastSentFrameTimestamp,
userId: this.session.connection._meteorSession.userId,
});
return;
} catch (err) {
Logger.error(`Skipping heartbeat error: ${err}`);
}
}
const supportsHeartbeats = this.ws.ping(null, () => clearTimeout(this.hto_ref));
if (supportsHeartbeats) {
this.hto_ref = setTimeout(() => {
try {
Logger.info('Heartbeat timeout', { userId: this.session.connection._meteorSession.userId, sentAt: currentTime, now: new Date().getTime() });
} catch (err) {
Logger.error(`Heartbeat timeout error: ${err}`);
if (customHeartbeatUseDataFrames) {
// Skipping heartbeat, because websocket is sending data
if (currentTime - this.ws.lastSentFrameTimestamp < 10000) {
try {
Logger.debug('Skipping heartbeat, because websocket is sending data', {
currentTime,
lastSentFrameTimestamp: this.ws.lastSentFrameTimestamp,
userId: this.session?.connection?._meteorSession?.userId,
});
return;
} catch (err) {
Logger.error(`Skipping heartbeat error: ${err}`);
}
}
}, Meteor.server.options.heartbeatTimeout);
} else {
Logger.error('Unexpected error supportsHeartbeats=false');
}
}
const supportsHeartbeats = this.ws.ping(null, () => {
clearTimeout(this.hto_ref);
});
if (supportsHeartbeats) {
this.hto_ref = setTimeout(() => {
try {
Logger.warn('Heartbeat timeout', { userId: this.session?.connection?._meteorSession?.userId, sentAt: currentTime, now: new Date().getTime() });
} catch (err) {
Logger.error(`Heartbeat timeout error: ${err}`);
} finally {
if (typeof heartbeatTimeoutCallback === 'function') {
heartbeatTimeoutCallback();
}
}
}, Meteor.server.options.heartbeatTimeout);
} else {
Logger.error('Unexpected error supportsHeartbeats=false');
}
};
};
// https://github.com/davhani/hagty/blob/6a5c78e9ae5a5e4ade03e747fb4cc8ea2df4be0c/faye-websocket/lib/faye/websocket/api.js#L84-L88
@ -134,8 +145,10 @@ Meteor.startup(() => {
}
recv.ws.meteorHeartbeat = session.heartbeat;
recv.__proto__.heartbeat = newHeartbeat;
recv.ws.__proto__.send = newSend;
recv.heartbeat = heartbeatFactory({
heartbeatTimeoutCallback: recv.heartbeat_cb
});
recv.ws.send = newSend;
session.bbbFixApplied = true;
}
}, 5000);

View File

@ -136,6 +136,7 @@ public:
breakoutRoomLimit: 16
# https://github.com/bigbluebutton/bigbluebutton/pull/10826
customHeartbeat: false
customHeartbeatUseDataFrames: true
showAllAvailableLocales: true
# Show "Audio Filters for Microphone" option in settings menu.
# When set to true, users are able to enable/disable microphone constraints,