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?
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"
}
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:
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.
Merci à tous les articles ci-dessus, voici du code entièrement fonctionnel pour diffuser de gros CSV. Ce code:
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
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
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/
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)
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
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}"
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.
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