ruby-on-rails streaming ruby-on-rails-3 cloudfiles

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:

  1. No requiere gemas adicionales.
  2. Utiliza Model.find_each () para no hinchar la memoria con todos los objetos coincidentes.
  3. Ha sido probado en rieles 3.2.5, ruby ​​1.9.3 y heroku usando unicornio, con un solo dinamómetro.
  4. Agrega un GC.start en cada 500 filas, para no explotar la memoria permitida del heroku dyno.
  5. 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