Стриминг в Rails 4

от автора

Что такое стриминг?

Стриминг крутился около Rails начиная с версии 3.2, но он был ограничен исключительно стримингом шаблонов. Rails 4 же вышел с более зрелым функционалом стриминга в реальном времени. По сути это значит что Rails сейчас способен нативно обрабатывать I/O объекты и посылать данные клиенту в риалтайме.

Streaming и Live — два отдельных модуля, реализованных внутри ActionController’а. Streaming включен по умолчанию, в то время как Live должен быть явно определен непосредственно в контроллере.

Основной api стриминга использует класс Fiber (доступен с версии ruby 1.9.2). Файберы предоставляют инструментарий для потоко-подобного параллелизма в ruby. Fiber дает возможность потокам приостанавливаться и возобновлять работу по желанию программиста, а не быть по сути упреждающими.

Стриминг шаблонов

Стриминг инвертирует обычный процесс рендеринга лэйаута и шаблона. По умолчанию Rails рендерит сначала шаблон, а потом лэйаут. Первое что он делает, запускает yield и загружает шаблон. После этого, рендерятся ассеты и лэйаут.

Рассмотрим action который делает много запросов, например:

class TimelineController   def index     @users = User.all     @tickets = Ticket.all     @attachments = Attachment.all   end end

В данном случае хорошо подойдет стриминг. Это обычная ситуация для Rails, страница загружается дольше чем обычно, потому как нужно для начала получить все данные.

Давайте добавим стриминг:

class TimelineController   def index     @users = User.all     @tickets = Ticket.all     @attachments = Attachment.all     render stream: true   end end 

Метод render stream: true лениво загрузит все запросы и даст им возможность выполняться после того как ассеты и лэйаут будут отрендерены. Стриминг работает с шаблонами и только с ними (но не с json или xml). Это дает хороший способ передать приоритет шаблонам основываясь на типе страницы и её содержимом.

Добавим чего-нибудь внутрь

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

Ведь так как база данных еще не вернула ответ на момент рендеринга шаблонов, попытка обратиться к инстансным переменным приведет к ошибке.

Следовательно, чтобы загрузить такие аттрибуты как title и meta нужно использовать content_for вместо привычного yield‘а. Тем не менее, yield все еще будет работать для body.

Ранее наш метод выглядел как-то так:

<%= yield :title %> 

Теперь же он будет выглядеть так:

<%= content_for :title, "My Awesome Title" %> 

Становимся живее с Live API

Live это специальный модуль, включенный в ActionController. Он позволяет Rails явно открывать и закрывать стримы. Давайте напишем простое приложение и посмотрим как это работает и как получить доступ к стриму извне.

Так как мы работаем в контексте стриминга и параллелизма, WEBrick нам тут не товарищ. Будем использовать Puma ибо она умеет работать с потоками.

Добавим пуму в Gemfile и запустим bundle.

gem "puma" 

:~/testapp$ bundle install 

Puma хорошо интегрируется с Rails, так что если вы теперь запустите rails s, Puma запустится на том же порту что и WEBRick.

:~/testapp$ rails s => Booting Puma => Rails 4.0.0 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options => Ctrl-C to shutdown server Puma 2.3.0 starting... * Min threads: 0, max threads: 16 * Environment: development * Listening on tcp://0.0.0.0:3000 

Давайте по-быстрому сгенерируем контроллер для отправки сообщений.

:~/testapp$ rails g controller messaging 

И добавим простой метод для их стриминга.

class MessagingController < ApplicationController   include ActionController::Live    def send_message     response.headers['Content-Type'] = 'text/event-stream'     10.times {       response.stream.write "This is a test Message\n"       sleep 1     }     response.stream.close   end end 

Добавим роут в routes.rb:

get 'messaging' => 'messaging#send_message' 

Теперь мы можем получить доступ к стриму например с помощью curl:

:~/testapp$ curl -i http://localhost:3000/messaging HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff X-UA-Compatible: chrome=1 Content-Type: text/event-stream Cache-Control: no-cache Set-Cookie: request_method=GET; path=/ X-Request-Id: 68c6b7c7-4f5f-46cc-9923-95778033eee7 X-Runtime: 0.846080 Transfer-Encoding: chunked This is a test message This is a test message This is a test message This is a test message 

Каждый раз когда запускается метод send_message, Puma создает новый поток, в котором производит стриминг данных для отдельного клиента. По умолчанию Puma сконфигурирована чтоб обрабатывать до 16 параллельных потоков, что значит 16 одновременно подключенных клентов. Конечно же это количество можно увеличить, но это несколько увеличит расход памяти.

Давайте-ка создадим форму и посмотрим можем ли мы послать какие-нибудь данные на view:

