Skip to content

Commit

Permalink
Support for backend vs frontend nodejs instances bbb-html5
Browse files Browse the repository at this point in the history
  • Loading branch information
antobinary committed Feb 16, 2021
1 parent e2e0b78 commit f43560d
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender)
//==================================================================

case ValidateAuthTokenRespMsg.NAME =>
msgSender.send(fromAkkaAppsRedisChannel, json) // needed for cases when single nodejs process is running (like in development)
msgSender.send("from-akka-apps-frontend-redis-channel", json)

// Message duplicated for frontend and backend processes
Expand All @@ -134,6 +135,14 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender)
msgSender.send(fromAkkaAppsRedisChannel, json)
msgSender.send("from-akka-apps-frontend-redis-channel", json)

case UserLeftMeetingEvtMsg.NAME =>
msgSender.send(fromAkkaAppsRedisChannel, json)
msgSender.send("from-akka-apps-frontend-redis-channel", json)

case UserLeftVoiceConfToClientEvtMsg.NAME =>
msgSender.send(fromAkkaAppsRedisChannel, json)
msgSender.send("from-akka-apps-frontend-redis-channel", json)

case _ =>
msgSender.send(fromAkkaAppsRedisChannel, json)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@ public void init() {
log.info("HTML5LoadBalancingService initialised");
}

