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... я мог бы сбросить туда свой код генерации потоков cloudfiles.

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

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

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


person jkndrkn    schedule 17.08.2010    source источник


Ответы (10)


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

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

Похоже, это сработало для меня в моем контроллере:

self.response_body =  proc{ |response, output|
  output.write "Hello world"
}
person Steven Yelton    schedule 04.10.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, вы можете написать это более компактно, используя 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.

person John    schedule 01.12.2010
comment
бесценный ответ, спасибо. Использовал его для реализации потоковых шаблонов для CSV-файла: github.com/fawce/csv_builder - person fawce; 11.08.2011
comment
Большое спасибо. Почему эти методы устарели и нет официального способа потоковой передачи данных?! - person m33lky; 11.03.2012
comment
к сожалению, это решение не работает для меня. Я начал новое обсуждение здесь ссылка - person dc10; 16.01.2013
comment
Джон, мы столкнулись с проблемой памяти в приведенном выше коде. Если мы будем передавать большой объем данных, кажется, что он будет потреблять большой объем оперативной памяти и никогда не освобождаться. Работаем под Passenger 3.0.19. У вас есть эта проблема? - person DrChanimal; 10.05.2013
comment
Судя по вашему описанию, Passenger может буферизовать ответ в памяти, а не передавать его клиенту. Я не использовал его, поэтому не могу сказать, ожидаемое ли это поведение или нет. - person John; 11.05.2013
comment
Работает и с рельсами 4.0.0! - person gucki; 21.08.2013
comment
Если это все еще не работает (Rails 3.1.x), попробуйте добавить заголовок Last-Modified (см. ответ от Exequiel) - person joel1di1; 11.09.2013

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

  1. Не требует никаких дополнительных драгоценных камней.
  2. Использует Model.find_each(), чтобы не раздувать память всеми соответствующими объектами.
  3. Было протестировано на rails 3.2.5, ruby ​​1.9.3 и heroku с использованием единорога с одним динамометром.
  4. Добавляет GC.start через каждые 500 строк, чтобы не взорвать разрешенную память динамометра heroku.
  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
person paneer_tikka    schedule 08.07.2012

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

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

person Exequiel    schedule 20.04.2012
comment
Это было решением для меня! Хотя мне нужно было отформатировать время так: Time.now.ctime.to_s - person Matt Fordham; 27.04.2012
comment
Я искал некоторое время, чтобы найти этот ответ. Я не понимаю, почему, когда вы не указываете заголовок, он не транслируется... во всяком случае, добавление этой строки сработало для меня. TX - person joel1di1; 11.09.2013

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

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

person moumar    schedule 14.03.2012

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

person Daniel Cadenas    schedule 08.10.2010

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

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

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

person Matt Hucke    schedule 30.08.2011

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

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

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

person shuji.koike    schedule 27.06.2012
comment
Если вы не знаете длину, которую вы можете попытаться добавить в config.ru, используйте Rack::Chunked после 'require' перед 'run' - person arkadiy kraportov; 25.09.2012

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

Мартин

person Martin    schedule 09.11.2010

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

Заявление

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

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

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

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

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

person Yogesh Nachnani    schedule 16.01.2013