Ruby on Rails 3: потоковая передача данных через Rails для клиента

Я работаю над приложением 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:[email protected]/Users/jderiksen/lt/lt-uber/site/app/controllers/prospect_uploads_controller.rb:75>

Похоже, может быть, метод proc object call не вызывается? Любые другие идеи?

Ответ 2

Назначьте 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, вы можете записать это более компактно, используя 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.

Ответ 3

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

  • Не требует никаких дополнительных драгоценных камней.
  • Использует Model.find_each(), чтобы не раздувать память со всеми соответствующими объектами.
  • Был протестирован на рельсах 3.2.5, ruby 1.9.3 и heroku с использованием единорога, с одним дино.
  • Добавляет GC.start на каждые 500 строк, чтобы не взорвать dynok's heroku допустимая память.
  • Вам может потребоваться настроить GC.start в зависимости от области памяти модели. Я успешно использовал это для потоковой передачи 105K моделей в csv 9.7MB без каких-либо проблем.

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

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

конфигурации /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

Ответ 4

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

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

Ответ 5

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

Ответ 9

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

Martin

Ответ 10

Применение метода Джона вместе с предложением Exequiel сработало для меня.

Утверждение

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

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

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

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

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