Streaming with Rails 4
What 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.
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…)
- All the streams have to be closed, else they will be open forever.
- You will have to make sure your code is threadsafe, as the controller always spawns a new thread when the method is called.
- After the first commit of the response, the header cannot be rewritten in
write
orclose
.
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.