From a177f4c06630555a24ec0cc4aaa68e0b3774e519 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Mon, 9 Jan 2023 15:52:01 +1100 Subject: [PATCH] Add feature to render reports in the background This is supposed to lower the memory footprint of all Puma workers. The reports code will occupy needed memory in one Sidekiq worker instead of in several Puma processes. The current code doesn't limit the execution time yet. We either need a way to terminate the report rendering after a while or send an email with a link to access a rendered report. --- .rubocop_todo.yml | 1 + app/controllers/admin/reports_controller.rb | 22 +++++++++--- app/jobs/report_job.rb | 34 +++++++++++++++++++ lib/reporting/report_renderer.rb | 2 +- spec/jobs/report_job_spec.rb | 37 +++++++++++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 app/jobs/report_job.rb create mode 100644 spec/jobs/report_job_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 79c5727eb1..b08465a54b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -692,6 +692,7 @@ Rails/ApplicationJob: - 'app/jobs/heartbeat_job.rb' - 'app/jobs/order_cycle_closing_job.rb' - 'app/jobs/order_cycle_notification_job.rb' + - 'app/jobs/report_job.rb' - 'app/jobs/subscription_confirm_job.rb' - 'app/jobs/subscription_placement_job.rb' diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index f6a30505d3..2dc18e4092 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -24,17 +24,17 @@ module Admin if report_format.present? export_report else - render_report + show_report end end private def export_report - send_data @report.render_as(report_format), filename: report_filename + send_data render_report_as(report_format), filename: report_filename end - def render_report + def show_report assign_view_data render "show" end @@ -45,12 +45,26 @@ module Admin @report_subtype = report_subtype @report_title = report_title @rendering_options = rendering_options - @table = @report.to_html if render_data? + @table = render_report_as(:html) if render_data? @data = Reporting::FrontendData.new(spree_current_user) end def render_data? request.post? end + + def render_report_as(format) + if OpenFoodNetwork::FeatureToggle.enabled?(:background_reports, spree_current_user) + job = ReportJob.perform_later( + report_class, spree_current_user, params, format + ) + sleep 1 until job.done? + + # This result has been rendered by Rails in safe mode already. + job.result.html_safe # rubocop:disable Rails/OutputSafety + else + @report.render_as(format) + end + end end end diff --git a/app/jobs/report_job.rb b/app/jobs/report_job.rb new file mode 100644 index 0000000000..7143b3637c --- /dev/null +++ b/app/jobs/report_job.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Renders a report and saves it to a temporary file. +class ReportJob < ActiveJob::Base + def perform(report_class, user, params, format) + report = report_class.new(user, params, render: true) + result = report.render_as(format) + write(result) + end + + def done? + @done ||= File.file?(filename) + end + + def result + @result ||= read_result + end + + private + + def write(result) + File.write(filename, result) + end + + def read_result + File.read(filename) + ensure + File.unlink(filename) + end + + def filename + Rails.root.join("tmp/report-#{job_id}") + end +end diff --git a/lib/reporting/report_renderer.rb b/lib/reporting/report_renderer.rb index c58917a5ae..d301c5a614 100644 --- a/lib/reporting/report_renderer.rb +++ b/lib/reporting/report_renderer.rb @@ -4,7 +4,7 @@ require 'spreadsheet_architect' module Reporting class ReportRenderer - REPORT_FORMATS = [:csv, :json, :xlsx, :pdf].freeze + REPORT_FORMATS = [:csv, :json, :html, :xlsx, :pdf].freeze def initialize(report) @report = report diff --git a/spec/jobs/report_job_spec.rb b/spec/jobs/report_job_spec.rb new file mode 100644 index 0000000000..9211ee0e44 --- /dev/null +++ b/spec/jobs/report_job_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ReportJob do + let(:report_args) { [report_class, user, params, format] } + let(:report_class) { Reporting::Reports::UsersAndEnterprises::Base } + let(:user) { enterprise.owner } + let(:enterprise) { create(:enterprise) } + let(:params) { {} } + let(:format) { :csv } + + it "generates a report" do + job = ReportJob.new + job.perform(*report_args) + expect_csv_report(job) + end + + it "enqueues a job for asynch processing" do + job = ReportJob.perform_later(*report_args) + expect(job.done?).to eq false + + # This performs the job in the same process but that's good enought for + # testing the job code. I hope that we can rely on the job worker. + ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true + job.retry_job + + expect(job.done?).to eq true + expect_csv_report(job) + end + + def expect_csv_report(job) + table = CSV.parse(job.result) + expect(table[0][1]).to eq "Relationship" + expect(table[1][1]).to eq "owns" + end +end