Ruby on Rails 3: Transmitindo dados através do Rails para o cliente

Eu estou trabalhando em um aplicativo Ruby on Rails que se comunica com cloudfiles RackSpace (semelhante ao Amazon S3, mas faltando alguns resources).

Devido à falta de disponibilidade de permissions de access por object e autenticação de cadeia de consulta, os downloads para os usuários precisam ser mediados por meio de um aplicativo.

No Rails 2.3, parece que você pode construir dinamicamente uma resposta da seguinte maneira:

# 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/classs/ActionController/Base.html#M000464 )

Em vez de 10_000_000.times... eu poderia despejar meu código de geração de stream cloudfiles lá.

O problema é que esta é a saída que recebo quando tento usar essa técnica no Rails 3.

 # 

Parece que talvez o método de call do object proc não esteja sendo chamado? Alguma outra ideia?

    Parece que isso não está disponível no Rails 3

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

    Isso pareceu funcionar para mim no meu controlador:

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

    Atribuir a response_body um object 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 

    Se você estiver usando o 1.9.x ou o Backports gem, você pode escrever isto de forma mais 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 

    Observe que quando e se os dados forem liberados depende do manipulador do Rack e do servidor subjacente que está sendo usado. Confirmei que o Mongrel, por exemplo, transmitirá os dados, mas outros usuários relataram que o WEBrick, por exemplo, armazena em buffer até que a resposta seja fechada. Não há como forçar a resposta a liberar.

    No Rails 3.0.x, existem várias dicas adicionais:

    • No modo de desenvolvimento, fazer coisas como acessar classs de modelo de dentro da enumeração pode ser problemático devido a interações ruins com o recarregamento de classs. Este é um bug aberto no Rails 3.0.x.
    • Um bug na interação entre o Rack e o Rails faz com que #each seja chamado duas vezes para cada solicitação. Este é outro bug aberto . Você pode contorná-lo com o seguinte patch de macaco:

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

    Ambos os problemas são corrigidos no Rails 3.1, onde o streaming HTTP é um recurso de seleção.

    Note que a outra sugestão comum, self.response_body = proc {|response, output| ...} self.response_body = proc {|response, output| ...} , funciona no Rails 3.0.x, mas foi preterido (e não irá mais transmitir os dados) em 3.1. Atribuindo um object que responde a #each trabalha em todas as versões do Rails 3.

    Graças a todos os posts acima, aqui está um código totalmente funcional para transmitir grandes CSVs. Este código:

    1. Não requer gemas adicionais.
    2. Usa Model.find_each () para não sobrecarregar a memory com todos os objects correspondentes.
    3. Foi testado em rails 3.2.5, ruby ​​1.9.3 e heroku usando unicórnio, com dyno único.
    4. Adiciona um GC.start a cada 500 linhas, para não explodir a memory permitida do heroku dyno.
    5. Você pode precisar ajustar o GC.start dependendo do tamanho da memory do seu modelo. Eu tenho usado com sucesso isso para transmitir modelos de 105K em um csv de 9,7MB sem quaisquer problemas.

    Método do 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 

    Caso você esteja atribuindo ao response_body um object que responda ao método #each e esteja em buffer até que a resposta seja fechada, tente no controlador de ação:

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

    Apenas para o registro, rails> = 3.1 tem uma maneira fácil de transmitir dados, atribuindo um object que responde ao método #each para a resposta do controlador.

    Tudo é explicado aqui: http://blog.sparqcode.com/2012/02/04/streaming-data-with-rails-3-1-or-3-2/

    Sim, response_body é a maneira do Rails 3 de fazer isso no momento: https://rails.lighthouseapp.com/projects/8994/tickets/4554-render-text-proc-regression

    Isso resolveu meu problema também – eu tenho arquivos CSV gzip’d, quero enviar para o usuário como CSV descompactado, então eu os leio uma linha de cada vez usando um GzipReader.

    Essas linhas também são úteis se você estiver tentando entregar um arquivo grande como download:

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

    Além disso, você terá que definir o header ‘Content-Length’ por sua auto.

    Caso contrário, o Rack terá que esperar (armazenando dados do corpo na memory) para determinar o comprimento. E isso arruinará seus esforços usando os methods descritos acima.

    No meu caso, eu poderia determinar o comprimento. Nos casos em que você não pode, você precisa fazer o Rack começar a enviar o corpo sem o header ‘Content-Length’ . Tente adicionar no config.ru “use Rack :: Chunked” depois de “require” antes do “run”. (Obrigado arkadiy)

    Comentei no ingresso do farol, só queria dizer que a abordagem self.response_body = proc funcionou para mim, embora eu precisasse usar o Mongrel em vez do WEBrick para ter sucesso.

    Martin

    Aplicar a solução de John junto com a sugestão de Exequiel funcionou para mim.

    A declaração

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

    marca a resposta como não-armazenável em rack.

    Depois de investigar mais, descobri que também se poderia usar isso:

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

    Isso, para mim, é apenas um pouco mais intuitivo. Ele transmite a mensagem para qualquer outra pessoa que esteja lendo meu código. Além disso, no caso de uma futura versão do rack parar de verificar Last-Modified, muito código pode quebrar e pode demorar um pouco para que as pessoas descubram o porquê.