TL;DR Rails 4.0 will allow you to stream arbitrary data at arbitrary intervals
with Live Streaming.

HAPPY MONDAY EVERYONE!

Besides enabling multi-threading by default, one of the things I really wanted
for Rails 4.0 is the ability to stream data to the client. I want the ability
to treat the response object as an I/O object, and have the data I write
immediately available to the client. Essentially, the ability to deliver
whatever data I want, whenever I want.

Last night I merged a patch to master that allows us to do exactly that: send
arbitrary data in real time to the client. In this article, I would like to
show off the feature by developing a small application that automatically
refreshes the page when a file is written. I’ll be working against edge Rails,
specifically against this commit (hopefully then people in the future will
notice if / when this article is out of date!)

Here is a video of the final product we’ll build in this article:

Response Streaming

The first thing I added was a “stream” object to the response. This object
is where where data will be buffered until it is sent to the client. The stream
object is meant to quack like an IO object. For example:

class MyController < ActionController::Base
  def index
    100.times {
      response.stream.write "hello world\n"
    }
    response.stream.close
  end
end

In order to maintain backwards compatibility, the above code will work, but it
will not stream data to the client. It will buffer the data until the response
is completed, then send everything at the same time.

Live Streaming

To make live streaming actually work, I added a module called
ActionController::Live. If you mix this module in to your controller, all
actions in that controller can stream data to the client in real time. We can
make the above MyController example live stream by mixing in the module like
so:

class MyController < ActionController::Base
  include ActionController::Live

  def index
    100.times {
      response.stream.write "hello world\n"
    }
    response.stream.close
  end
end

The code in our action stays exactly the same, but this time the data will be
streamed to the client as every time we call the write method.

Webservers

Before we start on our little example project, we should talk a bit about web
servers. By default, script/rails server uses WEBrick. The Rack adapter for
WEBrick buffers all output in a way we cannot bypass, so developing this example
with script/rails server will not work.

We could use Unicorn, but it is meant for fast responses. Unicorn will
kill our connection after 30 seconds. The protocol we’re going to use actually
makes this behavior irrelevant, but it’s a bit annoying to see the logs.

For this project, I think the best webserver would be either Rainbows!,
Puma, or Thin. I’ve been playing with Puma a lot lately, so I’ll use
it in this example.

Our application

We’re going to build an application that automatically reloads the page whenever
a file is saved. You can find the final repository here.

Technology

For this project we’re going to use edge Rails and Live Streaming, along with a
bit of JavaScript and Server-Sent Events. To detect file system changes,
we’re going to use the rb-fsevent gem. I think this gem only works on OS X,
but it should be easy to translate this project to Linux or Windows given the
right library.

Server-Sent Events

If you’ve never heard of Server-Sent Events (from here on I’ll call them SSEs),
it’s a feature of HTML5 that allows long polling, but is built in to the
browser. Basically, the browser keeps a connection open to the server, and
fires an event in JavaScript every time the server sends data. An example event
looks like this:

id: 12345\n
event: some_channel\n
data: {"hello":"world"}\n\n

Messages are delimited by two newlines. The data field is the event’s
payload. In this example, I’ve just embedded some JSON data in the payload.
The event field is the name of the event to fire in JavaScript. The id
field should be a unique id of the message. SSE does automatic reconnection;
if the connection is lost, the browser will automatically try to reconnect. If
an id has been provided with your messages, when the browser attempts to
reconnect, it will send a header (Last-Event-ID) to the server allowing you
to pick up where you left off.

We’re going to build a controller that emits SSEs and tells the browser to
refresh the page.

Getting Started

The first thing we’ll do is generate a new Rails project from the Rails git
repository (I keep all my git repos in ~/git):

$ cd ~/git/rails
$ ruby railties/bin/rails new ~/git/reloader --dev
$ cd ~/git/reloader

Update the Gemfile to include puma and rb-fsevent and re-bundle:

diff --git a/Gemfile b/Gemfile
index 9e075a8..51ce01c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,6 +6,8 @@ gem 'arel',      github: 'rails/arel'
 gem 'active_record_deprecated_finders', github: 'rails/active_record_deprecated_finders'
 
 gem 'sqlite3'
+gem 'puma'
+gem 'rb-fsevent'
 
 # Gems used only for assets and not required
 # in production environments by default.

Then we’ll generate a controller for emitting SSE messages to the browser:

$ ruby script/rails g controller browser

Moving on!

Generating SSEs

I’d like an object that knows how to format messages as SSE and emits those
messages to the live stream. To do this, we’ll write a small class that
decorates the output stream and knows how to dump objects as SSEs:

require 'json'

module Reloader
  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: #{JSON.dump(object)}\n\n"
    end

    def close
      @io.close
    end
  end
end

We’ll place this file under lib/reloader/sse.rb and require it from the
browser controller. In the controller, we’ll mix in ActionController::Live
and try emitting some SSEs:

require 'reloader/sse'

class BrowserController < ApplicationController
  include ActionController::Live

  def index
    # SSE expects the `text/event-stream` content type
    response.headers['Content-Type'] = 'text/event-stream'

    sse = Reloader::SSE.new(response.stream)

    begin
      loop do
        sse.write({ :time => Time.now })
        sleep 1
      end
    rescue IOError
      # When the client disconnects, we'll get an IOError on write
    ensure
      sse.close
    end
  end
end

Next, update your routes.rb to point at the new controller:

Reloader::Application.routes.draw do
  get 'browser' => 'browser#index'
end

Fire up Puma in one shell:

$ puma
Puma 1.5.0 starting...
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://0.0.0.0:9292
Use Ctrl-C to stop

Then in another shell curl against the endpoint. You should see an event
emitted every second. Here is my output after a few seconds:

