Rails 5 Preview: Action Cable Part I

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 Users 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.

Labs have a title and a description, as mentioned above, and Snippets have content. Snippets 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.

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus