web-dev-qa-db-fra.com

Ruby on Rails 3: Streaming de données via Rails vers le client)

Je travaille sur un Ruby on Rails app qui communique avec les fichiers cloud RackSpace (similaire à Amazon S3 mais manquant de fonctionnalités).

En raison du manque de disponibilité d'autorisations d'accès par objet et d'authentification de chaîne de requête, les téléchargements vers les utilisateurs doivent être négociés via une application.

Dans Rails 2.3, il semble que vous pouvez créer 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... Je pourrais y vider mon code de génération de flux cloudfiles.

Le problème est que c'est la sortie que j'obtiens lorsque j'essaie d'utiliser cette technique dans Rails 3.

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

Il semble que la méthode call de l'objet proc ne soit pas appelée? D'autres idées?

44
jkndrkn

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"
}
16
Steven Yelton

Affecter à response_body un objet qui répond à #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 la gemme Backports , vous pouvez l'écrire de manière plus compacte en utilisant 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, cela dépend du gestionnaire de rack et du serveur sous-jacent utilisé. J'ai confirmé que Mongrel, par exemple, diffusera les données, mais d'autres utilisateurs ont signalé que WEBrick, par exemple, les mettait en mémoire tampon jusqu'à la fermeture de la réponse. Il n'y a aucun moyen de forcer la réponse à vider.

Dans Rails 3.0.x, il existe plusieurs gotchas supplémentaires:

  • En mode développement, faire des choses comme accéder aux classes de modèle depuis l'énumération peut être problématique en raison de mauvaises interactions avec le rechargement de classe. Il s'agit d'un bogue ouvert in Rails 3.0.x.
  • Un bug dans l'interaction entre Rack et Rails provoque #each à appeler deux fois pour chaque demande. Ceci est un autre bogue ouvert . Vous pouvez le contourner avec le patch singe suivant:

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

Les deux problèmes sont résolus dans Rails 3.1, où le streaming HTTP est une fonctionnalité Marquee.

Notez que l'autre suggestion courante, self.response_body = proc {|response, output| ...}, fonctionne dans Rails 3.0.x, mais est obsolète (et ne diffusera plus réellement les données) dans 3.1. Assigner un objet qui répond à #each fonctionne dans toutes les versions Rails 3.

69
John

Merci à tous les articles ci-dessus, voici du code entièrement fonctionnel pour diffuser de gros CSV. Ce code:

  1. Ne nécessite pas de gemmes supplémentaires.
  2. Utilise Model.find_each () afin de ne pas surcharger la mémoire avec tous les objets correspondants.
  3. A été testé sur Rails 3.2.5, Ruby 1.9.3 et heroku utilisant Unicorn, avec dyno unique).
  4. Ajoute un GC.start toutes les 500 lignes, afin de ne pas faire exploser la mémoire autorisée du dyno heroku.
  5. Vous devrez peut-être ajuster le GC.start en fonction de l'empreinte mémoire de votre modèle. J'ai réussi à l'utiliser pour diffuser des modèles 105K dans un fichier csv de 9,7 Mo sans aucun problème.

Méthode du 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/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
23
paneer_tikka

Dans le cas où vous affectez à response_body un objet qui répond à la méthode #each et qui est en mémoire tampon jusqu'à ce que la réponse soit fermée, essayez dans le contrôleur d'action:

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

7
Exequiel

Juste pour mémoire, Rails> = 3.1 a 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/

5
moumar

De plus, vous devrez définir l'en-tête 'Content-Length' par vous-même.

Sinon, Rack devra attendre (mettre les données du corps en mémoire tampon) pour déterminer la longueur. Et cela ruinera vos efforts en utilisant les méthodes décrites ci-dessus.

Dans mon cas, j'ai pu déterminer la longueur. Dans le cas contraire, vous devez faire en sorte que Rack commence à envoyer le corps sans un en-tête 'Content-Length'. Essayez d'ajouter dans config.ru "use Rack :: Chunked" après "require" avant "run". (Merci Arkadiy)

2
shuji.koike

Oui, response_body est la façon Rails 3 de le faire pour le moment: https://Rails.lighthouseapp.com/projects/8994/tickets/4554-render-text- proc-regression

2
Daniel Cadenas

Cela a également résolu mon problème - j'ai des fichiers CSV compressés, je veux les envoyer à l'utilisateur au format CSV décompressé, donc je les lis une ligne à la fois en utilisant un GzipReader.

Ces lignes sont également utiles si vous essayez de livrer un gros fichier en téléchargement:

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

2
Matt Hucke

L'application de la solution de John avec la suggestion d'Exequiel a fonctionné pour moi.

La déclaration

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

marque la réponse comme non-cache dans le rack.

Après une enquête plus approfondie, j'ai pensé que l'on pourrait également utiliser ceci:

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

Pour moi, c'est juste un peu plus intuitif. Il transmet le message à tous ceux qui pourraient lire mon code. De plus, dans le cas où une future version de rack cesse de vérifier la dernière modification, beaucoup de code peut se casser et cela peut prendre un certain temps aux gens pour comprendre pourquoi.

1
Yogesh Nachnani

J'ai commenté dans le ticket du phare, je voulais juste dire que l'approche self.response_body = proc a fonctionné pour moi, même si j'avais besoin d'utiliser Mongrel au lieu de WEBrick pour réussir.

Martin

1
Martin