46

我正在开发一个与 RackSpace 云文件通信的 Ruby on Rails 应用程序(类似于 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>

看起来可能call没有调用 proc 对象的方法?还有其他想法吗?

4

10 回答 10

70

分配给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每个请求被调用两次。这是另一个开放的错误。您可以使用以下猴子补丁解决它:

    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 版本中都有效。

于 2010-12-01T01:17:04.177 回答
24

感谢上面的所有帖子,这里是流式传输大型 CSV 的完整工作代码。这段代码:

  1. 不需要任何额外的宝石。
  2. 使用 Model.find_each() 以免内存因所有匹配的对象而膨胀。
  3. 已经在 rails 3.2.5、ruby 1.9.3 和 heroku 上使用 unicorn 进行了测试,带有单个 dyno。
  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

配置/独角兽.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

模型.rb

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

  def csv_array
    [id, route, username]
  end
于 2012-07-08T21:25:29.603 回答
16

看起来这在 Rails 3 中不可用

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

这在我的控制器中似乎对我有用:

self.response_body =  proc{ |response, output|
  output.write "Hello world"
}
于 2010-10-04T16:01:31.307 回答
8

如果您为 response_body 分配一个响应 #each 方法的对象并且它正在缓冲直到响应关闭,请尝试在动作控制器中:

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

于 2012-04-20T20:02:31.910 回答
5

仅作记录,rails >= 3.1 通过将响应 #each 方法的对象分配给控制器的响应,有一种简单的方法来流式传输数据。

一切都在这里解释:http: //blog.sparqcode.com/2012/02/04/streaming-data-with-rails-3-1-or-3-2/

于 2012-03-14T10:00:15.897 回答
2

是的,response_body 是目前执行此操作的 Rails 3 方式:https ://rails.lighthouseapp.com/projects/8994/tickets/4554-render-text-proc-regression

于 2010-10-08T01:12:33.077 回答
2

这也解决了我的问题 - 我有 gzip 的 CSV 文件,想以解压缩的 CSV 格式发送给用户,所以我使用 GzipReader 一次读取一行。

如果您尝试以下载的形式提供大文件,这些行也很有帮助:

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

于 2011-08-30T23:28:35.270 回答
2

此外,您必须自己设置“Content-Length”标头。

如果没有,Rack 将不得不等待(将主体数据缓冲到内存中)来确定长度。使用上述方法会毁掉你的努力。

就我而言,我可以确定长度。如果不能,您需要让 Rack 开始发送没有“Content-Length”标头的正文。尝试在 'run' 之前的 'require' 之后添加到 config.ru “use Rack::Chunked”。(感谢阿卡迪)

于 2012-06-27T22:59:18.603 回答
1

我在灯塔票中发表了评论,只是想说 self.response_body = proc 方法对我有用,尽管我需要使用 Mongrel 而不是 WEBrick 才能成功。

马丁

于 2010-11-09T14:19:16.040 回答
1

应用约翰的解决方案和 Exequiel 的建议对我有用。

该声明

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

在机架中将响应标记为不可缓存。

在进一步调查后,我认为也可以使用这个:

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

对我来说,这只是稍微直观一些。它将消息传达给可能正在阅读我的代码的任何其他人。此外,如果机架的未来版本停止检查 Last-Modified ,很多代码可能会中断,人们可能需要一段时间才能弄清楚原因。

于 2013-01-16T06:11:41.643 回答