2010-08-17 86 views
44

我正在研究与RackSpace云文件(类似于Amazon S3但缺少某些功能)通信的Ruby on Rails应用程序。Ruby on Rails 3:通过Rails将数据流式传输到客户端

由于缺乏每个对象的访问权限和查询字符串认证的可用性,所以下载到用户必须通过应用程序进行调解。

在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:[email protected]/Users/jderiksen/lt/lt-uber/site/app/controllers/prospect_uploads_controller.rb:75> 

看起来也许PROC对象的call方法不会被调用?任何其他想法?

回答

16

它看起来这是不可用的Rails 3

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

这似乎为我在我的控制器工作:

self.response_body = proc{ |response, output| 
    output.write "Hello world" 
} 
+3

不适用于Rails 3.1。见约翰的答案。 – m33lky 2012-03-11 05:30:43

+0

在3.2中也不起作用。请参阅http://stackoverflow.com/a/4320399/850996下面的 – 2014-06-23 23:58:03

1

我在灯塔门票评论,只是想说self.response_body =亲c方法为我工作,虽然我需要使用Mongrel而不是WEBrick才能成功。

马丁

69

分配到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宝石,你可以这样写更紧凑使用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中的open bug
  • Rack与Rails之间的交互中的一个错误导致#each被调用两次为每个请求。这是另一个open bug。你可以解决它与下面的猴子补丁:

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

两个问题都固定在滑轨3.1,其中HTTP流是字幕功能。

请注意,另一个常见建议self.response_body = proc {|response, output| ...}确实在Rails 3.0.x中有效,但在3.1中已被弃用(并且不再实际流式传输数据)。指定一个响应#each的对象适用于所有Rails 3版本。

+1

非常宝贵的回应,谢谢。用它来实现csv文件的流模板:https://github.com/fawce/csv_builder – fawce 2011-08-11 15:01:03

+0

非常感谢。为什么这些方法不推荐,并且没有正式的数据流方式?! – m33lky 2012-03-11 05:13:11

+1

不幸的是,这个解决方案不适合我。我在这里开始一个新的讨论[链接](http://stackoverflow.com/questions/14356704/rails-3-2-streaming-data) – dc10 2013-01-16 10:57:08

2

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

这些线也是有帮助的,如果你想传递一个大文件下载:

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

7

如果你是分配给response_body,响应#each方法的对象和它的缓冲,直到响应被关闭,尝试在动作控制器:

自我。 response.headers [ '的Last-Modified'] = Time.now.to_s

+2

这是我的解决方案!虽然,我需要格式化时间,如下所示:Time.now.ctime.to_s – 2012-04-26 21:18:39

+0

我已经搜索了一段时间来找到这个响应。我不明白为什么当你不指定头文件时它不会流...无论如何,添加这条线为我工作。 tx – joel1di1 2013-09-11 12:35:35

2

此外,你将不得不通过你的自我设定'内容长度'头。

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

就我而言,我可以确定长度。 如果你不能,你需要让Rack开始发送没有'内容长度'标题的主体。 尝试在“运行”之前的'require'之后添加到config.ru“use Rack :: Chunked”中。 (谢谢arkadiy)

+0

如果你不知道长度,你可能会尝试添加到config.ru“use Rack :: Chunked”之后的'require'之前的'运行' – arkadiy 2012-09-25 05:13:19

22

感谢上面的所有帖子,这里是完全工作代码流大CSV。此代码:

  1. 不需要任何额外的宝石。
  2. 使用Model.find_each()以便不会使所有匹配对象的内存膨胀。
  3. 已经在rails 3.2.5上测试过, ruby​​ 1.9.3和heroku使用独角兽,单个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 

配置/ 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 
1

将John的解决方案与Exequiel的建议一起应用于我。

声明

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

标志着响应,在机架上不可缓存。

进一步调查之后,我想人们也可以使用此:

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

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