Heroku, 나는 당신을 스트리밍합니다!

4292 단어 webdevrailsherokuruby
문제: CSV 또는 기타 형식으로 내보낸 생성된 콘텐츠의 시간 초과.

페이지에서 CSV, XLSX, PDF 등과 같은 형식으로 내보내기 기능을 갖는 것은 매우 일반적인 기능입니다.

이것은 #respond_to 내부의 Rails에서 간단한 것입니다.

def index
  @records = ...
  respond_to do |format|
    format.html # index.html
    format.csv { some_csv_generation_code }
  end
end


그러나 언젠가 사용자가 많은 정보를 다운로드하려고 하면 오류가 나타나기 시작합니다.

서버는 특정 시간이 지나면 시간 초과 오류로 응답하고 파일을 생성하는 요청을 중지합니다.

Heroku와 같은 PaaS에서 이 시간 제한은 생성된 콘텐츠가 없는 30초입니다. 또한 각각의 새로운 출력 조각 사이에는 55''의 또 다른 타임아웃이 있습니다. 공식 정보 인용:

An application has an initial 30 second window to respond with a single byte back to the client. However, each byte transmitted thereafter (either received from the client or sent by your application) resets a rolling 55 second window. If no data is sent during the 55 second window, the connection will be terminated.



Rails에는 ActionController::Live에서 이러한 종류의 출력 생성을 처리하는 스트리밍 기능이 있는 것 같습니다. 또한 Rails edge(향후 v7)에는 이 기능을 달성하는 것으로 보이는 #send_stream 메서드가 있으므로 컨트롤러 문제로 내 Rails 5.2 앱에 복사하고 Enumerable #each 메서드를 구현하도록 CSV 생성 논리를 변경했습니다.
Last-Modified 헤더도 재정의하기 전까지는 작동하지 않았습니다. this issue discussion을 참조하십시오.

이것은 컨트롤러 문제의 전체 코드입니다.

require 'content_disposition'

module Streamable
  extend ActiveSupport::Concern
  include ActionController::Live

  # Almost verbatim copy of new Rails 7 method.
  # See https://edgeapi.rubyonrails.org/classes/ActionController/Live.html#method-i-send_stream
  def send_stream(filename:, disposition: 'attachment', type: nil)
    response.headers['Content-Type'] =
      (type.is_a?(Symbol) ? Mime[type].to_s : type) ||
      Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete('.')) ||
      'application/octet-stream'

    response.headers['Content-Disposition'] = ContentDisposition.format(disposition: disposition, filename: filename)
      # with rails 6 remove content_disposition gem and use:
      #  ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)

    # Extra: needed for streaming
    response.headers['Last-Modified'] = Time.now.httpdate

    yield response.stream
  ensure
    response.stream.close
  end
end



Rails 5.x의 경우 Content-Disposition 헤더를 설정하기 위해 추가 gem이 필요했습니다.
gem 'content_disposition', '~> 1.0' # Not needed on Rails 6
느린 더미 데이터를 생성하는 컨트롤러의 테스트 예:

class ExporterController < ApplicationController
  include Streameable

  def index
    respond_to do |format|
      format.html # index.html
      format.js   # index.js
      format.csv do
        send_stream(attachment_opts) do |stream|
          stream.write "email_address,updated_at\n"

          50.times.each do |i|
            line = "pepe_#{i}@acme.com,#{Time.zone.now}\n"
            stream.write line
            puts line
            sleep 1  # force slow response
          end
        end
      end
    end
  end

  private

  def attachment_opts
    {
      filename: "data_#{Time.zone.now.to_i}.csv",
      disposition: 'attachment',
      type: 'text/csv'
    }
  end
end


마지막으로 curl과 같은 것을 사용하면 출력이 초 단위로 생성되는 것을 볼 수 있습니다.

curl -i http://localhost:3000/exporter





이제 앱에서 챔피언처럼 대용량 데이터를 스트리밍할 수 있습니다!.

이 앱을 Heroku app에 배포했고 코드는 this github repo에 있습니다.

추가 성능 팁. ActiveRecord를 사용하여 DB에서 수많은 레코드를 가져오는 경우 #each 대신 #find_each를 사용하여 일괄적으로 레코드를 가져올 수 있습니다.

이것이 도움이 되었는지 알려주세요.
즐거운 Rails 해킹!

좋은 웹페이지 즐겨찾기