44 votes

Ruby on Rails 3: transmission de données en continu via Rails au client

Je suis en train de travailler sur Ruby on Rails application qui communique avec RackSpace cloudfiles (similaire à Amazon S3, mais manque un peu de fonctionnalités).

En raison de l'absence de la disponibilité de chaque objet autorisations d'accès et d'authentification de chaîne de requête, des téléchargements pour les utilisateurs doivent passer par l'intermédiaire d'une application.

Dans les Rails 2.3, il semble que vous pouvez générer dynamiquement une réponse comme suit:

# 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
}

(à partir de http://api.rubyonrails.org/classes/ActionController/Base.html#M000464)

Au lieu de 10_000_000.times... j'ai pu jeter mes cloudfiles flot de génération de code.

La difficulté est, c'est le résultat que j'obtiens quand j'essaie d'utiliser cette technique dans Rails 3.

#<Proc:0x000000010989a6e8@/Users/jderiksen/lt/lt-uber/site/app/controllers/prospect_uploads_controller.rb:75>

Ressemble peut-être que le proc de l'objet call méthode n'est pas appelée? D'autres idées?

69voto

John Points 9709

Il suffit d'attribuer à l' response_body un objet qui répond à l' #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 vous utilisez 1.9.x ou les Backports gem, vous pouvez écrire ceci de manière plus compacte à l'aide de Enumerator.new:

self.response_body = Enumerator.new do |y|
  10_000_000.times do |i|
    y << "This is line #{i}\n"
  end
end

Notez que quand et si les données sont vidées dépend du Rack gestionnaire et serveur sous-jacent utilisé. J'ai confirmé que la Mongrel, par exemple, le flux de données, mais d'autres utilisateurs ont rapporté que WEBrick, par exemple, les tampons jusqu'à ce que la réponse est fermé. Il n'y a aucun moyen de forcer la réponse à la chasse.

Dans Les Rails 3.0.x, il existe plusieurs autres problèmes:

  • Le mode de développement, de faire des choses telles que l'accès aux classes du modèle à partir de l'intérieur de l'énumération peut être problématique en raison de mauvaises interactions avec la classe de rechargement. C'est un bug ouvert dans les Rails 3.0.x.
  • Un bug dans l'interaction entre le bâti et les Rails provoque #each à être appelé deux fois pour chaque demande. C'est un autre bug ouvert. Vous pouvez contourner ce singe patch:

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

Les deux problèmes sont résolus dans les Rails 3.1, où le streaming HTTP est un rectangle de fonctionnalité.

Notez que les autres communes de la suggestion, self.response_body = proc {|response, output| ...}, ne travaillent dans des Rails 3.0.x, mais a été abandonné (et ne sera plus réellement de flux de données) 3.1. Affectation d'un objet qui répond à l' #each fonctionne dans tous les Rails 3 versions.

23voto

paneer_tikka Points 885

Merci à tous les postes ci-dessus, ici est entièrement code de travail pour diffuser de la grande CSVs. Ce code:

  1. Ne nécessite pas d'autres pierres précieuses.
  2. Utilise Le Modèle.find_each() afin de ne pas encombrer la mémoire avec tous les objets correspondants.
  3. A été testé sur des rails 3.2.5, ruby 1.9.3 et heroku à l'aide de la licorne, avec une seule puissance.
  4. Ajoute un GC.démarrer à tous les 500 lignes, afin de ne pas sauter le heroku dyno de l' permis de mémoire.
  5. Vous devrez peut-être ajuster la GC.commencez en fonction de votre Modèle de la mémoire. J'ai utilisé avec succès ce pour diffuser 105K des modèles dans un csv de 9,7 MO sans aucun problème.

Méthode De Contrôleur:

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/licorne.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

De modèle.rb

  def self.csv_header
    ["ID", "Route", "username"]
  end

  def csv_array
    [id, route, username]
  end

16voto

Steven Yelton Points 166

Il semble que cela ne soit pas disponible dans Rails 3

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

Cela a semblé fonctionner pour moi dans mon contrôleur:

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

7voto

Exequiel Points 61

Si vous attribuez à response_body un objet qui répond à la méthode #each et qu'il met en mémoire tampon jusqu'à la fermeture de la réponse, essayez dans le contrôleur d'action:

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

5voto

moumar Points 61

Pour le compte rendu, rails> = 3.1 offre un moyen facile de diffuser des données en affectant un objet qui répond à la méthode #each à la réponse du contrôleur.

Tout est expliqué ici: http://blog.sparqcode.com/2012/02/04/streaming-data-with-rails-3-1-or-3-2/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X