I am working on a Ruby on Rails app that communicates with RackSpace cloudfiles (similar to Amazon S3 but lacking some features).
Due to the lack of the availability of per-object access permissions and query string authentication, downloads to users have to be mediated through an application.
In Rails 2.3, it looks like you can dynamically build a response as follows:
# 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
}
(from http://api.rubyonrails.org/classes/ActionController/Base.html#M000464)
Instead of 10_000_000.times...
I could dump my cloudfiles stream generation code in there.
Trouble is, this is the output I get when I attempt to use this technique in Rails 3.
#<Proc:0x000000010989a6e8@/Users/jderiksen/lt/lt-uber/site/app/controllers/prospect_uploads_controller.rb:75>
Looks like maybe the proc object's call
method is not being called? Any other ideas?
Assign to response_body
an object that responds to #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
If you are using 1.9.x or the Backports gem, you can write this more compactly using Enumerator.new
:
self.response_body = Enumerator.new do |y|
10_000_000.times do |i|
y << "This is line #{i}\n"
end
end
Note that when and if the data is flushed depends on the Rack handler and underlying server being used. I have confirmed that Mongrel, for instance, will stream the data, but other users have reported that WEBrick, for instance, buffers it until the response is closed. There is no way to force the response to flush.
In Rails 3.0.x, there are several additional gotchas:
A bug in the interaction between Rack and Rails causes #each
to be called twice for each request. This is another open bug. You can work around it with the following monkey patch:
class Rack::Response
def close
@body.close if @body.respond_to?(:close)
end
end
Both problems are fixed in Rails 3.1, where HTTP streaming is a marquee feature.
Note that the other common suggestion, self.response_body = proc {|response, output| ...}
, does work in Rails 3.0.x, but has been deprecated (and will no longer actually stream the data) in 3.1. Assigning an object that responds to #each
works in all Rails 3 versions.
Thanks to all the posts above, here is fully working code to stream large CSVs. This code:
Controller Method:
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
It looks like this isn't available in Rails 3
https://rails.lighthouseapp.com/projects/8994/tickets/2546-render-text-proc
This appeared to work for me in my controller:
self.response_body = proc{ |response, output|
output.write "Hello world"
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With