Ruby on Rails 3: потоковая передача данных через Rails на клиент - PullRequest
44 голосов
/ 18 августа 2010

Я работаю над приложением Ruby on Rails, которое связывается с облачными файлами RackSpace (аналогично Amazon S3, но не имеет некоторых функций).

Из-за недостаточной доступности разрешений на доступ к объекту и аутентификации строки запроса загрузка пользователей должна осуществляться через приложение.

В Rails 2.3 похоже, что вы можете динамически построить ответ следующим образом:

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

(из http://api.rubyonrails.org/classes/ActionController/Base.html#M000464)

Вместо 10_000_000.times... я мог бы выбросить туда код генерации моего потока облачных файлов.

Проблема в том, что это вывод, который я получаю, когда пытаюсь использовать эту технику в Rails 3.

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

Похоже, может быть, метод call объекта proc не вызывается? Есть еще идеи?

Ответы [ 10 ]

69 голосов
/ 01 декабря 2010

Назначьте response_body объект, который отвечает на #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

Если вы используете 1.9.x или Backports gem, вы можете написать это более компактноиспользование Enumerator.new:

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

Обратите внимание, что когда и если данные сбрасываются, зависит от используемого обработчика Rack и используемого сервера.Я подтвердил, что Mongrel, например, будет передавать данные в потоковом режиме, но другие пользователи сообщали, что, например, WEBrick буферизует их до тех пор, пока ответ не будет закрыт.Нет способа заставить ответ сбросить.

В Rails 3.0.x есть несколько дополнительных ошибок:

  • В режиме разработки, например, доступ к классам модели извнутри перечисления могут возникнуть проблемы из-за плохого взаимодействия с перезагрузкой класса.Это открытая ошибка в Rails 3.0.x.
  • Ошибка во взаимодействии между Rack и Rails приводит к тому, что #each вызывается дважды для каждого запроса.Это еще один открытый баг .Вы можете обойти это с помощью следующего патча обезьяны:

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

Обе проблемы исправлены в Rails 3.1, где потоковая передача по HTTP является функцией выделения.

Обратите внимание, чтоДругое распространенное предложение, self.response_body = proc {|response, output| ...}, работает в Rails 3.0.x, но устарело (и больше не будет передавать данные) в версии 3.1.Назначение объекта, отвечающего #each, работает во всех версиях Rails 3.

23 голосов
/ 09 июля 2012

Благодаря всем постам выше, здесь есть полностью рабочий код для потоковой передачи больших CSV.Этот код:

  1. Не требует никаких дополнительных драгоценных камней.
  2. Использует Model.find_each (), чтобы не переполнять память всеми совпадающими объектами.
  3. Протестировано на рельсах 3.2.5, ruby ​​1.9.3 и героку с использованием единорога, с одним динамо,
  4. Добавляет GC.start через каждые 500 строк, чтобы не разрушать разрешенную память heroku dyno.
  5. Возможно, вам потребуется настроить GC.start в зависимости от объема памяти вашей модели.Я успешно использовал это для потоковой передачи моделей 105K в CSV размером 9,7 МБ без проблем.

Метод контроллера:

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
16 голосов
/ 04 октября 2010

Похоже, что это не доступно в Rails 3

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

Это работает для меня в моем контроллере:

self.response_body =  proc{ |response, output|
  output.write "Hello world"
}
7 голосов
/ 21 апреля 2012

Если вы назначаете response_body объект, который отвечает на метод #each, и он буферизует до закрытия ответа, попробуйте в действии контроллер:

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

5 голосов
/ 14 марта 2012

Только для записи, rails> = 3.1 имеет простой способ для потоковой передачи данных, назначив объект, который отвечает на метод #each, ответу контроллера.

Здесь все объясняется: http://blog.sparqcode.com/2012/02/04/streaming-data-with-rails-3-1-or-3-2/

2 голосов
/ 28 июня 2012

Кроме того, вам придется самостоятельно установить заголовок 'Content-Length' .

Если нет, Rack придется подождать (буферизуя данные тела в память), чтобыопределить длину.И это разрушит ваши усилия, используя методы, описанные выше.

В моем случае я мог бы определить длину.В тех случаях, когда вы не можете, вам нужно сделать Rack, чтобы начать отправку тела без заголовка Content-Length .Попробуйте добавить в config.ru «use Rack :: Chunked» после «require» перед «run».(Спасибо аркадий)

2 голосов
/ 31 августа 2011

Это также решило мою проблему - у меня есть файлы CSV gzip'd, я хочу отправить их пользователю как разархивированный CSV, поэтому я читаю их по очереди, используя GzipReader.

Эти строкитакже полезно, если вы пытаетесь доставить большой файл для загрузки:

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

2 голосов
/ 08 октября 2010

Да, response_body - это способ Rails 3 сделать это на данный момент: https://rails.lighthouseapp.com/projects/8994/tickets/4554-render-text-proc-regression

1 голос
/ 16 января 2013

Применение решения Джона вместе с предложением Экскиэля сработало для меня.

Оператор

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

помечает ответ как не кэшируемый в стойке.

После дальнейшего изучения я решил, что можно также использовать это:

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

Для меня это немного более интуитивно понятно.Он передает сообщение любому другому, кто может читать мой код.Кроме того, в случае, если будущая версия стойки прекратит проверку на наличие Last-Modified, может произойти сбой большого количества кода, и людям может потребоваться некоторое время, чтобы выяснить, почему.

1 голос
/ 09 ноября 2010

Я прокомментировал в билете маяка, просто хотел сказать, что подход self.response_body = proc работает для меня, хотя мне нужно было использовать Mongrel вместо WEBrick для успеха.

Martin

...