Как я могу обойти 30-секундный лимит HTTP в Heroku?

Я унаследовал приложение rails, которое развертывается с помощью Heroku (я думаю). Я редактирую его в AWS Cloud9 IDE и пока просто делаю все в режиме разработки. Цель приложения — обрабатывать большие объемы данных опроса и отображать их в отчете в формате PDF. Это работает для небольших отчетов с примерно 10 строками данных, но когда я загружаю отчет, который запрашивает загрузку данных из 5000+ строк для создания HTML-страницы, которая преобразуется в PDF, это занимает около 105 секунд, что намного дольше, чем у Heroku. 30 секунд отводится для HTTP-запросов.

Heroku говорит об этом на своем веб-сайте, что вселило в меня некоторую надежду:

«Heroku поддерживает функции HTTP 1.1, такие как длинный опрос и потоковые ответы. Приложение имеет начальное 30-секундное окно, чтобы ответить клиенту одним байтом. Однако каждый байт, переданный после этого (будь то получен от клиента или отправлен вашим приложение) сбрасывает скользящее 55-секундное окно. Если в течение 55-секундного окна данные не отправляются, соединение будет разорвано». (Источник: https://devcenter.heroku.com/articles/request-timeout#long-polling-and-streaming-responses)

Для меня это звучит превосходно - я могу просто отправлять запрос клиенту каждую секунду или около того в цикле, пока мы не закончим создание большого отчета в формате PDF. Однако я не знаю, как отправить или получить байт или около того, чтобы «сбросить скользящее 55-секундное окно», о котором они говорят.

Вот часть моего контроллера, которая отправляет запрос.

            return render pdf: pdf_name + " " + pdf_year.to_s,
                disposition: 'attachment',
                page_height: 1300,
                encoding: 'utf8',
                page_size:   'A4',
                footer: {html: {template: 'recent_grad/footer.html.erb'}, spacing: 0 },
                margin:  {   top:    10,                     # default 10 (mm)
                            bottom: 20,
                            left:   10,
                            right:  10 },
                template: "recent_grad/report.html.erb",
                locals: {start: @start, survey: @survey, years: @years, college: @college, department: @department, program: @program, emphasis: @emphasis, questions: @questions}

Я делаю другие запросы, чтобы добраться до этого момента, но я считаю, что часть, вызывающая проблему, находится здесь, где отображается шаблон. Мой шаблон запрашивает базу данных в конечном цикле, который останавливается, когда заканчиваются вопросы опроса для запроса.

Мой вопрос таков: как я могу «отправить или получить байт клиенту», чтобы сообщить Heroku: «Я все еще пытаюсь создать этот массивный PDF-файл, поэтому, пожалуйста, сбросьте таймер и дайте мне мои 55 секунд!» Это в форме запроса? Потому что, если это так, я снова и снова запрашиваю базу данных MySql в своем файле report.html.erb.

Кроме того, раньше он работал без проблем и работал с небольшими отчетами, но теперь я получаю сообщение об ошибке «504 Gateway Timeout» до завершения запроса на фактической странице, но моя консоль puma продолжает запрашивать базу данных, как сумасшедший. Я предполагаю, что это проблема Heroku, потому что ошибка 504 происходит ровно каждые 35 секунд (5 секунд для обработки других частей и 30 секунд, чтобы попытаться завершить цикл в шаблоне, чтобы он мог правильно отображаться).

Если вам нужна дополнительная информация или код, пожалуйста, спросите! заранее спасибо

РЕДАКТИРОВАТЬ: Оба приведенных ниже комментария предполагают возможные дубликаты, но ни один из них не имеет реального ответа с реальным кодом, они просто ссылаются на документы, которые я здесь цитирую. Я ищу пример кода (или, по крайней мере, способ войти в дверь), а не просто ссылку на документы. Спасибо!

РЕДАКТИРОВАТЬ 2:

Я попробовал то, что сказал @Sergio, и установил SideKiq. Я думаю, что я действительно близок, но все еще имею некоторые проблемы с работником. У работника нет доступа к ActionView::Base, который требуется для метода рендеринга в рельсах, поэтому он не работает. Я могу получить доступ к рабочему методу, что означает, что мои серверы sidekiq и redis работают правильно, но он попадает в строку ActionView с этой ошибкой:

ПРЕДУПРЕЖДЕНИЕ: NameError: неинициализированная константа HardWorker::ActionView

Вот рабочий код:

require 'sidekiq'

Sidekiq.configure_client do |config|
  # config.redis = { db: 1 }
  config.redis = { url: 'redis://172.31.6.51:6379/0' }
end

Sidekiq.configure_server do |config|
  # config.redis = { db: 1 }
  config.redis = { url: 'redis://172.31.6.51:6379/0' }
end  

class HardWorker
  include Sidekiq::Worker
  def perform(pdf_name, pdf_year)
    av = ActionView::Base.new()
    av.view_paths = ActionController::Base.view_paths
    av.class_eval do
      include Rails.application.routes.url_helpers
      include ApplicationHelper
    end
    puts "inside hardworker"
    puts pdf_name, pdf_year

    av.render pdf: pdf_name + " " + pdf_year.to_s,
                disposition: 'attachment',
                page_height: 1300,
                encoding: 'utf8',
                page_size:   'A4',
                footer: {html: {template: 'recent_grad/footer.html.erb'}, spacing: 0 },
                margin:  {   top:    10,                     # default 10 (mm)
                            bottom: 20,
                            left:   10,
                            right:  10 },
                template: "recent_grad/report.html.erb",
                locals: {start: @start, survey: @survey, years: @years, college: @college, department: @department, program: @program, emphasis: @emphasis, questions: @questions}
  end
end


Какие-либо предложения?

РЕДАКТИРОВАТЬ 3: я сделал то, что сказал @Sergio, и попытался создать PDF-файл напрямую из файла html.erb и сохранить его в файл. Вот мой код:

# /app/controllers/recentgrad_controller.rb

pdf = WickedPdf.new.pdf_from_html_file('home/ec2-user/environment/gradSurvey/gradSurvey/app/views/recent_grad/report.html.erb')
            save_path = Rails.root.join('pdfs', pdf_name + pdf_year.to_s + '.pdf')
            File.open(save_path, 'wb') do |file|
              file << pdf
            end

И вывод ошибки:

RuntimeError (Failed to execute:
["/usr/local/rvm/gems/ruby-2.4.1@gradSurvey/bin/wkhtmltopdf", "file:///home/ec2-user/environment/gradSurvey/gradSurvey/app/views/recent_grad/report.html.erb", "/tmp/wicked_pdf_generated_file20190523-15416-hvb3zg.pdf"]
Error: PDF could not be generated!
 Command Error: Loading pages (1/6)
Error: Failed loading page file:///home/ec2-user/environment/gradSurvey/gradSurvey/app/views/recent_grad/report.html.erb (sometimes it will work just to ignore this error with --load-error-handling ignore)
Exit with code 1 due to network error: ContentNotFoundError
):

Я понятия не имею, что это значит, когда он говорит: «иногда будет работать просто игнорировать эту ошибку с --load-error-handling ignore». Файл определенно существует, и я пробовал, может быть, 5 вариантов пути к файлу.


person Parzival    schedule 21.05.2019    source источник
comment
Возможный дубликат Обходной путь для Heroku 30-секундный тайм-аут с длинным внешний запрос   -  person Chris    schedule 22.05.2019
comment
См. редактирование выше, я ищу, как это сделать, а не просто где их найти в документах. Кажется, два других сообщения просто указывают спрашивающим на документы, на которые я ссылался в своем вопросе.   -  person Parzival    schedule 22.05.2019
comment
Мой шаблон запрашивает базу данных в конечном цикле, который останавливается, когда заканчиваются вопросы опроса для запроса. - Я чувствую здесь случай N+1   -  person Sergio Tulentsev    schedule 22.05.2019
comment
Получилось, когда я унаследовал от ApplicationJob в джоб, а не воркер, хотя по сути это одно и то же. Вот мое наследство: class CreatePdfJob < ApplicationJob. Настоящим победителем здесь стал @unixmonkey, который поддерживает wicked_pdf для рельсов и настроил для меня работающее приложение. Посмотреть вопрос можно здесь: github.com/mileszs/wicked_pdf/issues/835 А коммит, который действительно изменил его работу, можно посмотреть здесь: github.com/unixmonkey/generate_pdf_async_example/commit/… Наслаждайтесь!   -  person Parzival    schedule 29.05.2019


Ответы (1)


Мне приходилось делать что-то подобное несколько раз. Во всех случаях я закончил тем, что написал фоновое задание, которое выполняет всю тяжелую генерацию lifting. А поскольку это не веб-запрос, на него не влияет 30-секундный тайм-аут. Это выглядит примерно так:

  1. клиент (ваш код javascript) запрашивает новый отчет.
  2. сервер генерирует описание работы и ставит его в очередь для вашего работника.
  3. worker выбирает задание из очереди и начинает работу (запрос к базе данных и т. д.)
  4. тем временем клиент периодически спрашивает сервер «мой отчет уже готов?». Сервер отвечает "еще нет, попробуйте позже"
  5. worker закончил создание отчета. Он загружает файл в какое-либо хранилище (например, S3), устанавливает статус задания на «завершено», а результат задания — на ссылку для скачивания загруженного файла отчета.
  6. сервер, видя, что задание завершено, теперь может отвечать на запросы обновления статуса клиента: «Да, это сделано. Вот URL-адрес. Хорошего дня».
  7. Все счастливы. И никому не нужно было стримить или играть с тайм-аутами отклика heroku.

В приведенном выше сценарии используется короткий опрос. Я считаю его самым простым в реализации. Но это, конечно, немного расточительно по отношению к ресурсам. Вы можете использовать длинный опрос или веб-сокеты или другие причудливые вещи.

person Sergio Tulentsev    schedule 21.05.2019
comment
Спасибо за ответ Серхио. Я понимаю концепцию, но не знаю, как ее реализовать. Как сказать работнику, что делать? А что такое рабочий? Извините за мои нубские вопросы, я все еще новичок в Rails и разработке в целом. - person Parzival; 22.05.2019
comment
@Parzival: Как мне сказать работнику, что делать? - это называется ActiveJob. Вы можете (и должны) использовать другой бэкэнд для activejob (например, sidekiq) - person Sergio Tulentsev; 22.05.2019
comment
Кажется, все указывает на sidekiq, поэтому я попробую и дам вам знать, что произошло. Спасибо за вашу помощь до сих пор! - person Parzival; 22.05.2019
comment
Я так близок (sidekiq запущен, и я могу использовать этот метод), но я все еще получаю сообщение об ошибке при попытке использовать метод рендеринга, потому что я нахожусь в воркере. См. мой РЕДАКТИРОВАТЬ 2 выше. Есть идеи? - person Parzival; 23.05.2019
comment
Не могли бы вы создать файл, сохранить его на сервере, отправить обратно пользователю/посетителю, а затем удалить в какой-то момент в будущем? т. е. установите соответствующие заголовки, чтобы файл загружался. - person prieber; 23.05.2019
comment
@prieber, что ты имеешь в виду? Создаваемый PDF-файл очень большой, поэтому для его создания требуется время (отсюда и фоновая обработка). Я довольно новичок в программировании, поэтому я действительно не знаю, как сохранить его на сервере. - person Parzival; 23.05.2019
comment
@Парсифаль Нет проблем. Этот ответ должен помочь создать файл. Файл по-прежнему будет генерироваться в фоновом режиме, но вам не нужно будет включать try, включая какой-либо код ActionView. - person prieber; 23.05.2019
comment
Можете ли вы показать мне код? У меня появилась идея поместить его в pdf_content, но я не знаю, как это будет выглядеть, учитывая, что мой рендер изначально просто отображал pdf в виде злого pdf. - person Parzival; 23.05.2019
comment
@Parzival: прокрутите ридми wicked_pdf немного дальше :) -расширенное использование - person Sergio Tulentsev; 23.05.2019
comment
@SergioTulentsev Я смог использовать эти документы, чтобы создать PDF-файл из строки и сохранить его в файл, но не сделать PDF-файл из файла. См. редактирование выше для ошибки, которую я получаю. Пожалуйста и спасибо вам! - person Parzival; 23.05.2019
comment
@Parzival: обратите внимание, что метод называется pdf_from_html_file и то, что вы ему передаете, не является html. Вы, вероятно, добьетесь большего успеха, отрендерив этот шаблон ERB вручную. Например, как это. - person Sergio Tulentsev; 24.05.2019
comment
Во всяком случае, это все больше и больше выходит за рамки. - person Sergio Tulentsev; 24.05.2019
comment
@SergioTulentsev Спасибо за вашу помощь, я пока отмечу ваш ответ как правильный. Я ценю, что вы меня в правильном направлении! - person Parzival; 24.05.2019