diff --git a/Dockerfile b/Dockerfile index f7687b5fed4d4f786769a3894fe6754e6147e190..a619a66ac2cf3f7da2be16b82c5bcd5221dc8318 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 b510c1b896102d8c8699562beca39ae64bd4a04b..408c0b8f867bd7a74e20d5db3700d81f1dad1884 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 67902d8ed79a9577d9882c04e04af176484d8c37..0e820092ebfb64266a374dc741b81ec090fbdd3c 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 0000000000000000000000000000000000000000..1b343769231f711b24323073541a63b826a60612 --- /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 0000000000000000000000000000000000000000..6f75794c64b9ba7c3baf6c6ec4e191a8fc5f02d7 --- /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 7c829da1ddead39dee11b4862a43da6b55287913..4e9ef9d0c0f47f6c864a57a0eebf567ec2fce063 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 a7b23143b6630c139869a4f74b595726c101d759..9e785e50979ff2b5eac35badf32c3a81b8c73843 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 89caf11cff5571419942bfa062cc355d5c240ab8..02b22870656935745fc59aa980516395626e6602 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 0000000000000000000000000000000000000000..a4a3070d7904da7268ddd2fd46ac60a6a1f6b16a --- /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 0000000000000000000000000000000000000000..8a3af2e91c3c0bca3a5ee71c2534a77e8a698d81 --- /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