web-dev-qa-db-ja.com

Ruby on Rails 3:Streaming through through Rails= client

私はRuby on Railsアプリは、RackSpaceクラウドファイルと通信します(Amazon S3に似ていますが、いくつかの機能がありません)に取り組んでいます)。

オブジェクトごとのアクセス許可とクエリ文字列認証を利用できないため、ユーザーへのダウンロードはアプリケーションを介して行う必要があります。

Rails 2.3では、次のように動的に応答を作成できるようです:

# Streams about 180 MB of generated data to the browser.
render :text => proc { |response, output|
  10_000_000.times do |i|
    output.write("This is line #{i}\n")
  end
}

http://api.rubyonrails.org/classes/ActionController/Base.html#M000464 から)

の代わりに 10_000_000.times... cloudfilesストリーム生成コードをそこにダンプできます。

問題は、このテクニックをRails 3.で使用しようとしたときに得られる出力です。

#<Proc:0x000000010989a6e8@/Users/jderiksen/lt/lt-uber/site/app/controllers/prospect_uploads_controller.rb:75>

おそらくprocオブジェクトのcallメソッドが呼び出されていないようですか?他のアイデアはありますか?

44
jkndrkn

これはRails 3では利用できないようです

https://Rails.lighthouseapp.com/projects/8994/tickets/2546-render-text-proc

これは私のコントローラーで私のために働くように見えました:

self.response_body =  proc{ |response, output|
  output.write "Hello world"
}
16
Steven Yelton

response_bodyに応答するオブジェクトを#eachに割り当てます。

class Streamer
  def each
    10_000_000.times do |i|
      yield "This is line #{i}\n"
    end
  end
end

self.response_body = Streamer.new

1.9.xまたは Backports gemを使用している場合、 Enumerator.new を使用してこれをよりコンパクトに記述できます。

self.response_body = Enumerator.new do |y|
  10_000_000.times do |i|
    y << "This is line #{i}\n"
  end
end

データがフラッシュされるタイミングとタイミングは、使用されているラックハンドラーと基になるサーバーに依存することに注意してください。たとえばMongrelがデータをストリーミングすることを確認しましたが、他のユーザーは、応答が閉じられるまで、たとえばWEBrickがデータをバッファリングすると報告しています。応答を強制的にフラッシュする方法はありません。

Rails 3.0.xでは、いくつかの追加の問題があります:

  • 開発モードでは、列挙内からモデルクラスにアクセスするなどの処理は、クラスの再読み込みとの相互作用が悪いために問題になる可能性があります。これは 未解決のバグ Rails 3.0.x.
  • RackとRailsの間の相互作用のバグにより、リクエストごとに#eachが2回呼び出されます。これは別の オープンバグ です。回避できます次のサルのパッチを使用:

    class Rack::Response
      def close
        @body.close if @body.respond_to?(:close)
      end
    end
    

両方の問題はRails 3.1で修正されており、HTTPストリーミングはマーキー機能です。

他の一般的な提案であるself.response_body = proc {|response, output| ...}はRails 3.0.xで機能しますが、3.1で非推奨になりました(実際にはデータをストリーミングしなくなります)。オブジェクトの割り当て#eachに応答するすべてのRails 3バージョンで動作します。

69
John

上記のすべての投稿のおかげで、ここに大きなCSVをストリーミングするための完全に機能するコードがあります。このコード:

  1. 追加の宝石は必要ありません。
  2. Model.find_each()を使用して、一致するすべてのオブジェクトでメモリを膨張させないようにします。
  3. Rails 3.2.5、Ruby 1.9.3およびherokuで、単一のdynoを使用したUnicornを使用してテストされています。
  4. 500行ごとにGC.startを追加して、heroku dynoの許可されたメモリを圧迫しないようにします。
  5. モデルのメモリフットプリントによっては、GC.startを調整する必要がある場合があります。これを使用して105Kモデルを9.7MBのcsvに問題なくストリーミングすることに成功しました。

