2010-08-17 10 views
44

Estoy trabajando en una aplicación Ruby on Rails que se comunica con los archivos en la nube RackSpace (similar a Amazon S3 pero que carece de algunas funciones).Ruby on Rails 3: Transmisión de datos a través de Rails al cliente

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 se puede construir 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 
} 

(desde http://api.rubyonrails.org/classes/ActionController/Base.html#M000464)

En lugar de 10_000_000.times... que podría volcar mis CloudFiles flujo de generación de código en ese país.

El problema es que esta es la salida que consigo cuando intento utilizar esta técnica en Rails 3.

#<Proc:[email protected]/Users/jderiksen/lt/lt-uber/site/app/controllers/prospect_uploads_controller.rb:75> 

Parece que tal vez no se está llamando el método del objeto proc call? ¿Alguna otra idea?

Respuesta

16

Parece que este no está disponible en Rails 3

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

Esto parecía trabajar para mí en mi controlador:

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

No funciona a partir de Rails 3.1. Ver la respuesta de John – m33lky

+0

No funciona en 3.2 tampoco. Ver http://stackoverflow.com/a/4320399/850996 debajo de –

1

Comenté en el ticket del faro, solo quería decir el self.response_body = pro El enfoque c funcionó para mí, aunque necesitaba usar Mongrel en lugar de WEBrick para tener éxito.

Martin

69

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 Backports joya, puede escribir esto de forma más compacta usando 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 el servidor subyacente siendo utilizado. 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 varias trampas adicionales:

  • En el modo de desarrollo, haciendo cosas tales como el acceso a las clases del modelo desde dentro de la enumeración puede ser problemático debido a las malas interacciones con la recarga de clases. Este es un open bug en Rails 3.0.x.
  • Un error en la interacción entre Rack y Rails hace que se llame a #each dos veces para cada solicitud. Este es otro open bug.Puede trabajar alrededor de ella con el siguiente parche mono:

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

Ambos problemas están fijados en Rails 3.1, donde el flujo HTTP es una característica marquesina.

Tenga en cuenta que la otra sugerencia común, self.response_body = proc {|response, output| ...}, funciona en Rails 3.0.x, pero ha quedado obsoleta (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.

+1

respuesta invaluable, gracias. Lo usé para implementar plantillas de transmisión para el archivo csv: https://github.com/fawce/csv_builder – fawce

+0

Muchas gracias. ¿Por qué estos métodos están en desuso y no hay forma oficial de transmitir datos? – m33lky

+1

desafortunadamente esta solución no funciona para mí. Comencé una nueva discusión aquí [link] (http://stackoverflow.com/questions/14356704/rails-3-2-streaming-data) – dc10

2

Esto también resolvió mi problema - Tengo archivos gzip'd CSV, quiero enviarlos al usuario como CSV descomprimido, así que los leo línea a línea usando un GzipReader.

Estas líneas también son útiles si usted está tratando de entregar un archivo grande como una descarga:

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

7

En caso de que va a asignar a response_body un objeto que responde a #each método y es el búfer hasta que la respuesta está cerrado, trate de controlador de acción:

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

+2

¡Esta fue la solución para mí! Aunque, necesitaba formatear el tiempo de la siguiente manera: Time.now.ctime.to_s –

+0

He buscado por un tiempo para encontrar esta respuesta. No entiendo por qué cuando no especificas el encabezado, no transmite ... de todos modos, agregar esta línea me funcionó. tx – joel1di1

2

Además, tendrá que ajustar la encabezado 'Content-Length' por su cuenta propia.

De lo contrario, Rack deberá 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 los que no puede hacerlo, debe hacer que Rack comience a enviar el cuerpo sin un encabezado 'Content-Length'. Intenta agregar en config.ru "use Rack :: Chunked" después de 'require' antes de 'run'. (Gracias arkadiy)

+0

Si no conoce la longitud, puede intentar agregarla a config.ru "use Rack :: Chunked" después de 'require' antes de 'ejecutar' – arkadiy

22

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 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 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 
1

La aplicación de 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 no almacenable en caché en el bastidor.

Después de investigar más a fondo, pensé que se podría también utilizar esto:

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

Esto, para mí, es sólo 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é.