Long-polling with Puma, Rails 4 and Server Sent Events
In this post, I am going to show you how to create a long polling server with Puma, ActionController::Live in Rails 4 and Server Side Events in modern browsers. Why there is another post when there are already many of them on the Internet? It is just because I could not accomplish my goal by looking at just one post. I had to browse through several posts and had to collect piece and piece of information from each site. And my goal is to have a one place which you can really read and understand how to implement.
Important notes
- You have to turn on
config.cache_classes=true
andconfig.eager_load=true
insideenvironments/development.rb
or it will not work. And after this setting, please be noted that you will have to restart the server everytime you make changes to the code. Yes, EVERYTIME you make changes to the code. - You have to use Puma server. Don't try this with other servers such as Thin or Webrick because I don't think it will work, they do not support concurrent requests like Puma
Ok, let's get started
First of all, I think it is necessary to understand the difference between Long Polling and Short Polling.
-
Short Polling: You send a request to server every x seconds to retrieve updates. This is how Pivotral Tracker is using to update their dashboard in real-time
-
Long Polling: You send a request to server and wait for the response until server returns something or timeout. In order server such as Nodejs, this is easy to accomplish. But in Rails, you have to use loop statement to achieve this.
Why Puma?
Good question. As I mention above, you have to use loop statement in server side code. And the current process will be hang inside the loop and will not serve another client request. Puma is a server which support running multiple processes or multiple threads, and one stuck thread won't stop it from serving other users.
What is Server Sent Events?
This is a HTML5 technology which allow browsers to receive updates from server automatically. And when it comes to HTML5 playground, one guy stands out of my way: IE. Please, I have warned you from the beginning of this post that IE will not be supported.
We are going to build a very simple application which allow user to get a new messages submitted by other client in real-time. It is just like a chat app but much simpler
Controller
We will first need to configure our Puma server a little bit. Add following line to your Gemfile
and run bundle install
gem 'puma'
Create a config file for Puma server inside config
directory and name it puma.rb
. The content of this file looks like this:
workers Integer(ENV['PUMA_WORKERS'] || 3)
threads Integer(ENV['MIN_THREADS'] || 1), Integer(ENV['MAX_THREADS'] || 16)
preload_app!
rackup DefaultRackup
port ENV['PORT'] || 3000
environment ENV['RACK_ENV'] || 'development'
on_worker_boot do
# worker specific setup
ActiveSupport.on_load(:active_record) do
config = ActiveRecord::Base.configurations[Rails.env] ||
Rails.application.config.database_configuration[Rails.env]
config['pool'] = ENV['MAX_THREADS'] || 16
ActiveRecord::Base.establish_connection(config)
end
end
Again, as I have said earlier, you have to set following two config in development.rb to make it work
config.cache_classes = true
config.eager_load = true
Now, you can start your server using the following command
puma -C config/puma.rb
Create a model called Message in your app. This is a very simple model for storing message submitted by users and it has just one main column content
rails g model Message content:text
Create a simple HomeController
with the following content
require 'simple_app/sse.rb'
class HomeController < ApplicationController
include ActionController::Live
def check
# SSE expects the `text/event-stream` content type
response.headers['Content-Type'] = 'text/event-stream'
sse = SimpleApp::SSE.new(response.stream)
begin
20.times do
messages = Message.where("created_at > ?", 3.seconds.ago)
unless messages.empty?
sse.write({messages: messages.as_json}, {event: 'refresh'})
end
sleep 3
end
rescue IOError
# When the client disconnects, we'll get an IOError on write
ensure
sse.close
end
end
end
In Rails 4, we have the ability to send data directly to client through ActionController::Live
module. You can read more at this link: http://edgeguides.rubyonrails.org/action_controller_overview.html#live-streaming-of-arbitrary-data
You will notice that I also create a wrapper at lib/simple_app/sse.rb
for sending Server Sent Events to client. I actually borrow the code from this excellent blog: http://tenderlovemaking.com/2012/07/30/is-it-live.html
require 'json'
module SimpleApp
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
Hey, please pay a very close attention to the \n\n
character at the end of this line
@io.write "data: #{JSON.dump(object)}\n\n"
These \n\n
are not there just for fun. They play an important roles. They are the signal to tell client that there are data returned from the server and the client can process it. If you remove these characters by accident (just like I did before), your app will not work as you expected. So please pay attention to that.
Of course, remember to config your routes.rb
so that Rails can understand this route
get '/check', to: 'home#check'
Javascript
Here is the JS code which used to check data from server
setTimeout((function() {
var source;
source = new EventSource('/check');
source.addEventListener("refresh", function(e) {
addMessages($.parseJSON(e.data).messages);
});
}), 1);
Do you wonder why there is setTimeout
function? It is because we want to start this listening event in a new thread so that it won't block the current UI thread on our web. The code is quite simple and easy to understand: we set up and EventSource pointing to our location on server and we listen to the refresh
event on this EventSource
object. If this event happens, we will parse the data returned from server by running the function addMessages
. Be noted that the data returned from server would be in plain text format and we need to convert it to correct JSON object.
I have a very simple addMessages
function like the following, you can customize it according to your need:
function addMessages(messages){
for(var i=0; i<messages.length; i++){
document.writeln(messages[i].content);
}
}
That's it. Now try running Rails console to create a new Message record in the database and you notice that the message displayed immediately on client side.
You can also clone the code from this Github repos to run on your local https://github.com/xuanchien/puma_sse_example. Let me know if you have any issues on your side