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