def index end  def send_message   response.headers['Content-Type'] = 'text/event-stream'   10.times {     response.stream.write "#{params[:message]}\n"     sleep 1   }   response.stream.close end 

Делаем форму для посылки данных в стрим:

<%= form_tag messaging_path, :method => 'get' do %>   <%= text_field_tag :message, params[:message] %>   <%= submit_tag "Post Message" %> <% end %> 

И настроим роуты:

root 'messaging#index' get  'messaging' => 'messaging#send_message', :as => 'messaging' 

Как только вы введете сообщение и нажмете «Post Message», браузер получит потоковый ответ в виде загружаемого текстового файла, который содержит ваше сообщение, залогированное 10 раз.

Здесь, однако, стрим не знает куда посылать данные и в каком формате, поэтому то он их и пишет в текстовый файл на сервере.

Можно также проверить работу с помощью curl, передав параметры:

:~/testapp$ curl -i http://localhost:3000/messaging?message="awesome" HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff X-UA-Compatible: chrome=1 Content-Type: text/event-stream Cache-Control: no-cache Set-Cookie: request_method=GET; path=/ X-Request-Id: 382bbf75-7d32-47c4-a767-576ec59cc364 X-Runtime: 0.055470 Transfer-Encoding: chunked awesome awesome 

События на стороне сервера (Server Side Events)

HTML5 предоставляет метод, называемый Server Side Events (SSE). SSE это метод доступный браузеру, который распознает и инициирует события каждый раз когда сервер присылает данные.

Мы можем использовать SSE в сочетании с Live API чтобы наладить двухстороннюю связь сервера с клиентом.

По умолчанию Rails предоставляет только одностороннюю комуникацию — позволяет потоково посылать клиенту данные как только они становятся доступны. Однако если мы добавим SSE то сможем использовать события и ответы в двухстороннем режиме.

Простой SSE выглядит приблизительно так:

require 'json'  module ServerSide   class SSE     def initialize io       @io = io     end      def write object, options = {}       options.each do |k,v|         @io.write "#{k}: #{v}\n"       end       @io.write "data: #{object}\n\n"     end      def close       @io.close     end   end end 

Данный модуль получает объект I/O стрима в хэш и конвертирует его в пару ключ-значение так, чтоб его можно было легко читать, хранить и отсылать обратно в формате JSON.

Теперь можно обернуть наш стрим-объект в SSE. Во-первых, подключим SSE модуль к контроллеру. Теперь открытие и закрытие стрима регулируется SSE модулем. Также, если не завершить явно, цикл будет повторяться бесконечно и подключение будет открыто всегда, поэтому мы добавим условие ensure, чтобы удостовериться что стрим таки закроется.

require 'server_side/sse'  class MessagingController < ApplicationController   include ActionController::Live    def index   end    def stream     response.headers['Content-Type'] = 'text/event-stream'     sse = ServerSide::SSE.new(response.stream)     begin       loop do         sse.write({ :message => "#{params[:message]}" })         sleep 1       end     rescue IOError     ensure       sse.close     end   end end 

Этот код выдаст ответ вроде этого:

:~/testapp$ curl -i http://localhost:3000/messaging?message="awesome" HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff X-UA-Compatible: chrome=1 Content-Type: text/event-stream Cache-Control: no-cache Set-Cookie: request_method=GET; path=/ X-Request-Id: b922a2eb-9358-429b-b1bb-015421ab8526 X-Runtime: 0.067414 Transfer-Encoding: chunked data: {:message=>"awesome"} data: {:message=>"awesome"} 

Подводные камни

Будьте внимательны, есть пара подводных камней (куда ж без них то):

  1. Все стримы должны быть закрыты явно, иначе они будут открыты всегда.
  2. Вы должны удостовериться в том что ваш код потокобезопасен, так как контроллер всегда порождает новый поток при запуске метода.
  3. После первой порции ответа хедеры нельзя изменить в write или close

Заключение

Это возможность, которую многие давно искали в Rails, потому как она может существенно увеличить производительность приложений (стриминг шаблонов) и составить серьезную конкуренцию node.js (Live).

Некоторые уже проводят бенчмарки, сравнивают, но я думаю что это только самое начало и должно пройти время (читай несколько релизов) чтобы эта фича, так сказать, дозрела. Сейчас же это хороший старт и волнующая возможность попробовать всё в деле.

PS: это мой первый опыт перевода, буду очень благодарен за замечания в личку. Спасибо.

Оригинал тут.

Сможет ли Rails через n месяцев/лет стать полноценной заменой Node.js в реал-тайм приложениях?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Никто ещё не голосовал. Воздержавшихся нет.

ссылка на оригинал статьи http://habrahabr.ru/post/187994/