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.
This commit is contained in:
Maikel Linke
2023-01-09 15:52:01 +11:00
committed by Filipe
parent 19c4596b9e
commit a177f4c066
5 changed files with 91 additions and 5 deletions

View File

@@ -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'

View File

@@ -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

34
app/jobs/report_job.rb Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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