コントローラメソッド:

def csv_export
  respond_to do |format|
    format.csv {
      @filename = "responses-#{Date.today.to_s(:db)}.csv"
      self.response.headers["Content-Type"] ||= 'text/csv'
      self.response.headers["Content-Disposition"] = "attachment; filename=#{@filename}"
      self.response.headers['Last-Modified'] = Time.now.ctime.to_s

      self.response_body = Enumerator.new do |y|
        i = 0
        Model.find_each do |m|
          if i == 0
            y << Model.csv_header.to_csv
          end
          y << sr.csv_array.to_csv
          i = i+1
          GC.start if i%500==0
        end
      end
    }
  end
end

config/Unicorn.rb

# Set to 3 instead of 4 as per http://michaelvanrooijen.com/articles/2011/06/01-more-concurrency-on-a-single-heroku-dyno-with-the-new-celadon-cedar-stack/
worker_processes 3

# Change timeout to 120s to allow downloading of large streamed CSVs on slow networks
timeout 120

#Enable streaming
port = ENV["PORT"].to_i
listen port, :tcp_nopush => false

Model.rb

  def self.csv_header
    ["ID", "Route", "username"]
  end

  def csv_array
    [id, route, username]
  end
23
paneer_tikka

#eachメソッドに応答するオブジェクトをresponse_bodyに割り当て、応答が閉じるまでバッファリングする場合は、アクションコントローラで試してください。

self.response.headers ['Last-Modified'] = Time.now.to_s

7
Exequiel

参考までに、Rails> = 3.1では、#eachメソッドに応答するオブジェクトをコントローラーの応答に割り当てることで、データを簡単にストリーミングできます。

すべてはここで説明されています: http://blog.sparqcode.com/2012/02/04/streaming-data-with-Rails-3-1-or-3-2/

5
moumar

さらに、自分で'Content-Length'ヘッダーを設定する必要があります。

そうでない場合、Rackは長さを決定するために(ボディデータをメモリにバッファする)待機する必要があります。そして、それは上記の方法を使用してあなたの努力を台無しにします。

私の場合、長さを決定することができました。できない場合は、'Content-Length'ヘッダーなしで本文を送信するようにRackを作成する必要があります。 「require」の後に「run」の前にconfig.ruに「use Rack :: Chunked」を追加してみてください。 (ありがとうアルカディ)

2
shuji.koike

はい、response_bodyはRails現時点でこれを行う3つの方法: https://Rails.lighthouseapp.com/projects/8994/tickets/4554-render-text- proc-regression

2
Daniel Cadenas

これで私の問題も解決しました-CSVファイルをgzipしていて、解凍したCSVとしてユーザーに送信したいので、GzipReaderを使用して一度に1行ずつ読み取ります。

これらの行は、大きなファイルをダウンロードとして配信する場合にも役立ちます。

self.response.headers["Content-Type"] = "application/octet-stream" self.response.headers["Content-Disposition"] = "attachment; filename=#{filename}"

2
Matt Hucke

エクソンエルの提案と共にジョンの解決策を適用することは私にとってうまくいきました。

ステートメント

self.response.headers['Last-Modified'] = Time.now.to_s

応答をラックでキャッシュ不可としてマークします。

さらに調査した後、私はこれも使用できると考えました:

headers['Cache-Control'] = 'no-cache'

これは、私にとっては少しだけ直感的です。それは私のコードを読んでいる可能性のある他の人にメッセージを伝えます。また、将来のバージョンのラックがLast-Modifiedのチェックを停止した場合、多くのコードが壊れる可能性があり、その理由を理解するのにしばらく時間がかかる場合があります。

1
Yogesh Nachnani

私は灯台のチケットでコメントしましたが、成功するにはWEBrickではなくMongrelを使用する必要がありましたが、self.response_body = procアプローチがうまく機能したことを伝えたかっただけです。

マーティン

1
Martin