Ruby on Rails 5.0+ has been out for a few months now and has brought with it a number of much needed improvements (AdequateRecord, Rails API), and a few which were more controversial such as ApplicationRecord. When it was first announced, ActionCable was among the “more controversial” features – coming as a surprise to even some of the core team – but now it’s regarded as the next logical step in Rails’ evolution.
ActionCable brings WebSockets to your Rails application, allowing “for real-time features to be written in Ruby in the same style and form as the rest of your Rails application, while still being performant and scalable.” (ActionCable Overview) Of course Ruby can only be run on the server, so it also provides a JavaScript framework as part of the stack.
When considering real-time components for the web, our thoughts are inevitably drawn to creating chat rooms. It’s the obvious choice: we’re familiar with them, they’re the perfect example of “real-time”, and 98% of the examples available seem to be exactly that.
Of course there is more to web sockets than just chat rooms, but drawing from our understanding of chat rooms we can see potential opportunities for products and improved user experiences such as helpdesk chats. As we further remove ourselves from chat, we can also imagine live comments for blogs or discussion sites, and from there it’s not difficult to see the opportunity of viewing any kind of personal update (i.e. likes, images, and reposts). Finally, as we disconnect ourselves from the idea of person-to-person interaction and toward website components being the recipients of live data, new ideas abound: stock tickers, live poll results, notifications, log feeds, and more.
Before we dive into an example, we need to get a better idea of what happens when a user loads an ActionCable enhanced page. ActionCable’s has a lot of moving parts, and the following list details which of those parts come in to play during a page’s life cycle, and when.
/app/assets/javascripts/cable.js
/app/channels/application_cable/connection.rb
/app/assets/javascripts/cable/subscriptions/*.js
/app/channels/*Channel.rb
) within the connection to the appropriate subscribersNate Berkopec summarizes this process:
Action Cable provides the following:
- A "Cable" or "Connection", a single WebSocket connection from client to server. It's worthwhile to note that Action Cable assumes you will only have one WebSocket connection, and you'll send all the data from your application along different...
- "Channels" - basically subdivisions of the "Cable". A single "Cable" connection has many "Channels".
- A "Broadcaster" - Action Cable provides its own server. Yes, you're going to be running another server process now. Essentially, the Action Cable server just uses Redis' pubsub functions to keep track of what's been broadcasted on what cable and to whom.
While chat rooms can be a lot of fun, they’re overused as examples for real-time web interactions. Let’s do something different; let’s create a live-updating poll.
We want to keep this as simple as possible – it’s an example after all – so we’re only going to look at code immediately related to ActionCable
. Other code, such as setting up migrations and authentication, are available in the repo, but won’t be covered here. You can find the complete app, named Pollish, on GitHub.
There are three features we want to implement:
Because this is an introductory article, and we want to focus on ActionCable itself, we’re not going to worry about tests, (but it sounds like a great subject for a future article.) We’ll start off by laying out the groundwork of our app by creating the page for polls, voting, and results, and then wire it up with ActionCable.
Let’s start things off by creating our Polls resource. All we need for this is a page listing the polls in which users can participate and a page where they can cast their vote. We’ll do that with a simple PollsController
.
# /app/controllers/polls_controller.rb
class PollsController < ApplicationController
def index
@polls = Poll.all
end
def show
@poll = Poll.find(params[:id])
@vote = @poll.votes.build
if session[@poll.code].present?
redirect_to polls_path, notice: "Don't ruin the fun, You've already voted."
end
end
end
The PollsController
is straightforward. We don’t want people voting more than
once, so we are guarding against that by checking for the presence of the
session variable in the #show
method.
The views for #index
and #show
are equally straightforward:
<%# index.html.erb %>
<ul>
<% @polls.each do |poll| %>
<li><%= link_to poll.name, poll %>
<% if user_signed_in? %>
[<%= link_to "results", poll_results_path(poll) %>]
<% end %>
</li>
<% end %>
</ul>
The index.html.erb
file allows everyone to see the available polls, but only allows authenticated users access to the final results.
<%# show.html.erb %>
<h1><%= @poll.name %> <small>:: <%= @poll.code %></small></h1>
<%= render partial: @poll.code %>
The show.html.erb
file is pretty empty, displaying both the name
and code
of the vote before it renders the partial. As you can see from the last line,
the poll’s code
is the name of the partial file. Note: This isn’t the way you
would want to do things in real life.
Finally, let’s look at the _example.html.erb
partial. (This will only work for a poll with the code
“example”):
<%# _example.html.erb %>
<p><%= @poll.description %></p>
<%= form_for [@poll, @vote] do |f| %>
<%= f.radio_button :value, "yes" %>
<%= f.label :value, "Yes", value: "yes" %>
<br/>
<%= f.radio_button :value, "no" %>
<%= f.label :value, "No", value: "no" %>
<br/>
<%= f.radio_button :value, "maybe" %>
<%= f.label :value, "Maybe", value: "maybe" %>
<br/>
<%= f.submit %>
<% end %>
The partial does the majority of the work displaying the actual form for the poll. When a person submits their vote, it’s sent to the VotesController
which then redirects the user back to the PollsController
index page.
# /app/controllers/votes_controller.rb
class VotesController < ApplicationController
def create
@poll = Poll.find(params[:poll_id])
@vote = @poll.votes.build(vote_params)
if @vote.save
session[@poll.code] = true
redirect_to polls_path, notice: "Thanks for your vote!"
else
flash.now[:alert] = "Are you sure you pressed the button correctly?"
render partial: "polls/show"
end
end
private
def vote_params
params.require(:vote).permit(:value)
end
end
At this point people are able to view the list of available polls and vote in them. Now we need to display the results.
Like the logic for PollsController
, the logic for the ResultsController
is very simple and only requires an index
action.
# /app/controllers/results_controller.rb
class ResultsController < ApplicationController
before_action :authenticate_user!
def index
@poll = Poll.includes(:votes).find(params[:poll_id])
end
end
For our lone action, we are retrieving a specific Poll
, and as we do so, we “include” the votes
relationship to more easily get our poll results in the view.
<#= index.html.erb %>
<h1><%= @poll.name %> <small>:: <%= @poll.code %></small></h1>
<h2>Results</h2>
<div style="width: 600px; height: 400px">
<canvas id="chart" width="600" height="400"></canvas>
</div>
<%= render partial: @poll.code %>
The only thing to note on the “index” page is the canvas
section which is
required by Chart.js (included in /views/layouts/application.html.erb
.)
The real magic starts to happen in the partial:
<%# /app/views/results/_example.html.erb %>
<%= javascript_tag do %>
var ctx = document.getElementById("chart");
var myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ["Yes", "No", "Maybe"],
datasets: [{
label: '# of Votes',
data: [
<%= @poll.votes.find_all {|v| v.value == "yes"}.size %>,
<%= @poll.votes.find_all {|v| v.value == "no"}.size %>,
<%= @poll.votes.find_all {|v| v.value == "maybe"}.size %>
],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
],
borderColor: [
'rgba(255,99,132,1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
],
borderWidth: 1
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
});
<% end %>
Although there is quite a bit of code here, the piece to note is where we gather
the vote counts in the data
key (No, it’s not performant.)
At this point, we have a fully armed and operational voting machine. People can participate in polls, they can vote, and if they have accounts, they can see the results. What we want, however, is for people to see the results update in the chart as votes come in. To do that, we have to connect ActionCable.
As described in “ActionCable Lifecycle” above, the first thing that happens
after an ActionCable page loads, is a connection is made back to the server
through logic in /app/assets/javascripts/cable.js
. At its most basic – and
indeed all we’ll need in our example – the file looks like this:
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
This small block of code is responsible for creating the websocket connection.
If your websockets originate from anywhere other than /cable
, you would need
to change the initialization to something like this:
App.cable = ActionCable.createConsumer("ws://cable.pollish.com");
With the client now looking for a place to connect, we must provide it with an endpoint to fulfill that search. To do that, we’ll create our ActionCable::Connection
. Again, this class is responsible for determining authentication of the consumer (i.e. the client or browser)
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
ActionCable::Connection
uses identified_by
as “a connection identifier that
can be used to find the specific connection later.”(ActionCable Overview) In the
example above, we’re grabbing the user object from the “warden” environment
variable. We can’t grab it from the session, because “[t]he WebSocket server
doesn’t have access to the session.”
Once the connection is established, the ActionCable server is able to identify the connection and channel through which it will send data.
With our connection established, we’re ready to subscribe to channels. Again, we’ll start with our client code:
// /app/assests/javascripts/cable/subscriptions/polling.js
App.cable.subscriptions.create(
{ channel: "PollingChannel", code: "example" },
{
connected: function() {
console.log("connected");
},
disconnected: function() {
console.log("disconnected");
},
rejected: function() {
console.log("rejected");
},
received: function(data) {
console.log(data);
myChart.data.datasets[0].data = [
_.find(data, function(vote) {
return vote.value == "yes";
}).vote_count,
_.find(data, function(vote) {
return vote.value == "no";
}).vote_count,
_.find(data, function(vote) {
return vote.value == "maybe";
}).vote_count
];
myChart.update();
}
}
);
In the code above, we are sending a block of data (the parameters identified by
“channel” and “code”) identifying what the client is subscribing to. The
channel
parameter is required for ActionCable to determine which channel (in
/app/channels
) to subscribe to. Any subsequent parameters can be added to
further identify the data to retrieve or just to pass useful information. We’ll
look at PollingChannel
momentarily.
The next thing to notice are the four methods: connected
, disconnected
, rejected
, and received
. These methods are activated depending on what is happening with the connection. You can add more methods to provide more functionality, but these four will be triggered by ActionCable itself.
The last thing to note is the received
method. Here we’re taking the data
received and populating a three element array with the counts for “yes”, “no” and “maybe”. Once we have that, we update myChart
.
Note: we’re using the find
method from lodash.js, and accessing the myChart
variable defined in /app/views/results/_example.html.erb
Finally, we need the server to publish to our subscriber. We do that with /app/channels/polling_channel.rb
From the Rails Guide:
If you have a stream that is related to a model, then the broadcasting used can be generated from the model and channel.
This is exactly what we’re doing here:
class PollingChannel < ApplicationCable::Channel
def subscribed
poll = Poll.where(code: params[:code]).first
stream_for poll
end
end
We are taking the code
parameter passed from the polling.js
and streaming
for the object identified by the “code” passed in, (in this case, it is
“example”). Any time data is “broadcast” for a Poll
, it can do so over the
PollingChannel
.
How is data broadcast? By using the broadcast_to
class method inherited from
ApplicationCable::Channel
. For the purposes of our example, we can add that to
the save
method of the VotesController
. Here is an excerpt of the updated
logic:
# /app/controllers/votes_controller.rb
class VotesController < ApplicationController
…
if @vote.save
PollingChannel.broadcast_to(@poll, @poll.results)
session[@poll.code] = true
redirect_to polls_path, notice: message
else
…
end
When a vote is cast, the server broadcasts the new results for the poll to the channel for that poll, which all subscribers then use to update the results view.
In our example, it “feels” like there’s a lot going on, but the majority of the work will be with the files in /app/assets/javascripts/cable/subscription
, /app/channels
, and wherever you need to trigger a broadcast.
Out of the box, ActionCable will not work with the Rails console (or Rake tasks) the way you might expect it to. While building Pollish and researching ActionCable, I burned a number of cycles figuring that out. I had hoped I could trigger page updates by sending PollingChannel.broadcast_to(@poll, @poll.results)
with new data, but it wasn’t to be.
By default, the development environment uses the async
adapter, which only operates on a single process. In order to allow broadcasts from multiple sources, change the adapter to redis
(this assumes you have redis running in your development environment) in /config/cable.yml
:
development:
adapter: redis
url: redis://localhost:6379/1
test:
adapter: async
production:
adapter: redis
url: redis://localhost:6379/1
You know what I’m going to say here, right?
It depends.
Whether you use ActionCable or not is dependent on your current and future needs. ActionCable is definitely cool and we have a lot of fun playing with it, but it’s not right for every situation and we’re being slow about recommending its implementation for some of the following reasons:
Is there a feature in your app which can actively benefit from being a live feed? Is it really going to make a difference to your users? As developers, we often look for excuses to play with new toys, and we’ve been known to justify the addition of a feature merely to experiment with those new toys. Adding ActionCable to your app is a real commitment; make sure the addition is warranted, and not just an excuse to play.
The Rails community has been very vocal about the added complexity which comes with each new version of Rails, and 5.0 is no different. Consider the example we used in this article. There is new terminology, new classes and resources, and javascript files which have to stay in line with those new classes and resources.
Not only are there new files, naming, and resources, there’s also a new server. Running ActionCable in production requires the addition of the Redis data structure server.
ActionCable requires a lot more memory and a lot more CPU power. Matthew O’Riordan, writing for the Ably blog found in testing that “…ActionCable started over 1,690 operating system threads. In comparison, the process with next highest number of threads was the kernel itself at circa 150 threads.”
Furthermore, O’Riordan and others found ActionCable to lag behind alternative frameworks with regard to performance. Without a lot of solid understanding about performance issues and ActionCable in general, it’s likely not going to be a good fit for high traffic sites.
Is real-time really necessary? Sometimes it is, but sometimes an occasional update is just as good. Polling has been around a long time and can be just as effective as websockets in the right circumstances. It furthermore has the added advantage of not needing to recover from getting disconnected from the server; it’s a brand new connection each time.
ActionCable is likely the most exciting feature to come out of Rails 5.0, and in this article we’ve looked at how it fits in to the existing Rails directory structure, but more importantly we’ve discovered the responsibilities of each of the new components and how they fit into the new architecture.
In the same way that there is more to WebSockets than merely chat rooms, there’s more to ActionCable than what we’ve touched on in this article. As more and more developers take advantage of this new feature, expect to see new and creative features get added to both familiar and as of yet undiscovered websites.
Even though we’ve been slow to recommend clients implement ActionCable in their apps, we’re still excited and bullish on this new addition to Rails. This is the first release of ActionCable, and like all features, it’s only going to improve. There was a time when ActiveRecord was utter rubbish with an inconsistent API and occasionally was the cause of data loss. Look at how far it’s come. We expect the same kind of improvements with ActionCable. It’s only going to get better.