Ruby on Rails 3: Streaming dei dati tramite Rails al client

Sto lavorando a un’app Ruby on Rails che comunica con i file cloud di RackSpace (simile a Amazon S3 ma privo di alcune funzionalità).

A causa della mancanza di disponibilità delle autorizzazioni di accesso per object e dell’autenticazione della stringa di query, i download agli utenti devono essere mediati tramite un’applicazione.

In Rails 2.3, sembra che puoi build dynamicmente una risposta come segue:

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

(da http://api.rubyonrails.org/classs/ActionController/Base.html#M000464 )

Invece di 10_000_000.times... Potrei scaricare il mio codice di generazione del stream di cloudfile.

Il problema è che questo è l’output che ottengo quando tento di utilizzare questa tecnica in Rails 3.

 # 

Sembra che il metodo di call dell’object proc non venga chiamato? Altre idee?

Sembra che questo non sia disponibile in Rails 3

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

Questo sembrava funzionare per me nel mio controller:

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

Assegna a response_body un object che risponde 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 si utilizza 1.9.x o la gem Backports , è ansible scrivere in modo più compatto utilizzando Enumerator.new :

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

Si noti che quando e se i dati vengono svuotati dipende dal gestore di rack e dal server sottostante in uso. Ho confermato che Mongrel, ad esempio, trasmetterà i dati in streaming, ma altri utenti hanno segnalato che WEBrick, ad esempio, lo memorizza fino a quando la risposta non viene chiusa. Non c'è modo di forzare la risposta al colore.

In Rails 3.0.x, ci sono diversi trucchi aggiuntivi:

  • In modalità di sviluppo, fare cose come accedere alle classi di modelli dall'enumerazione può essere problematico a causa di cattive interazioni con il ricaricamento della class. Questo è un bug aperto in Rails 3.0.x.
  • Un bug nell'interazione tra Rack e Rails fa sì che #each venga chiamato due volte per ogni richiesta. Questo è un altro bug aperto . Puoi aggirarlo con la seguente patch per le scimmie:

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

Entrambi i problemi sono stati risolti in Rails 3.1, in cui lo streaming HTTP è una funzione di selezione.

Si noti che l'altro suggerimento comune, self.response_body = proc {|response, output| ...} self.response_body = proc {|response, output| ...} , funziona in Rails 3.0.x, ma è stato deprecato (e non scorrerà più effettivamente i dati) in 3.1. Assegnare un object che risponde a #each funziona in tutte le versioni di Rails 3.

Grazie a tutti i post sopra, qui è completamente funzionante il codice per lo streaming di grandi CSV. Questo codice:

  1. Non richiede gemme aggiuntive.
  2. Utilizza Model.find_each () in modo da non ingigantire la memoria con tutti gli oggetti corrispondenti.
  3. È stato testato su rails 3.2.5, ruby ​​1.9.3 e heroku con unicorno, con singolo dyno.
  4. Aggiunge un GC.start ad ogni 500 righe, in modo da non soffiare la memoria permessa dal heroku dyno.
  5. Potrebbe essere necessario regolare GC.start a seconda dell’impronta di memoria del modello. Ho usato con successo questo per lo streaming di modelli 105K in un csv di 9.7 MB senza problemi.

Metodo del controller:

 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 

Nel caso in cui si assegni a response_body un object che risponde al metodo #each e viene eseguito il buffering fino a quando la risposta non viene chiusa, provare in action controller:

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

Solo per la cronaca, rails> = 3.1 ha un modo semplice per lo streaming dei dati assegnando un object che risponde al metodo #each alla risposta del controller.

Tutto è spiegato qui: http://blog.sparqcode.com/2012/02/04/streaming-data-with-rails-3-1-or-3-2/

Sì, response_body è il modo Rails 3 per farlo al momento: https://rails.lighthouseapp.com/projects/8994/tickets/4554-render-text-proc-regression

Questo ha risolto anche il mio problema: ho i file CSV di gzip, che voglio inviare all’utente come CSV non compresso, quindi li leggo una riga alla volta usando GzipReader.

Queste linee sono anche utili se stai cercando di consegnare un file di grandi dimensioni come download:

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

Inoltre, dovrai impostare l’intestazione ‘Content-Length’ da te stesso.

In caso contrario, Rack dovrà attendere (bufferizzare i dati del corpo in memoria) per determinare la lunghezza. E rovinerà i tuoi sforzi usando i metodi descritti sopra.

Nel mio caso, potrei determinare la lunghezza. Nei casi in cui non è ansible, è necessario creare Rack per avviare l’invio del corpo senza un’intestazione “Content-Length” . Prova ad aggiungere in config.ru “usa Rack :: Chunked” dopo “require” prima di “run”. (Grazie arkadiy)

Ho commentato il biglietto del faro, volevo solo dire che l’approccio self.response_body = proc ha funzionato per me anche se avevo bisogno di usare Mongrel invece di WEBrick per avere successo.

balestruccio

L’applicazione della soluzione di John insieme al suggerimento di Exequiel ha funzionato per me.

La dichiarazione

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

contrassegna la risposta come non memorizzabile nella cache nel rack.

Dopo aver esaminato ulteriormente, ho pensato che si potesse usare anche questo:

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

Questo, per me, è solo leggermente più intuitivo. Trasmette il messaggio a chiunque altro stia leggendo il mio codice. Inoltre, nel caso in cui una versione futura di rack smettesse di cercare Last-Modified, un sacco di codice potrebbe rompersi e potrebbe essere un po ‘di tempo per le persone per capire perché.