For those of you out there who are eagerly awaiting the upcoming release of Rails 5 (okay, all of you), here's a preview of one of it's soon-to-be native features: Action Cable.
Aside: For some reason, whenever I hear "Action Cable" I think of the Captain Planet theme song. Not sure why, but I'm just going to go with it. Here is a picture of Captain Planet:
http://captainplanetfoundation.org/wp-content/uploads/2011/11/planeteers-youth.jpg
What is Action Cable?
According to Action Cable, Action Cable:
...seamlessly integrates websockets with the rest of your Rails application. It allows 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. It's a full-stack offering that provides both a client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with ActiveRecord or your ORM of choice.
In other words, Action Cable holds open a socket connection within your Rails app and allows you to define a channel (i.e. that socket), stream or post data to that channel and get or subscribe to data from that channel. For those of you familiar with the pub/sub model, the concept is the same.
An excellent and common use-case for this capability would be that of a chat between different users of your application. I decided to make it even harder on myself by going in a slightly different direction, but the concept is the same as that of a chat.
This post will serve as the first of two on implementing and deploying a chat-like application using Action Cable.
App Background
Lately I've become increasingly interested in the ways in which programmers communicate about code. What tools are available to facilitate remote collaboration? Thinking more about this, I decided to build an app through which users could upload code snippets into a chat room for other users to comment and collaborate on. Hence Code Party (sorry, working title) was born. The functionality of uploading code snippets fast took a back seat to using and deploying Action Cable, so this app is nowhere near complete. I did, however, want to share what I've learned about Action Cable before too much time had passed.
Application Architecture
##### Models and Migrations
Okay, now that we have the background out of the way, let's talk a little about the structure of the application. We have User
, Lab
and Snippet
models. Users can create labs (think code challenge) by giving them a title and a description. A lab's show page functions as a chat room and users can post snippets.
We'll start with the User
s model. Users
can sign up and sign in (using Devise). They can create a new Lab
that has a title and a description. Then, on that lab's show page, user's can submit code snippets. For the purposes of this tutorial, think of a lab as a conversation or chat room and snippets as the messages flying back and forth between users.
Lab
s have a title and a description, as mentioned above, and Snippet
s have content. Snippet
s belong to the user who posts them, and a lab has many snippets and many users through snippets.
So, when you generate your migrations, your snippets table should have a user_id
column and a lab_id
column, as well as a column for content
.
Let's take a look at the models to solidify our understanding of their attributes and associations.
# app/models/lab.rb
class Lab < ActiveRecord::Base
has_many :snippets
has_many :users, through: :snippets
validates_presence_of :name
end
# app/models/user.rb
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
validates_presence_of :name, :email
has_many :snippets
end
# app/models/snippet.rb
class Snippet < ActiveRecord::Base
belongs_to :user
belongs_to :lab
validates_presence_of :content
end
There are no private labs (i.e. chat rooms), so any snippet that gets posted simply belongs to the user that posted it and belongs to the lab that it is posted to. This app has no concept of sender/recipient users at the current time.
Next, we define our routes.
Routes
Our routes are pretty straightforward. We have the Devise routes for signing up, in and out. We have a landing page and we have the resources for creating, editing and destroying a lab as well as the resources for creating, editing and destroying a snippet:
# config/routes.rb
Rails.application.routes.draw do
resources :labs
resources :snippets
devise_for :users
resources :users
root 'welcome#welcome'
devise_scope :user do
get "/login" => "devise/sessions#new"
get "signup" => "devise/registrations#new"
get "logout" => "devise/sessions#destroy"
end
We also have some extra routes in there (the show page for an individual snippet for example) that we don't really need, but I'll clean that up later.
Let's create the necessary views for creating a lab, visiting that lab's show page and posting a snippet to that page.
Views
#app/views/labs/new.html.erb
<h1>Create a new lab!</h1>
<%=form_for(@lab) do |f|%>
<%=f.label :name%>
<%=f.text_field :name %>
<%=f.label :description%>
<%=f.text_field :description %>
<%=f.submit "create"%>
<%end%>
We can create a new lab with the above form. That form posts to the create
action of the LabsController
which will instantiate and save our new lab.
Let's check out the show page for an individual lab:
# app/views/labs/show.html.erb
<h1><%=@lab.name%></h1>
<h2><%=@lab.description%></h2>
<div id="snippets">
</div>
Add your snippet!
<%=form_for @snippet, :remote=> true do |f|%>
<%=f.label :content%>
<%=f.text_area :content %>
<%=f.hidden_field :lab, value: @lab.id%>
<%=f.submit "create"%>
<%end%>
On the lab's show page, we have a form_for
a new snippet. The form has the remote: true
data attribute so that we can send the post request for creating a new snippet Ajax-ically. We also have an empty < div > with an id of "snippets" that we will append newly-created snippets to after a user submits them.
Now we need to build out our SnippetsController
to successfully create new snippets, then we'll implement Action Cable.
Controllers
# app/controllers/snippets_controller.rb
class SnippetsController < ApplicationController
def create
@snippet = Snippet.create(snippet_params)
@snippet.user = current_user
@snippet.lab = Lab.find(params[:snippet][:lab])
@snippet.save
end
private
def snippet_params
params.require(:snippet).permit(:content)
end
end
We need Action Cable
Okay, at this point our app successfully allows users to create new snippets via the form on a lab's show page. That form uses remote: true
so that the user doesn't leave the page and the page doesn't refresh. At this point, we could use Ajax to easily append the newly-created snippet to the page that the user is currently on. But what about other users looking at the same page from their browsers? Just using Ajax alone doesn't implement a chat functionality. For a chat to work, the server must be able to send new snippets to any one in the chat room. In order for that to happen, the chat room needs to be listening to the server at all times. There are a number of ways to do this. You could use Faye and Private Pub to set up the publish/subscribe model or Javascript long polling to constantly ping the server for new snippets/messages. Rails 5 however, will come with Action Cable out of the box. Action Cable is specifically designed to easily integrate Websockets into a Rails app. Because we are cutting-edge programmers, we're going to try out this new technology.
Implementing Action Cable
##### Puma and Action Cable
Include the actioncable
and puma
gems in your Gemfile.
# Gemfile
ruby '2.2.1'
gem 'actioncable', github: 'rails/actioncable'
gem 'puma'
Make sure you are using Ruby version of 2.2.0 or greater.
I chose to use Puma for my web server but Action Cable supports Unicorn and Passenger as well as Puma. Action Cable runs its own server process. In other words, Action Cable will run on (in my case) the Puma server and the rest of our application will run on a separate set of server processes entirely. The Action Cable process will operate over the Puma server and listen to certain actions being carried out by our main application and post or stream to other areas of the main application.
How? I think the Action Cable docs explain it best:
Action Cable uses the Rack socket hijacking API to take over control of connections from the application server. Action Cable then manages connections internally, in a multithreaded manner, regardless of whether the application server is multi-threaded or not.
Establishing Channels
In the top level of app
create a directory channels
. First, we'll define our ApplicationCable::Connection
class. This is the place where we would authorize the incoming connection, and proceed to establish it if all is well. We're not going to do any authorization for this example. It's not necessary to get it working. We will establish the connection though.
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end
Now we'll define our ApplicationCable::Channel
class. This is where we'll put any shared logic between channels. For the purposes of this example, though, we only have one channel. We won't put much code here, but we will inherit our SnippetsChannel
from this class.
# app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
Now we're ready to define our SnippetsChannel
.
# app/channels/snippets_channel.rb
class SnippetsChannel < ApplicationCable::Channel
def subscribed
stream_from 'snippets'
end
end
Whenever a client subscribes to SnippetsChannel, the .subscribed
method will be called. This method streams anything broadcast to the “snippets” stream. Let's take a closer look at this broadcast/stream relationship.
Action Cable's Broadcast/Stream Model
Let's think about the cycle of posting and receiving snippets in plain English first.
We need to set up a publish/subscribe, or broadcast/stream, relationship between a lab's show page and the create action of the SnippetsController
. When new snippets are created, they should be broadcast from the server to all of the clients subscribing to, or streaming from, the SnippetsChannel
––i.e., anyone looking at that lab's show page.
Broadcasting Snippets
When does a new snippet need to get broadcast to the SnippetsChannel
? Right after it is created. So, we'll write our broadcasting code in the create
action of the SnippetsController
:
class SnippetsController < ApplicationController
def create
@snippet = Snippet.create(snippet_params)
@snippet.user = current_user
@snippet.lab = Lab.find(params[:snippet][:lab])
@snippet.save
ActionCable.server.broadcast 'snippets',
snippet: @snippet.content,
user: @snippet.user
head :ok
end
...
This sets up the stream for our SnippetsChannel
to stream from. Let's revisit our SnippetsChannel
:
# app/channels/snippets_channel.rb
class SnippetsChannel < ApplicationCable::Channel
def subscribed
stream_from 'snippets'
end
end
Now, we need to write some Javascript for our lab's show page. We need to write a function that will subscribe to the snippets stream we just finished creating.
Subscribing to Snippets
# app/assets/javascripts/channels/snippets.js
App.snippets = App.cable.subscriptions.create('SnippetsChannel', {
received: function(data) {
return $('#snippets').append(this.renderSnippet(data));
},
renderSnippet: function(data) {
return "<p> <b>" + data.user.name + ": </b>" + data.snippet + "</p>";
}
});
Here, we are subscribing to the SnippetsChannel
. The subscribed
method of the SnippetsChannel
gets triggered by the above function, thereby linking this function to the snippets stream set up in our SnippetsController
.
When the client receives data through the websocket, he App.snippets.received function gets called. The incoming data is sent as JSON, so we can access data.user.name and data.snippet in the renderSnippet function.
Lastly, make sure your /channels
javascript gets included in application.js
by adding the following line:
# app/assets/javascripts/application.js
...
//= require_tree ./channels
Okay, we're almost done. We have our broadcast/subscribe model set up and we've already discussed that we need the Puma web server to subscribe to the broadcast. Now we need to set up our Puma server and Action Cable's connection to it.
Configuring the Action Cable Server
Action Cable is going to run on a Puma server on port 28080 and our main application will run on a Puma server on port 5000.
In the top level of your directory, create a cable
directory. Create the following file:
# cable/config.ru
require ::File.expand_path('../../config/environment', __FILE__)
Rails.application.eager_load!
require 'action_cable/process/logging'
ActionCable.server.config.allowed_request_origins = ["http://localhost:3000"]
run ActionCable.server
Note: The following line: ActionCable.server.config.allowed_request_origins = ["http://localhost:3000"]
is specific to your development environment. You'll need to change this to the URL of your app once you're in production. This line is recently required due to some security changes to Action Cable. Thanks to median for the heads up regarding this change.
We'll rack up our Action Cable server with the following line:
bundle exec puma -p 28080 cable/config.ru
Now we're ready to establish our connection to the Action Cable server on the client side.
# app/javascripts/channels/labs.js
//= require cable
//= require_self
//= require_tree .
this.App = {};
App.cable = Cable.createConsumer('ws://127.0.0.1:28080');
Almost done! Action Cable publishes data to and subcribes to data from Redis. Let's configure Redis in our application.
Setting Up Redis
Make sure you have Redis installed on your machine. brew install redis
if you haven't done so already. Then, fire up the Redis server to test it out with redis-server
in your terminal.
Create a redis
subdirectory inside config
.
# config/redis/cable.yml
local: &local
:url: redis://localhost:6379
:host: localhost
:port: 6379
:timeout: 1
:inline: true
development: *local
test: *local
We're ready to run our app!
Run Locally with Foreman
Since we have three processes to fire up––redis, the main server and the application server––we'll use Foreman to execute a Procfile to start up all these processes with one simple command.
gem install foreman
if you haven't done so already.
Let's write our Procfile.
web: bundle exec puma -p 5000 ./config.ru
actioncable: bundle exec puma -p 28080 cable/config.ru
redis: redis-server
Now, fire up our app withe foreman s
in the command line and watch the magic happen!
Coming Up...
Next up we'll walk through how to deploy Action Cable to Heroku.
For further reference, you can checkout the code for this project here. Make sure you are looking at the "local" branch, not master. Master is a mess right now. Sorry.