Ruby on Rails 3: Transmisión de datos a través de Rails al cliente
ruby-on-rails streaming (10)
Estoy trabajando en una aplicación de Ruby on Rails que se comunica con los archivos en la nube RackSpace (similar a Amazon S3 pero que carece de algunas funciones).
Debido a la falta de disponibilidad de permisos de acceso por objeto y autenticación de cadena de consulta, las descargas a los usuarios deben ser mediadas a través de una aplicación.
En Rails 2.3, parece que puede generar dinámicamente una respuesta de la siguiente manera:
# 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
}
(de http://api.rubyonrails.org/classes/ActionController/Base.html#M000464 )
En lugar de 10_000_000.times...
podría volcar allí el código de generación de flujo de mis archivos en la nube.
El problema es que este es el resultado que obtengo cuando intento usar esta técnica en Rails 3.
#<Proc:0x000000010989a6e8@/Users/jderiksen/lt/lt-uber/site/app/controllers/prospect_uploads_controller.rb:75>
Parece que tal vez no se está call
método de call
del objeto de proceso. ¿Alguna otra idea?
Además, tendrá que configurar el encabezado ''Content-Length'' por su cuenta.
De lo contrario, Rack tendrá que esperar (almacenar los datos del cuerpo en la memoria) para determinar la longitud. Y arruinará sus esfuerzos utilizando los métodos descritos anteriormente.
En mi caso, pude determinar la longitud. En los casos en que no puedas, debes hacer que Rack comience a enviar cuerpo sin un encabezado ''Content-Length'' . Intenta agregar en config.ru "use Rack :: Chunked" después de "require" antes de la "ejecución". (Gracias arkadiy)
Aplicar la solución de John junto con la sugerencia de Exequiel funcionó para mí.
La declaración
self.response.headers[''Last-Modified''] = Time.now.to_s
marca la respuesta como no almacenable en rack.
Después de investigar más, pensé que uno también podría usar esto:
headers[''Cache-Control''] = ''no-cache''
Esto, para mí, es solo un poco más intuitivo. Transmite el mensaje a cualquier otra persona que pueda estar leyendo mi código. Además, en caso de que una versión futura del rack deje de verificar Last-Modified, es posible que se rompa una gran cantidad de código y que la gente tarde en descubrir por qué.
Asignar a response_body
un objeto que responde a #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
Si está utilizando 1.9.x o la gema Backports , puede escribir esto de forma más compacta utilizando Enumerator.new
:
self.response_body = Enumerator.new do |y|
10_000_000.times do |i|
y << "This is line #{i}/n"
end
end
Tenga en cuenta que cuando y si los datos se vacían depende del controlador de Rack y del servidor subyacente que se esté utilizando. He confirmado que Mongrel, por ejemplo, transmitirá los datos, pero otros usuarios han informado que WEBrick, por ejemplo, lo almacena en búfer hasta que se cierra la respuesta. No hay forma de forzar la respuesta al enjuague.
En Rails 3.0.x, hay varios errores adicionales:
- En el modo de desarrollo, hacer cosas como acceder a las clases de modelo desde la enumeración puede ser problemático debido a las malas interacciones con la recarga de clase. Este es un error abierto en Rails 3.0.x.
Un error en la interacción entre Rack y Rails hace que
#each
sea llamado dos veces para cada solicitud. Este es otro error abierto . Puede solucionarlo con el siguiente parche de mono:class Rack::Response def close @body.close if @body.respond_to?(:close) end end
Ambos problemas están solucionados en Rails 3.1, donde la transmisión HTTP es una función de marquesina.
Tenga en cuenta que la otra sugerencia común, self.response_body = proc {|response, output| ...}
self.response_body = proc {|response, output| ...}
, funciona en Rails 3.0.x, pero ha quedado obsoleto (y ya no transmitirá los datos) en 3.1. La asignación de un objeto que responde a #each
funciona en todas las versiones de Rails 3.
Comenté en el boleto del faro, solo quería decir que el enfoque self.response_body = proc funcionó para mí, aunque necesitaba usar Mongrel en lugar de WEBrick para tener éxito.
Martín
En caso de que esté asignando a response_body un objeto que responda a cada uno de los métodos y está almacenando en el búfer hasta que se cierre la respuesta, intente en el controlador de acciones:
self.response.headers [''Last-Modified''] = Time.now.to_s
Esto también resolvió mi problema: tengo archivos gzip''d CSV, quiero enviarlos al usuario como CSV descomprimido, así que los leo en línea a la vez usando un GzipReader.
Estas líneas también son útiles si intentas entregar un archivo grande como descarga:
self.response.headers["Content-Type"] = "application/octet-stream" self.response.headers["Content-Disposition"] = "attachment; filename=#{filename}"
Gracias a todas las publicaciones anteriores, aquí está el código completamente operativo para transmitir grandes CSV. Este código:
- No requiere gemas adicionales.
- Utiliza Model.find_each () para no hinchar la memoria con todos los objetos coincidentes.
- Ha sido probado en rieles 3.2.5, ruby 1.9.3 y heroku usando unicornio, con un solo dinamómetro.
- Agrega un GC.start en cada 500 filas, para no explotar la memoria permitida del heroku dyno.
- Es posible que deba ajustar GC.start dependiendo de la huella de memoria de su modelo. Lo he usado con éxito para transmitir 105K modelos en un csv de 9.7MB sin ningún problema.
Método del controlador:
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
Parece que esto no está disponible en Rails 3
https://rails.lighthouseapp.com/projects/8994/tickets/2546-render-text-proc
Esto pareció funcionar para mí en mi controlador:
self.response_body = proc{ |response, output|
output.write "Hello world"
}
Sí, response_body es la manera en que Rails 3 hace esto por el momento: https://rails.lighthouseapp.com/projects/8994/tickets/4554-render-text-proc-regression
Solo para el registro, los rieles> = 3.1 tienen una manera fácil de transmitir datos asignando un objeto que responda a # cada método a la respuesta del controlador.
Todo se explica aquí: http://blog.sparqcode.com/2012/02/04/streaming-data-with-rails-3-1-or-3-2/