// Find nodejs processes associated with processing meeting events
// $ ps -u meteor -o pcpu,cmd= | grep NODEJS_BACKEND_INSTANCE_ID
// 1.1 /usr/share/node-v12.16.1-linux-x64/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=1
// 1.0 /usr/share/node-v12.16.1-linux-x64/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=2
public void scanHTML5processes() {
try {
this.list = new ArrayList<HTML5ProcessLine>();
Process p1 = Runtime.getRuntime().exec(new String[]{"ps", "-u", "meteor", "-o", "pcpu,cmd="});
InputStream input1 = p1.getInputStream();
Process p2 = Runtime.getRuntime().exec(new String[]{"grep", "node-"});
Process p2 = Runtime.getRuntime().exec(new String[]{"grep", HTML5ProcessLine.BBB_HTML5_PROCESS_IDENTIFIER});
OutputStream output = p2.getOutputStream();
IOUtils.copy(input1, output);
output.close(); // signals grep to finish
Expand All @@ -73,24 +77,6 @@ private boolean listItemWithIdExists(int id) {
return false;
}

public int findSuitableHTML5ProcessByLookingAtCPU() {
this.scanHTML5processes();
if (list.isEmpty()) {
log.warn("Did not find any instances of html5 process running");
return 1;
}
double smallestCPUvalue = this.list.get(0).percentageCPU;
int instanceIDofSmallestCPUValue = this.list.get(0).instanceId;
for (HTML5ProcessLine line : this.list) {
System.out.println(line.toString());
if (smallestCPUvalue > line.percentageCPU) {
smallestCPUvalue = line.percentageCPU;
instanceIDofSmallestCPUValue = line.instanceId;
}
}
return instanceIDofSmallestCPUValue;
}

public int findSuitableHTML5ProcessByRoundRobin() {
this.scanHTML5processes();
if (list.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,21 @@ public class HTML5ProcessLine {
public int instanceId;
public double percentageCPU;

public static final String BBB_HTML5_PROCESS_IDENTIFIER = "NODEJS_BACKEND_INSTANCE_ID";

public HTML5ProcessLine(String input) {
// System.out.println("input:" + input);
// 0.1 /usr/share/node-v12.16.1-linux-x64/bin/node main.js INFO_INSTANCE_ID=3
// $ ps -u meteor -o pcpu,cmd= | grep NODEJS_BACKEND_INSTANCE_ID
// 1.1 /usr/share/node-v12.16.1-linux-x64/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=1
// 1.0 /usr/share/node-v12.16.1-linux-x64/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=2

String[] a = input.trim().split(" ");
this.percentageCPU = Double.parseDouble(a[0]);
String instanceIdInfo = a[3];
this.instanceId = Integer.parseInt(instanceIdInfo.replace("INFO_INSTANCE_ID=", ""));

for (int i = 0; i < a.length; i++) {
if (a[i].toString().indexOf(BBB_HTML5_PROCESS_IDENTIFIER) > -1) {
this.instanceId = Integer.parseInt(a[i].replace(BBB_HTML5_PROCESS_IDENTIFIER + "=", ""));
}
}
}

public String toString() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function handleMeetingDestruction({ body }) {
const { meetingId } = body;
check(meetingId, String);

if (!process.env.METEOR_ROLE || process.env.METEOR_ROLE === 'frontend') {
if (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'frontend') {
destroyExternalVideo(meetingId);
removeAnnotationsStreamer(meetingId);
removeCursorStreamer(meetingId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export default function addMeeting(meeting) {
try {
const { insertedId, numberAffected } = Meetings.upsert(selector, modifier);

if (!process.env.METEOR_ROLE || process.env.METEOR_ROLE === 'frontend') {
if (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'frontend') {
addAnnotationsStreamer(meetingId);
addCursorStreamer(meetingId);
// TODO add addExternalVideoStreamer(meetingId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import clearAuthTokenValidation from '/imports/api/auth-token-validation/server/
import Metrics from '/imports/startup/server/metrics';

export default function meetingHasEnded(meetingId) {
if (!process.env.METEOR_ROLE || process.env.METEOR_ROLE === 'frontend') {
if (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'frontend') {
removeAnnotationsStreamer(meetingId);
removeCursorStreamer(meetingId);
// TODO add removeExternalVideoStreamer(meetingId);
Expand Down
10 changes: 8 additions & 2 deletions bigbluebutton-html5/imports/startup/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Meteor.startup(() => {
const APP_CONFIG = Meteor.settings.public.app;
const env = Meteor.isDevelopment ? 'development' : 'production';
const CDN_URL = APP_CONFIG.cdn;
const instanceId = APP_CONFIG.instanceId.slice(1); // remove the leading '/' character
const instanceId = parseInt(process.env.INSTANCE_ID, 10) || 1;

Logger.warn('Started bbb-html5 process with instanceId=' + instanceId);

Expand Down Expand Up @@ -132,7 +132,13 @@ Meteor.startup(() => {

setMinBrowserVersions();

Logger.warn(`SERVER STARTED.\nENV=${env},\nnodejs version=${process.version}\nMETEOR_ROLE=${process.env.METEOR_ROLE}\nCDN=${CDN_URL}\n`, APP_CONFIG);
Logger.warn(`SERVER STARTED.
ENV=${env}
nodejs version=${process.version}
BBB_HTML5_ROLE=${process.env.BBB_HTML5_ROLE}
INSTANCE_ID=${instanceId}
PORT=${process.env.PORT}
CDN=${CDN_URL}\n`, APP_CONFIG);
}
});

Expand Down
98 changes: 75 additions & 23 deletions bigbluebutton-html5/imports/startup/server/redis.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ class RedisPubSub {
this.didSendRequestEvent = false;
const host = process.env.REDIS_HOST || Meteor.settings.private.redis.host;
const redisConf = Meteor.settings.private.redis;
this.instanceMax = parseInt(process.env.INSTANCE_MAX, 10) || 1;
this.instanceId = parseInt(process.env.INSTANCE_ID, 10) || 1; // 1 also handles running in dev mode
this.role = process.env.BBB_HTML5_ROLE;
this.customRedisChannel = `to-html5-redis-channel${this.instanceId}`;

const { password, port } = redisConf;
Expand All @@ -141,6 +141,7 @@ class RedisPubSub {

this.emitter = new EventEmitter2();
this.mettingsQueues = {};
// We create this _ meeting queue because we need to be able to handle system messages (no meetingId in core.header)
this.mettingsQueues[NO_MEETING_ID] = new MeetingMessageQueue(this.emitter, this.config.async, this.config.debug);

this.handleSubscribe = this.handleSubscribe.bind(this);
Expand All @@ -156,20 +157,29 @@ class RedisPubSub {
channelsToSubscribe.push(this.customRedisChannel);


switch (process.env.METEOR_ROLE) {
switch (this.role) {
case 'frontend':
this.sub.psubscribe('from-akka-apps-frontend-redis-channel');
if (this.redisDebugEnabled) {
Logger.debug(`Redis: NodeJSPool:${this.instanceId} Role: frontend. Subscribed to 'from-akka-apps-frontend-redis-channel'`);
}
break;
default:
case 'backend':
channelsToSubscribe.forEach((channel) => {
this.sub.psubscribe(channel);
if (this.redisDebugEnabled) {
Logger.debug(`Redis: NodeJSPool:${this.instanceId} Role: backend. Subscribed to '${channelsToSubscribe}'`);
}
});
break;
default:
this.sub.psubscribe('from-akka-apps-frontend-redis-channel');
channelsToSubscribe.forEach((channel) => {
this.sub.psubscribe(channel);
if (this.redisDebugEnabled) {
Logger.debug(`Redis: NodeJSPool:${this.instanceId} Role:${this.role} (likely only one nodejs running, doing both frontend and backend. Dev env? ). Subscribed to '${channelsToSubscribe}'`);
}
});

break;
}
Expand All @@ -183,7 +193,7 @@ class RedisPubSub {

// TODO: Move this out of this class, maybe pass as a callback to init?
handleSubscribe() {
if (this.didSendRequestEvent) return;
if (this.didSendRequestEvent || this.role === 'frontend') return;

// populate collections with pre-existing data
const REDIS_CONFIG = Meteor.settings.private.redis;
Expand All @@ -201,8 +211,8 @@ class RedisPubSub {

handleMessage(pattern, channel, message) {
const parsedMessage = JSON.parse(message);
const { name: eventName, meetingId } = parsedMessage.core.header;
const { ignored: ignoredMessages, async } = this.config;
const eventName = parsedMessage.core.header.name;

if (ignoredMessages.includes(channel)
|| ignoredMessages.includes(eventName)) {
Expand All @@ -215,36 +225,78 @@ class RedisPubSub {
return;
}

const queueId = meetingId || NO_MEETING_ID;
// System messages like Create / Destroy Meeting, etc do not have core.header.meetingId.
// Process them in MeetingQueue['_'] --- the NO_MEETING queueId
const meetingIdFromMessageCoreHeader = parsedMessage.core.header.meetingId || NO_MEETING_ID;

if (eventName === 'MeetingCreatedEvtMsg' || eventName === 'SyncGetMeetingInfoRespMsg') {
const newIntId = parsedMessage.core.body.props.meetingProp.intId;
const instanceId = parsedMessage.core.body.props.systemProps.html5InstanceId;
if (this.role === 'frontend') {
// receiving this message means we need to look at it. Frontends do not have instanceId.
if (meetingIdFromMessageCoreHeader === NO_MEETING_ID) { // if this is a system message

Logger.warn(`${eventName} (name=${parsedMessage.core.body.props.meetingProp.name}) received with meetingInstance: ${instanceId} -- this is instance: ${this.instanceId}`);

if (instanceId === this.instanceId) {
this.mettingsQueues[newIntId] = new MeetingMessageQueue(this.emitter, async, this.redisDebugEnabled);
} else {
// Logger.error('THIS NODEJS ' + this.instanceId + ' IS **NOT** PROCESSING EVENTS FOR THIS MEETING ' + instanceId)
if (eventName === 'MeetingCreatedEvtMsg' || eventName === 'SyncGetMeetingInfoRespMsg') {
const meetingIdFromMessageMeetingProp = parsedMessage.core.body.props.meetingProp.intId;
this.mettingsQueues[meetingIdFromMessageMeetingProp] = new MeetingMessageQueue(this.emitter, async, this.redisDebugEnabled);
return; // we don't want to process the create meeting message since it can lead to duplication of meetings in mongo.
}
}
}

// if (channel !== this.customRedisChannel && queueId in this.mettingsQueues) {
// Logger.error(`Consider routing ${eventName} to ${this.customRedisChannel}` );
// // Logger.error(`Consider routing ${eventName} to ${this.customRedisChannel}` + message);
// }

if (channel === this.customRedisChannel || queueId in this.mettingsQueues) {
this.mettingsQueues[queueId].add({
// process the event - whether it's a system message or not, the meetingIdFromMessageCoreHeader value is adjusted
this.mettingsQueues[meetingIdFromMessageCoreHeader].add({
pattern,
channel,
eventName,
parsedMessage,
});

} else {
if (meetingIdFromMessageCoreHeader === NO_MEETING_ID) { // if this is a system message
const meetingIdFromMessageMeetingProp = parsedMessage.core.body.props?.meetingProp?.intId;
const instanceIdFromMessage = parsedMessage.core.body.props?.systemProps?.html5InstanceId; // end meeting message does not seem to have systemProps

if (this.instanceId === instanceIdFromMessage) {
// create queue or destroy queue
if (eventName === 'MeetingCreatedEvtMsg' || eventName === 'SyncGetMeetingInfoRespMsg') {
this.mettingsQueues[meetingIdFromMessageMeetingProp] = new MeetingMessageQueue(this.emitter, async, this.redisDebugEnabled);
}
this.mettingsQueues[NO_MEETING_ID].add({
pattern,
channel,
eventName,
parsedMessage,
});
} else {
if (eventName === 'MeetingEndedEvtMsg' || eventName === 'MeetingDestroyedEvtMsg') {
// MeetingEndedEvtMsg does not follow the system message pattern for meetingId
// but we still need to process it on the backend which is processing the rest of the events
// for this meetingId (it does not contain instanceId either, so we cannot compare that)
const meetingIdForMeetingEnded = parsedMessage.core.body.meetingId;
if (!!this.mettingsQueues[meetingIdForMeetingEnded]) {
this.mettingsQueues[NO_MEETING_ID].add({
pattern,
channel,
eventName,
parsedMessage,
});
}
}
// I ignore
}
} else {
// add to existing queue
if (!!this.mettingsQueues[meetingIdFromMessageCoreHeader]) {
// only handle message if we have a queue for the meeting. If we don't have a queue, it means it's for a different instanceId
this.mettingsQueues[meetingIdFromMessageCoreHeader].add({
pattern,
channel,
eventName,
parsedMessage,
});
}
}
}
}


destroyMeetingQueue(id) {
delete this.mettingsQueues[id];
}
Expand Down
7 changes: 2 additions & 5 deletions bigbluebutton-html5/imports/startup/server/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@ import fs from 'fs';
import YAML from 'yaml';

const YAML_FILE_PATH = process.env.BBB_HTML5_SETTINGS || 'assets/app/config/settings.yml';
const INSTANCE_MAX = parseInt(process.env.INSTANCE_MAX, 10) || 1;
const REQUESTED_INSTANCE_ID = parseInt(process.env.INSTANCE_ID, 10) || 1;
const INSTANCE_ID = (INSTANCE_MAX < REQUESTED_INSTANCE_ID) ? 1 : REQUESTED_INSTANCE_ID;


try {
if (fs.existsSync(YAML_FILE_PATH)) {
const SETTINGS = YAML.parse(fs.readFileSync(YAML_FILE_PATH, 'utf-8'));

Meteor.settings = SETTINGS;
Meteor.settings.public.app.instanceId = `/${INSTANCE_ID}`;
Meteor.settings.public.app.instanceId = ''; // no longer use instanceId in URLs. Likely permanent change
// Meteor.settings.public.app.instanceId = `/${INSTANCE_ID}`;

__meteor_runtime_config__.PUBLIC_SETTINGS = SETTINGS.public;
} else {
Expand Down
8 changes: 3 additions & 5 deletions bigbluebutton-html5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@
"generate-refs-visual-regression": "rm -rf tests/webdriverio/screenshots; npm run test-visual-regression",
"test-visual-regression-desktop": "export BROWSER_NAME=firefox; wdio ./tests/webdriverio/wdio.vreg.conf.js; export BROWSER_NAME=chrome; wdio ./tests/webdriverio/wdio.vreg.conf.js",
"generate-refs-visual-regression-desktop": "rm -rf tests/webdriverio/screenshots; npm run test-visual-regression-desktop",
"start:prod": "meteor reset && ROOT_URL=http://127.0.0.1/html5client/1 meteor run --production --port=4000",
"start:dev": "ROOT_URL=http://127.0.0.1/html5client/1 meteor run --port=4000",
"start:dev-fast-mongo": "env ROOT_URL=http://127.0.0.1/html5client/1 MONGO_OPLOG_URL=mongodb://127.0.1.1/local MONGO_URL=mongodb://127.0.1.1/meteor ROOT_URL=http://127.0.0.1/html5client/1 NODE_ENV=development meteor run --port=4000",
"start:backend": "env METEOR_ROLE=backend MONGO_OPLOG_URL=mongodb://127.0.1.1/local MONGO_URL=mongodb://127.0.1.1/meteor ROOT_URL=http://127.0.0.1/html5client/1 PORT=3100 NODE_ENV=development meteor run --port 3100",
"start:frontend": "env METEOR_ROLE=frontend MONGO_OPLOG_URL=mongodb://127.0.1.1/local MONGO_URL=mongodb://127.0.1.1/meteor ROOT_URL=http://127.0.0.1/html5client/1 NODE_ENV=development meteor",
"start:prod": "meteor reset && ROOT_URL=http://127.0.0.1/html5client meteor run --production --port=4000",
"start:dev": "ROOT_URL=http://127.0.0.1/html5client meteor run --port=4000",
"start:dev-fast-mongo": "env ROOT_URL=http://127.0.0.1/html5client MONGO_OPLOG_URL=mongodb://127.0.1.1/local MONGO_URL=mongodb://127.0.1.1/meteor ROOT_URL=http://127.0.0.1/html5client NODE_ENV=development meteor run --port=4000",
"test": "wdio ./tests/webdriverio/wdio.conf.js",
"lint": "eslint . --ext .jsx,.js"
},
Expand Down
4 changes: 2 additions & 2 deletions bigbluebutton-web/grails-app/conf/bigbluebutton.properties
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,13 @@ bigbluebutton.web.logoutURL=default

# The url of the BigBlueButton HTML5 client. Users will be redirected here when
# successfully joining the meeting.
defaultHTML5ClientUrl=${bigbluebutton.web.serverURL}/html5client/%%INSTANCEID%%/join
defaultHTML5ClientUrl=${bigbluebutton.web.serverURL}/html5client/join

# Allow requests without JSESSIONID to be handled (default = false)
allowRequestsWithoutSession=false

# The url for where the guest will poll if approved to join or not.
defaultGuestWaitURL=${bigbluebutton.web.serverURL}/html5client/%%INSTANCEID%%/guestWait
defaultGuestWaitURL=${bigbluebutton.web.serverURL}/html5client/guestWait

# The default avatar image to display.
useDefaultAvatar=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,10 +496,6 @@ class ApiController {
boolean redirectClient = true;
String clientURL = paramsProcessorUtil.getDefaultHTML5ClientUrl();

String meetingInstance = meeting.getHtml5InstanceId();
meetingInstance = (meetingInstance == null) ? "1" : meetingInstance;
clientURL = clientURL.replaceAll("%%INSTANCEID%%", meetingInstance);

if (!StringUtils.isEmpty(params.redirect)) {
try {
redirectClient = Boolean.parseBoolean(params.redirect);
Expand All @@ -524,7 +520,6 @@ class ApiController {
String destUrl = clientURL + "?sessionToken=" + sessionToken
if (guestStatusVal.equals(GuestPolicy.WAIT)) {
String guestWaitUrl = paramsProcessorUtil.getDefaultGuestWaitURL();
guestWaitUrl = guestWaitUrl.replaceAll("%%INSTANCEID%%", meetingInstance);
destUrl = guestWaitUrl + "?sessionToken=" + sessionToken
msgKey = "guestWait"
msgValue = "Guest waiting for approval to join meeting."
Expand Down Expand Up @@ -1354,13 +1349,9 @@ class ApiController {
String destUrl = clientURL
log.debug("destUrl = " + destUrl)

String meetingInstance = meeting.getHtml5InstanceId();
meetingInstance = (meetingInstance == null) ? "1" : meetingInstance;

if (guestWaitStatus.equals(GuestPolicy.WAIT)) {
meetingService.guestIsWaiting(us.meetingID, us.internalUserId);
clientURL = paramsProcessorUtil.getDefaultGuestWaitURL();
clientURL = clientURL.replaceAll("%%INSTANCEID%%", meetingInstance);
destUrl = clientURL + "?sessionToken=" + sessionToken
log.debug("GuestPolicy.WAIT - destUrl = " + destUrl)
msgKey = "guestWait"
Expand Down

0 comments on commit f43560d

Please sign in to comment.