From 8cbf7825599757d6366958f56b066b18320d2e12 Mon Sep 17 00:00:00 2001 From: Samuel Couillard <43917914+scouillard@users.noreply.github.com> Date: Tue, 25 Jul 2023 14:36:37 -0400 Subject: [PATCH] Improvements for handling missed "meeting_ended" and "recording_ready" callbacks (#5327) * Initial commit * Remove unused files * Improvements in error handling, refactoring * add running_meeting_checker spec * improve recordings_poller * remove provider scoping, improve code with logs * rubo * Fix Dockerfile, docker-compose * fix logs issue * remove database name from docker-compose * Rework poller script to match start script * Put proper image to docker-compose --- Dockerfile | 3 +- app/controllers/api/v1/rooms_controller.rb | 7 +- app/services/running_meeting_checker.rb | 6 +- bin/config.env | 17 ++++ bin/poller | 13 +++ bin/start | 17 +--- config/environments/production.rb | 1 + docker-compose.yml | 10 +++ lib/tasks/poller.rake | 82 +++++++++++++++++++ spec/services/running_meeting_checker_spec.rb | 54 ++++++++++++ 10 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 bin/config.env create mode 100644 bin/poller create mode 100644 lib/tasks/poller.rake create mode 100644 spec/services/running_meeting_checker_spec.rb diff --git a/Dockerfile b/Dockerfile index f7687b5f..a619a66a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,8 @@ RUN apk update \ COPY . ./ RUN apk update \ && apk upgrade \ - && update-ca-certificates + && update-ca-certificates \ + && chmod +x ./bin/poller EXPOSE ${PORT} ENTRYPOINT [ "./bin/start" ] diff --git a/app/controllers/api/v1/rooms_controller.rb b/app/controllers/api/v1/rooms_controller.rb index b510c1b8..408c0b8f 100644 --- a/app/controllers/api/v1/rooms_controller.rb +++ b/app/controllers/api/v1/rooms_controller.rb @@ -40,7 +40,8 @@ module Api # Returns a list of the current_user's rooms and shared rooms def index shared_rooms = SharedAccess.where(user_id: current_user.id).select(:room_id) - rooms = Room.where(user_id: current_user.id) + rooms = Room.includes(:user) + .where(user_id: current_user.id) .or(Room.where(id: shared_rooms)) .order(online: :desc) .order('last_session DESC NULLS LAST') @@ -50,7 +51,7 @@ module Api room.shared = true if room.user_id != current_user.id end - RunningMeetingChecker.new(rooms:, provider: current_provider).call + RunningMeetingChecker.new(rooms:).call render_data data: rooms, status: :ok end @@ -58,7 +59,7 @@ module Api # GET /api/v1/rooms/:friendly_id.json # Returns the info on a specific room def show - RunningMeetingChecker.new(rooms: @room, provider: current_provider).call if @room.online + RunningMeetingChecker.new(rooms: @room).call if @room.online @room.shared = current_user.shared_rooms.include?(@room) diff --git a/app/services/running_meeting_checker.rb b/app/services/running_meeting_checker.rb index 67902d8e..0e820092 100644 --- a/app/services/running_meeting_checker.rb +++ b/app/services/running_meeting_checker.rb @@ -18,19 +18,19 @@ # Pass the room(s) to the service and it will confirm if the meeting is online or not and will return the # of participants class RunningMeetingChecker - def initialize(rooms:, provider:) + def initialize(rooms:) @rooms = rooms - @provider = provider end def call online_rooms = Array(@rooms).select { |room| room.online == true } online_rooms.each do |online_room| - bbb_meeting = BigBlueButtonApi.new(provider: @provider).get_meeting_info(meeting_id: online_room.meeting_id) + bbb_meeting = BigBlueButtonApi.new(provider: online_room.user.provider).get_meeting_info(meeting_id: online_room.meeting_id) online_room.participants = bbb_meeting[:participantCount] rescue BigBlueButton::BigBlueButtonException online_room.update(online: false) + next end end end diff --git a/bin/config.env b/bin/config.env new file mode 100644 index 00000000..1b343769 --- /dev/null +++ b/bin/config.env @@ -0,0 +1,17 @@ +# Set default port if PORT is not set +PORT="${PORT:=3000}" + +# Parse Rails DATABASE and REDIS urls to get host and port +TXADDR=${DATABASE_URL/*:\/\/} +TXADDR=${TXADDR/*@/} +TXADDR=${TXADDR/\/*/} +IFS=: TXADDR=($TXADDR) IFS=' ' +PGHOST=${TXADDR[0]} +PGPORT=${TXADDR[1]:-5432} + +TXADDR=${REDIS_URL/*:\/\/} +TXADDR=${TXADDR/*@/} +TXADDR=${TXADDR/\/*/} +IFS=: TXADDR=($TXADDR) IFS=' ' +RDHOST=${TXADDR[0]} +RDPORT=${TXADDR[1]:-6379} diff --git a/bin/poller b/bin/poller new file mode 100644 index 00000000..6f75794c --- /dev/null +++ b/bin/poller @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +source config.env + +if [ "$RAILS_ENV" = "production" ]; then + while ! nc -zw3 $PGHOST $PGPORT + do + echo "Waiting for postgres to start up ..." + sleep 1 + done +fi + +bundle exec rake poller:run_all diff --git a/bin/start b/bin/start index 7c829da1..4e9ef9d0 100755 --- a/bin/start +++ b/bin/start @@ -1,21 +1,6 @@ #!/usr/bin/env bash -PORT="${PORT:=3000}" - -# Parse Rails DATABASE and REDIS urls to get host and port -TXADDR=${DATABASE_URL/*:\/\/} -TXADDR=${TXADDR/*@/} -TXADDR=${TXADDR/\/*/} -IFS=: TXADDR=($TXADDR) IFS=' ' -PGHOST=${TXADDR[0]} -PGPORT=${TXADDR[1]:-5432} - -TXADDR=${REDIS_URL/*:\/\/} -TXADDR=${TXADDR/*@/} -TXADDR=${TXADDR/\/*/} -IFS=: TXADDR=($TXADDR) IFS=' ' -RDHOST=${TXADDR[0]} -RDPORT=${TXADDR[1]:-6379} +source config.env echo "Greenlight-v3 starting on port: $PORT" diff --git a/config/environments/production.rb b/config/environments/production.rb index a7b23143..9e785e50 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -136,6 +136,7 @@ Rails.application.configure do # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") if ENV['RAILS_LOG_TO_STDOUT'].present? + $stdout.sync = true logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) diff --git a/docker-compose.yml b/docker-compose.yml index 89caf11c..02b22870 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,3 +33,13 @@ services: depends_on: - postgres - redis + + greenlight-v3-poller: + entrypoint: [bin/poller] + image: bigbluebutton/greenlight:v3 + container_name: greenlight-v3-poller + env_file: .env + depends_on: + - postgres + - redis + diff --git a/lib/tasks/poller.rake b/lib/tasks/poller.rake new file mode 100644 index 00000000..a4a3070d --- /dev/null +++ b/lib/tasks/poller.rake @@ -0,0 +1,82 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see <http://www.gnu.org/licenses/>. + +# frozen_string_literal: true + +namespace :poller do + desc 'Runs all pollers' + task :run_all, %i[interval] => :environment do |_task, args| + args.with_defaults(interval: 30) + interval = args[:interval].to_i.minutes # set the interval in minutes + + poller_tasks = %w[poller:meetings_poller poller:recordings_poller] + + info "Running poller with interval #{interval}" + loop do + poller_tasks.each do |poller_task| + info "Running #{poller_task} at #{Time.zone.now}" + Rake::Task[poller_task].invoke(interval) + rescue StandardError => e + err "An error occurred in #{poller_task}: #{e.message}. Continuing..." + end + + sleep interval + + poller_tasks.each do |poller_task| + Rake::Task[poller_task].reenable + end + end + end + + desc 'Polls meetings to check if they are still online' + task meetings_poller: :environment do + online_meetings = Room.includes(:user).where(online: true) + + RunningMeetingChecker.new(rooms: online_meetings).call + + rescue StandardError => e + err "Unable to poll meetings. Error: #{e}" + end + + desc 'Polls recordings to check if they have been created in GL' + task :recordings_poller, %i[interval] => :environment do |_task, args| + # Returns the providers which have recordings disabled + disabled_recordings = RoomsConfiguration.joins(:meeting_option).where(meeting_option: { name: 'record' }, value: 'false').pluck(:provider) + + # Returns the rooms which have been online recently and have not been recorded yet + recent_meeting_interval = args[:interval] * 2 + recent_meetings = Room.includes(:user) + .where(last_session: recent_meeting_interval.ago..Time.zone.now, online: false) + .where.not(user: { provider: disabled_recordings }) + + recent_meetings.each do |meeting| + recordings = BigBlueButtonApi.new(provider: meeting.user.provider).get_recordings(meeting_ids: meeting.meeting_id) + recordings[:recordings].each do |recording| + next if Recording.exists?(record_id: recording[:recordID]) + + unless meeting.recordings_processing.zero? + meeting.update(recordings_processing: meeting.recordings_processing - 1) # cond. in case both callbacks fail + end + + RecordingCreator.new(recording:).call + + rescue StandardError => e + err "Unable to poll Recording:\nRecordID: #{recording[:recordID]}\nError: #{e}" + next + end + end + end +end diff --git a/spec/services/running_meeting_checker_spec.rb b/spec/services/running_meeting_checker_spec.rb new file mode 100644 index 00000000..8a3af2e9 --- /dev/null +++ b/spec/services/running_meeting_checker_spec.rb @@ -0,0 +1,54 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see <http://www.gnu.org/licenses/>. + +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RunningMeetingChecker, type: :service do + let!(:online_room) { create(:room, online: true, user: create(:user, provider: 'greenlight')) } + let(:bbb_api) { instance_double(BigBlueButtonApi) } + + before do + allow(BigBlueButtonApi).to receive(:new).and_return(bbb_api) + end + + context 'when the meeting is running' do + let(:bbb_response) do + { + running: true + } + end + + it 'updates the online status to true' do + allow(bbb_api).to receive(:get_meeting_info).and_return(bbb_response) + + described_class.new(rooms: Room.all).call + + expect(online_room.reload.online).to eq(bbb_response[:running]) + end + end + + context 'when the meeting is not running' do + it 'updates the online status to false' do + allow(bbb_api).to receive(:get_meeting_info).and_raise(BigBlueButton::BigBlueButtonException) + + described_class.new(rooms: Room.all).call + + expect(online_room.reload.online).to be_falsey + end + end +end -- GitLab