Key Takeaways
- Streaming functionality in Rails 4 is more mature and provides real-time data transmission to the client. It includes two modules: Streaming and Live. Streaming is included by default while Live needs to be defined explicitly in the controller subclass.
- Streaming alters the regular Rails process of rendering the layout and template, which can be beneficial for query-intensive views. However, it can cause issues with templates making use of instance variables and requires the use of content_for instead of the regular yield method.
- The Live API allows Rails to open and close a stream explicitly and can be used in conjunction with Server Side Events (SSEs) to achieve full-duplex communication. However, it requires careful handling to ensure all streams are closed and the code is thread-safe.
- Streaming with Rails 4 can significantly improve the performance of Rails apps and is ideal for real-time applications. However, it requires a persistent connection between the client and the server which can lead to scalability issues.
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 isyield
, 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 liketitle
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.Frequently Asked Questions about Streaming with Rails 4
What are the prerequisites for streaming with Rails 4?
Before you start streaming with Rails 4, you need to have a basic understanding of Ruby on Rails, HTML, and JavaScript. You should also have Ruby on Rails installed on your system. Additionally, you need to have a basic understanding of MVC (Model View Controller) and CRUD (Create, Read, Update, Delete) operations.
How can I implement live streaming in Rails 4?
Rails 4 introduced a feature called ActionController::Live that allows you to implement live streaming. This feature allows you to send data to the client as it becomes available, rather than waiting for the entire response to be ready. To use ActionController::Live, you need to include it in your controller and use the response.stream.write method to send data to the client.
What are the benefits of streaming with Rails 4?
Streaming with Rails 4 allows you to send data to the client as soon as it becomes available. This can improve the user experience by reducing the perceived latency. It can also reduce the load on your server, as it doesn’t need to hold onto the entire response before sending it.
Can I use streaming with Rails 4 for real-time applications?
Yes, you can use streaming with Rails 4 to build real-time applications. By using ActionController::Live, you can push updates to the client as soon as they happen, making it ideal for applications that require real-time updates, such as chat applications or live video streaming.
What are the challenges of streaming with Rails 4?
One of the main challenges of streaming with Rails 4 is that it requires a persistent connection between the client and the server. This can increase the load on your server and potentially lead to scalability issues. Additionally, not all browsers support streaming, so you may need to implement fallback mechanisms for those that don’t.
How can I handle errors when streaming with Rails 4?
When streaming with Rails 4, you can handle errors by rescuing them in your controller action and then closing the stream. This will ensure that the connection is properly closed even if an error occurs.
Can I use streaming with Rails 4 for file downloads?
Yes, you can use streaming with Rails 4 to send files to the client. This can be useful for large files, as it allows you to start sending the file before it’s fully loaded into memory.
How can I test my streaming functionality in Rails 4?
Testing streaming functionality in Rails 4 can be challenging, as it requires a persistent connection between the client and the server. However, you can use tools like Capybara and Selenium to simulate a client and test your streaming functionality.
Can I use streaming with Rails 4 in a multi-threaded environment?
Yes, you can use streaming with Rails 4 in a multi-threaded environment. However, you need to be aware that each streaming connection will tie up a thread until the connection is closed, which can potentially lead to scalability issues.
What alternatives are there to streaming with Rails 4?
If streaming with Rails 4 doesn’t meet your needs, there are several alternatives you can consider. For example, you could use a different framework that supports streaming, such as Node.js. Alternatively, you could use a service like Pusher or Firebase to handle real-time updates for you.
A Ruby Developer, writer and cook. I love to write code in ruby and rails as a part of my day job. In evenings and weekends, I moonlight as a sci-fi writer and cook. I used to run a ruby and rails consulting firm but I love working for myself now.