module CanvasSync module Jobs # ActiveJob class used to check the status of a pending Canvas report. # Re-enqueues itself if the report is still processing on Canvas. # Enqueues the ReportProcessor when the report has completed. class ReportChecker < CanvasSync::Job REPORT_TIMEOUT = 24.hours COMPILATION_TIMEOUT = 1.hour MAX_TRIES = 3 class FatalReportError < ::RuntimeError; end discard_on FatalReportError # @param report_name [Hash] e.g., 'provisioning_csv' # @param report_id [Integer] # @param processor [String] a stringified report processor class name # @param options [Hash] hash of options that will be passed to the job processor # @return [nil] def perform(report_name, report_id, processor, options, checker_context = {}) # rubocop:disable Metrics/AbcSize max_tries = options[:report_max_tries] || batch_context[:report_max_tries] || MAX_TRIES account_id = options[:account_id] || batch_context[:account_id] || "self" report_status = CanvasSync.get_canvas_sync_client(batch_context) .report_status(account_id, report_name, report_id) case report_status["status"].downcase when "complete" CanvasSync::Jobs::ReportProcessorJob.perform_later( report_name, report_status["attachment"]["url"], processor, options, report_id, ) when "error", "deleted" checker_context[:failed_attempts] ||= 0 checker_context[:failed_attempts] += 1 failed_attempts = checker_context[:failed_attempts] message = "Report failed to process; status was #{report_status} for report_name: #{report_name}, report_id: #{report_id}, #{current_organization.name}. This report has now failed #{checker_context[:failed_attempts]} time." # rubocop:disable Metrics/LineLength Rails.logger.error(message) if failed_attempts >= max_tries Rails.logger.error("This report has failed #{failed_attempts} times. Giving up.") raise FatalReportError, message else restart_report(options, report_name, processor, checker_context) end else report_timeout = parse_timeout(options[:report_timeout] || batch_context[:report_timeout] || REPORT_TIMEOUT) if timeout_met?(options[:sync_start_time], report_timeout) raise FatalReportError, "Report appears to be stuck #{report_name}##{report_id}" end if report_status["status"].downcase == 'compiling' checker_context['compiling_since'] ||= DateTime.now.iso8601 compilation_timeout = parse_timeout(options[:report_compilation_timeout] || batch_context[:report_compilation_timeout] || COMPILATION_TIMEOUT) if timeout_met?(checker_context['compiling_since'], compilation_timeout) raise FatalReportError, "Report appears to be stuck #{report_name}##{report_id}" end end CanvasSync::Jobs::ReportChecker .set(wait: report_checker_wait_time) .perform_later( report_name, report_id, processor, options, checker_context ) end end protected def timeout_met?(base_time, timeout_length) return false unless base_time.present? && timeout_length.present? DateTime.now > (DateTime.parse(base_time) + timeout_length) end def parse_timeout(val) val end def restart_report(options, report_name, processor, checker_context) account_id = options[:account_id] || batch_context[:account_id] || "self" options[:sync_start_time] = DateTime.now.utc.iso8601 new_context = {} new_context[:failed_attempts] = checker_context[:failed_attempts] report_id = start_report(account_id, report_name, options[:report_params]) CanvasSync::Jobs::ReportChecker .set(wait: report_checker_wait_time) .perform_later( report_name, report_id, processor, options, new_context ) end def start_report(account_id, report_name, report_params) report = CanvasSync.get_canvas_sync_client(batch_context) .start_report(account_id, report_name, report_params) report["id"] end end end end