lib/canvas_sync/jobs/report_checker.rb



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