Streaming with Rails 4

Media Player App IconWhat is Streaming ?

Streaming has been around in Rails since 3.2, but it has been limited to template streaming. Rails 4 comes with much more mature, real time streaming fuctionality. Essentially, this means Rails is now able to handle I/O objects natively and send data to the client in real-time.

Streaming and Live are two different modules written under ActionController. Streaming comes included by default, whereas Live needs to be defined explicitly in the controller subclass.

The main streaming api uses the Fiber class of Ruby (available in 1.9.2+). Fiber provides the building blocks of thread-like concurrency in ruby. Fiber invoked threads can be paused and resumed at will by the programmer, rather than being inherently preemptive.

Template Streaming

Streaming inverts the regular Rails process of rendering the layout and template. By default, Rails renders template first and then the layout. The first method it runs is yield, and loads up the template. Then, the assets and layouts are rendered.

Consider a query-intensive view, such as a system-wide timeline of multiple classes, like so:

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

In this case, streaming seems to be a good fit. In a typical Rails scenario, this page takes longer to load than usual because it has to retrieve all the data first.

Let’s add streaming:

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

    render stream: true
  end
end

The Rails method render stream: true will lazily load the queries and allow them to run after the assets and layout have been rendered. Streaming only works with templates and not any other forms (such as json or xml). This adds a clever technique to make the application prioritize templates based on the type of page and content.

Passing the Stuff in Between

Streaming changes the method of rendering the template and layout. This brings forth a logical issue: Templates making use of instance variables.

Since the database calls have not happened when the templates are rendered, references to instance variables will fail.

Hence, in order to load attributes like title or meta we need to use content_for instead of the regular yield method. yield, however, still works for the body.

Previously, our method looked like this:

<%= yield :title %>

It will now look like this :

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

Going Live with the Live API

Live is a special module included in ActionController class. It enables Rails to open and close a stream explicitly. Let’s create a simple app and see how to include this and access it from the outside.

We are looking at concepts of live streaming and concurrency, and WEBrick does not play well with such things. We will, as a result, use Puma for handling the concurrency and threads in our app.

Add Puma to the Gemfile and bundle.

gem "puma"
:~/testapp$ bundle install

Puma integrates well with Rails, so as soon as you run `rails s` (server restart required if you are already running it) Puma boots up on the same port number as 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

Let’s quickly generate a controller for sending out messages.

:~/testapp$ rails g controller messaging

Also add the basic method to stream out messages to standard out.

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 Messagen"
      sleep 1
    }
    response.stream.close
  end
end

and a route in routes.rb

get 'messaging' => 'messaging#send_message'

We can access this method via curl as follows:

~/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

When we make a call on the method send_message, Puma initiates a new thread and handles the data streaming for a single client in this thread. Default Puma configuration allows 16 concurrent threads, which means 16 clients. Of course, this can be increased, but not without some memory overhead.

Let’s build a form and see if we can send some data to our 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

Create a form to send the data to the stream.

<%= form_tag messaging_path, :method => 'get' do %>

    <%= text_field_tag :message, params[:message] %>
    <%= submit_tag "Post Message" %>

<% end %>

And a route ot make the call.

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

As soon as you type the message and press “Post Message”, the browser receives the stream response as a downloadable text file wich contains the message logged 10 times.

stream

Here, however, the stream does not know where to send the data or in what format. Thus, it writes to a text file on the server.

You can also check it by sending the params via 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 (SSEs)

HTML5 introduced a method called server side events (SSEs). SSE is a method available in the browser, that recognizes and fires an event whenever the server sends the data.

You can use SSE in conjunction with the Live API to achieve full-duplex communication.

By default Rails provides a one-way communication process by writing the stream to the client when data is available. However, if we can add SSEs, we can enable events and responses, thus making it two-way.

A simple SSE looks like the following :

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}nn"
    end

    def close
      @io.close
    end
  end
end

This module assigns the I/O stream object to a hash and converts it into a key-value pair so that it is easy to read, store, and send it back in JSON format.

You can now wrap your stream object inside the SSE class. First, include your SSE module inside your controller. Now, the opening and closing of connections are handled by the SSE module. Also, if not terminated explicitly, the loop will go on infinitely and connection will be open forever, so we add the ensure clause.

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

This is what the response looks like:

:~/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"}

Gotchas

There are a few gotchas (there always are…)

  1. All the streams have to be closed, else they will be open forever.
  2. You will have to make sure your code is threadsafe, as the controller always spawns a new thread when the method is called.
  3. After the first commit of the response, the header cannot be rewritten in write or close.

Conclusion

This is a feature many people are looking forward to, because it will significantly improve the performance of Rails apps (template streaming) and would also pose a strong competition to node.js (Live).

There are folks already benchmarking the differences, but I feel it’s just the beginning and will take time (read further releases) for the feature to mature. For now, it’s a good start and exciting to get these features in Rails.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://codefol.io Noah Gibbs

    I really like the idea of Live streaming, but… Some additions to your list of Gotchas:

    * the multi-threading that you need isn’t fully compatible with the automatic class reloading in Rails debug mode, so you need to turn reloading back on, *and* expect some crashes in many Ruby implementations including MRI

    * now you’re both Ruby implementation-dependent and app-server-dependent. Unicorn, for instance, is fine with concurrency but does *not* work with Live mode since it’s forked, not threaded.

    * If you don’t close the connections, you won’t know until you run out of threads.

    * Threading-only means you’re now more dependent on good memory management since you can kill *all* your workers at once on a given server with an OOM or an exception.

    Live streaming is a great idea, but it’s not fully baked yet. I’m glad it’s now in Rails so we can start fixing problems with it :-)

  • Hongli Lai

    @Noah:

    1) Can’t you just disable multithreading during development?

    4) That’s why multiprocessing in combination with multithreading is useful (supported by e.g. Phusion Passenger Enterprise). You can kill one process and all threads in it, but the others still stay alive.

  • Giovanni

    Thanks!