$ curl -i http://localhost:9292/browser
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
X-UA-Compatible: IE=Edge
X-Request-Id: 76cfaa39-d23b-4eac-8337-f915410dc0de
X-Runtime: 0.430762
Transfer-Encoding: chunked

data: {"time":"2012-07-30T10:02:05-07:00"}

data: {"time":"2012-07-30T10:02:06-07:00"}

data: {"time":"2012-07-30T10:02:07-07:00"}

data: {"time":"2012-07-30T10:02:08-07:00"}

data: {"time":"2012-07-30T10:02:09-07:00"}

data: {"time":"2012-07-30T10:02:10-07:00"}

^C
$

Next we should monitor the file system.

File System Monitoring

Now we’ll update the controller to emit an event every time a file under
app/assets or app/views changes. Rather than a loop in our controller,
we’ll use the FSEvent object:

require 'reloader/sse'

class BrowserController < ApplicationController
  include ActionController::Live

  def index
    # SSE expects the `text/event-stream` content type
    response.headers['Content-Type'] = 'text/event-stream'

    sse = Reloader::SSE.new(response.stream)

    begin
      directories = [
        File.join(Rails.root, 'app', 'assets'),
        File.join(Rails.root, 'app', 'views'),
      ]
      fsevent = FSEvent.new

      # Watch the above directories
      fsevent.watch(directories) do |dirs|
        # Send a message on the "refresh" channel on every update
        sse.write({ :dirs => dirs }, :event => 'refresh')
      end
      fsevent.run

    rescue IOError
      # When the client disconnects, we'll get an IOError on write
    ensure
      sse.close
    end
  end
end

The controller will send an SSE named “refresh” every time a file is modified.
Start Puma in one shell, then curl in a second shell, and touch a file in the
third shell, you will see an event.

Curl started in one shell:

$ curl -i http://localhost:9292/browser

Touch a file in another:

$ touch app/assets/javascripts/application.js

Now the curl shell should look like this:

$ curl -i http://localhost:9292/browser
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
X-UA-Compatible: IE=Edge
X-Request-Id: 98331d36-ef7c-4d15-ad99-331149fc589b
X-Runtime: 43.307765
Transfer-Encoding: chunked

event: refresh
data: {"dirs":["/Users/aaron/git/reloader/app/assets/javascripts/"]}

Every time a file is modified under the directories we’re watching, an SSE will
be sent up to the browser.

Listening with JavaScript

Next let’s add the JavaScript that will actually refresh the page. I’m going to
add this directly to app/assets/javascripts/application.js. The JavaScript
we’ll add simply opens an SSE connection and listens for refresh events.

jQuery(document).ready(function() {
  setTimeout(function() {
    var source = new EventSource('/browser');
    source.addEventListener('refresh', function(e) {
      window.location.reload();
    });
  }, 1);
});

Whenever a refresh event happens, the browser will reload the current page.

Parallel Requests

We need to update the configuration in development to handle multiple requests
at the same time. One request for the page we’re working on, and another
request for the SSE controller. Add these lines to your
config/environments/development.rb but please note that they may change in
the future
:

  config.preload_frameworks = true
  config.allow_concurrency = true

Next we’ll see everything work together.

Trying it out!

To see the automatic refreshes in action, let’s create a test controller and
view. I just want to see the automatic refreshes happen, so I’ll use the
scaffolding to generate a model, view, and controller:

$ ruby script/rails g scaffold user name:string
$ rake db:migrate

Now run Puma, and navigate to http://localhost:9292/users. If you watch
the developer tools, you’ll see the browser connect to /browser but the
request will never finish. That is what we want: the browser listening for
events on that endpoint.

If you change any file under app/assets or app/views, a message should be
sent to the browser, and the browser will refresh the page.

YAY!

SSE Caveats / Features

SSEs will not work on IE (yet). If you want to use this with IE, you’ll have to
find another way. SSEs will work on pretty much every other browser, including
Mobile Safari.

Some webservers (notably Unicorn) cut off the request after a particular
timeout. Be mindful of this when designing your application, and remember that
SSE will automatically reconnect after a connection is lost.

Heroku will cut off your connections after 30 seconds. I had trouble getting
the SSE to reconnect to a Heroku server, but I haven’t had time to investigate
the issue.

Rails Live Streaming Caveats

Mixing the Live Streaming module in to your controller will enable every action
in that controller
to have a Live Streaming object. In order to make this
feature work, I had to work around the Rack API and I did this by executing Live
Stream actions in a new thread. This shouldn’t be a problem for most people,
but I thought it would be good for people to know.

Headers cannot be written after the first call to write or close on the
stream. You will get an exception if you attempt to write to the headers after
those calls are made. This is because when you call write on the stream, the
server will send all the headers up to the client, so writing new headers after
that point is useless and probably a bug in your code.

Always make sure to close your Live Stream IO objects. If you don’t, it
might mean that a socket will sit open forever.

WUT?

I thought streaming was already introduced in Rails 3.2. How is this
different?

Yes, streaming templates were added to Rails 3.2. The main difference between
Live Streaming and Streaming Templates is that with Streaming Template, the
application developer could not choose what data was sent to the client and
when. Live Streaming gives the application developer full control of what data
is sent to the client and when.

Final Thoughts

I’m very excited about this feature of Rails 4. In my opinion, it is one of the
most important new features. I’ve been interested in streaming data from Rails
for a long time. We can use this feature to reduce latency and deliver data
more quickly to clients on slow connections (e.g. cell phones), for infinite
streams like chatrooms, or for cool productivity hacks like this article shows.

I hope you enjoyed this article! I think for the next demo of Live Streams, I
would like to show how to reduce latency when sending JSON streams to the
client. That might be fun.

<3<3<3

Read more at the source