DEV Community

Cover image for Action Cable Configuration & Subscriptions in Rails
Ethan Gustafson
Ethan Gustafson

Posted on • Updated on

Action Cable Configuration & Subscriptions in Rails

Table Of Contents

NOTE: At the time this blog was written, the Rails version in use was 6.0.


In this blog, I will walk through configuring and implementing Action Cable into a Rails Application. As I'm writing, I don't know how Action Cable works and what underlying processes make it functional.

This is why I love writing technical blogs. It is such a great way to learn and document processes to reference later. There's no way I'm going to remember everything, but as long as the fundamentals are there I will know where to look when I need to remember.

Action Cable

Action Cable is a bundle of code providing a client-side JavaScript framework and a server-side Ruby framework.

It integrates WebSockets with the rest of a rails application. This allows the application to have certain real-time features to be written in Ruby.

For an example, I'm currently writing an application called FilmPitch, where filmmakers can fund their dream movies. A Project has_many :comments. When a user comments, the browser will update so that the comment will display in real-time.

So what are WebSockets and how do they make real-time features possible?

Web Sockets

There is a lot of Wikipedia information in this section. I wanted to piece together the important bits to know before going forward.

A WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection.

A communications protocol is a system of rules that allow two or more entities of a communication system to transmit information via any kind of variation of a physical quantity.

A full-duplex (FDX) system, or sometimes called double-duplex, allows communication in both directions, and, unlike half-duplex, allows this to happen simultaneously.


The WebSocket protocol is different from HTTP, the Hypertext Transfer protocol, although it is compatible with HTTP. Essentially, the WebSocket protocol facilitates real-time data transfer from and to the server.

HTTP is a request-response protocol. It doesn't keep a connection open. It only sends data when requested. The WebSocket protocol sends data back and forth between client and server continuously, without being requested by the client.


This is made possible by providing a standardized way for the server to send content to the client without being first requested by the client, and allowing messages to be passed back and forth while keeping the connection open. In this way, a two-way ongoing conversation can take place between the client and the server.

For example, cell phones are full-duplex, as two callers are allowed to speak and hear the other at the same time.

TCP/IP

The Transmission Control Protocol (TCP) is one of the main protocols of the Internet protocol suite. TCP provides reliable, ordered, and error-checked delivery of a stream of octets (bytes) between applications running on hosts communicating via an IP network.

TCP is connection-oriented, and a connection between client and server is established before data can be sent. The server must be listening (passive open) for connection requests from clients before a connection is established. Three-way handshake (active open), retransmission, and error-detection adds to reliability but lengthens latency.


Applications that do not require reliable data stream service may use the User Datagram Protocol (UDP), which provides a connectionless datagram service that prioritizes time over reliability. TCP employs network congestion avoidance. However, there are vulnerabilities to TCP including denial of service, connection hijacking, TCP veto, and reset attack. For network security, monitoring, and debugging, TCP traffic can be intercepted and logged with a packet sniffer.

The Network Function section of the TCP Wiki will detail more on how the protocol functions.


The Internet protocol suite is the conceptual model and set of communications protocols used in the Internet and similar computer networks. It is commonly known as TCP/IP because the foundational protocols in the suite are the Transmission Control Protocol (TCP) and the Internet Protocol (IP).

The Internet protocol suite provides end-to-end data communication specifying how data should be packetized, addressed, transmitted, routed, and received. This functionality is organized into four abstraction layers, which classify all related protocols according to the scope of networking involved. From lowest to highest, the layers are the link layer, containing communication methods for data that remains within a single network segment (link); the internet layer, providing internetworking between independent networks; the transport layer, handling host-to-host communication; and the application layer, providing process-to-process data exchange for applications.

Terminology

The Action Cable Terminology section of the Ruby on Rails Guide will detail all of the terms I list below. I'll stitch together everything so that it makes more sense. If it doesn't, the configuration section will help make it clear.

Action Cable can handle many connection instances. There is one connection instance for every WebSocket. A user could have more than one tab open in their browser, which means that there can be more than one connection instance in a user's browser.

The client is referred to as the browser. The client of a WebSocket connection is called the consumer.

Each consumer can subscribe to multiple cable channels. When a consumer is subscribed to a channel, they act as a subscriber.

Pub/Sub, or Publish-Subscribe, refers to a message queue paradigm whereby senders of information (publishers), send data to an abstract class of recipients (subscribers), without specifying individual recipients. Action Cable uses this approach to communicate between the server and many clients.

Essentially, all users(consumers) subscribed to a channel will get updates without requesting them.

In software architecture, publish-subscribe is a messaging pattern where senders of messages, called publishers, do not program the messages to be sent directly to specific receivers, called subscribers, but instead categorize published messages into classes without knowledge of which subscribers, if any, there may be. Similarly, subscribers express interest in one or more classes and only receive messages that are of interest, without knowledge of which publishers, if any, there are.

The connection between the subscriber and the channel is called a subscription. A consumer could subscribe to multiple chat rooms at the same time.

Each channel encapsulates a logical unit of work, similar to what a controller does in a regular MVC setup. For example, you could have a ChatChannel and an AppearancesChannel, and a consumer could be subscribed to either or to both of these channels. At the very least, a consumer should be subscribed to one channel.

Each channel can stream zero or more broadcastings. A broadcasting is a pubsub link where anything transmitted by the broadcaster is sent directly to the channel subscribers who are streaming that named broadcasting.

Controllers will work like normal. In my Commentscontroller, the #create action is what will create, save, and call the job that will broadcast the newly saved comment to the channel. ActiveJob will then handle broadcasting the information to channel subscribers.

Queue Data Structure

The Queue Data Structure is just like the Stack Data Structure. Stacks follow a LIFO (Last-in First-out) principle. Queues follow the FIFO (First-in First-out) principle.

Rails/JavaScript Code

This section details the purpose behind the files in app/channels and app/javascript/channels. Don't worry about configuration for now.

A lot of it is from the Action Cable guide, and that is on purpose. The important bits are set in bold. The Terminology section introduces the terms, this section introduces what you'll be working with, and the configuration section pieces everything together in a linear fashion.

Server-Side Components

Connections

For every WebSocket accepted by the server, a connection object is instantiated. This object becomes the parent of all channel subscriptions that are created from thereon.

The connection itself does not deal with any specific application logic beyond authentication and authorization.


Connections are instances of ApplicationCable::Connection. In this class, you authorize the incoming connection, and proceed to establish it if the user can be identified.

# 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

    private
      def find_verified_user
        if verified_user = User.find_by(id: cookies.encrypted[:user_id])
          verified_user
        else
          reject_unauthorized_connection
        end
      end
  end
end
Enter fullscreen mode Exit fullscreen mode

identified_by is a connection identifier that can be used to find this specific connection later.

The above example assumes that you authenticated your user somewhere else in your app and set a signed cookie with the user_id.

The cookie is then automatically sent to the connection instance when a new connection is attempted, and you use that to set the current_user. By identifying the connection by this same current user, you're also ensuring that you can later retrieve all open connections by a given user (and potentially disconnect them all if the user is deleted or unauthorized).

Channels

A channel encapsulates a logical unit of work, similar to what a controller does in a regular MVC setup. By default, Rails creates a parent ApplicationCable::Channel class for encapsulating shared logic between your channels.

# app/channels/application_cable/channel.rb
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end
Enter fullscreen mode Exit fullscreen mode

This is the parent channel.

You don't have to adjust anything here. Any new channel you create will inherit from ActionCable::Channel.

rails g channel --help will detail the ways in which you can generate a new channel. I will be creating a comments channel, so my command will be rails g channel Comments.

Subscriptions

Consumers subscribe to channels, acting as subscribers. Their connection is called a subscription. Produced messages are then routed to these channel subscriptions based on an identifier sent by the cable consumer.

Data is broadcasted to this channel.

# app/channels/comments_channel.rb

class CommentsChannel < ApplicationCable::Channel
  def subscribed
    # this is called when the consumer has successfully
    # become a subscriber to this channel.
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end
Enter fullscreen mode Exit fullscreen mode

Client-Side Components

Connections

Consumers require an instance of the connection on their side. This can be established using the following JavaScript, which is generated by default by Rails:

// app/javascript/channels/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()
Enter fullscreen mode Exit fullscreen mode

From the Action Cable guide, createConsumer will connect to "/cable" automatically if you don't specify a URL argument for it. There's not much else to this file.

Subscriber

In order for a user to subscribe to a channel, you have to create a subscription in your channel -> app/javascript/channels/${channel_name}.js.

My comments channel was generated like this:

import consumer from "./consumer";

// Generated with `rails g channel Comments`

consumer.subscriptions.create("CommentsChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
  }
});
Enter fullscreen mode Exit fullscreen mode

The channel name needs to match your rails channel name and/or room. If you've never seen the syntax above, it is a Method Properties shorthand feature in ES6.

It's essentially the same as:

{
  connected: function(){

  },
  disconnected: function(){

  },
  received: function(data){

  },
}
Enter fullscreen mode Exit fullscreen mode

If you need to see the flow of their Consumer, Subscription(s) classes, you can find them here.

Client-Server Interactions

Streams

Streams enable a channel to route broadcasts to subscribers. When new data is sent, the stream allows the channel to route that data to clients connected to the channel.

stream_for and stream_from basically do the same thing. Here is their code.

stream_for is more used for a related model. It automatically generates broadcasting from the model and channel for you.

Broadcasting

A broadcasting is a pub/sub link where anything transmitted by a publisher is routed directly to the channel subscribers who are streaming that named broadcasting.

The default pubsub queue for Action Cable is redis in production and async in development and test environments.

I will show you how to use ActiveJob with rails so that Action Cable can use Redis in the configuration section. ActiveJob allows jobs to run in queueing backends.

Subscriptions

When a consumer subscribes to a channel, they become a subscriber. The connection between the two is a subscription. The data sent by the rails channel will be available as an argument to the method properties objects in the channel js file.

The received(data) method is called when there's incoming data on the WebSocket for a channel. In my comments_channel.js file, the data is an already rendered erb template. It's already in HTML, so I'm just appending it where I want it.

received(data) {
    // console.log("Recieving...")
    console.log(data);
    // console.log("Appending...")
    this.appendComment(data);
    // console.log("I have appended!")
  },
Enter fullscreen mode Exit fullscreen mode

Passing Parameters to Channels

If you're looking at your ${name}_channel.rb #subscribed method confused about where the params are coming in from, they're coming from the ${name}_channel.js file. If you start byebug when the subscribed method is called, the only params you will get is the channel name because it was defined where the subscription was created at the top:

consumer.subscriptions.create("CommentsChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
  }
});

Enter fullscreen mode Exit fullscreen mode

Configuration

Note: I'm using Postgres & Devise in this application.

Redis

I will be using Redis as the queueing backend.

Redis is an in-memory data structure project implementing a distributed, in-memory key–value database with optional durability. Redis supports different kinds of abstract data structures, such as strings, lists, maps, sets, sorted sets, HyperLogLogs, bitmaps, streams, and spatial indexes.

If you don't have it installed on Mac, install it with brew install redis.

Install the Redis gem with gem install redis. In case this gem isn't in your Gemfile, add it and run bundle install.

In your config/cable.yml file, make sure the adapter for your environments is Redis. For some reason, Redis was having errors with the other adapters set with async, so I set them all to redis. Also set the URL, which should already be present in the environment file.

development:
  adapter: redis
  url: redis://localhost:6379/1
Enter fullscreen mode Exit fullscreen mode

In order for Rails to connect to Redis, you have to start a server in another terminal. Start a Redis server by running redis-server.

Action Cable Server

The Action Cable Server can run either separately from or alongside your application. I have it set so that it runs when I launch my Rails server.

config/application.rb

In config/application.rb, you have to mount the path for Action Cable: config.action_cable.mount_path = '/cable'. This is where it will listen for WebSocket requests.

views/layouts/application/html.erb

In the views/layouts/application/html.erb, add an action_cable_meta_tag in the head. ActionCable.createConsumer() will connect the path from this meta_tag and use it as an argument.

  <%= action_cable_meta_tag %>
Enter fullscreen mode Exit fullscreen mode

To configure the URL, add a call to action_cable_meta_tag in your HTML layout HEAD. This uses a URL or path typically set via config.action_cable.url in the environment configuration files.

config/environments/development

In config/environments/development, add:

config.action_cable.url = "ws:localhost:3000/cable"

  config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
  config.action_cable.worker_pool_size = 5
Enter fullscreen mode Exit fullscreen mode

Action Cable will only accept requests from specified origins, which are passed to the server config as an array.

Set the pool size equal to what you have in your config/database.yml file.

...your server must provide at least the same number of database connections as you have workers. The default worker pool size is set to 4, so that means you have to make at least 4 database connections available. You can change that in config/database.yml through the pool attribute.

config/routes.rb

I don't believe I saw this in the Action Cable guide nor the example application they had, but it is present in many other blog examples. Not sure why it's omitted in the guide, have to look into it later.

Mount the Action Cable Server in config/routes.rb:

  mount ActionCable.server => '/cable'
Enter fullscreen mode Exit fullscreen mode

app/channels/application_cable

In this directory, you'll find two files: channel.rb and connection.rb.

That channel is the parent channel, so you don't need to alter that file at all.

connection.rb is where you will authenticate and authorize your user for their connection. I'm using Devise, so my user is authenticated like so:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    def disconnect
      # Any cleanup work needed when the cable connection is cut.
      # close(reason: nil, reconnect: true)
    end

    private
      def find_verified_user
        if verified_user = env['warden'].user
          verified_user
        else
          # You can find the reject_unauthorized_connection method here -> https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/authorization.rb
          reject_unauthorized_connection
        end
      end
  end
end
Enter fullscreen mode Exit fullscreen mode

Essentially, logged in users connect to the action cable server. They don't become a subscriber yet, though. The channel's #subscribed method will handle that portion. This class is all about authenticating and authorizing the user for this specific connection, allowing Action Cable to find the connection later.

reject_unauthorized_connection is a method given to you by ActionCable::Connection::Authorization. You can also find this method here in the Github.

comments_channel.rb

I generated my comments channel with the rails g channel command.

class CommentsChannel < ApplicationCable::Channel
  def subscribed
    project = Project.find_by_id(params[:id])
    # in Rails 6.1, a new method for handling the below control structure is defined as
    # stream_or_reject_for(record), which houses this code:

    # if there is a record, subscribe the user and start a stream, else reject
    # the user and don't start a new stream.
    if project
      stream_for project
    else
      reject
    end
  end

  def receive(data)
    # Rebroadcast a message sent by one client to any other connected clients
    # ActionCable.server.broadcast(project, data)
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
    # stop_all_streams() -> Unsubscribes all streams associated with this channel from the pubsub queue
  end
end
Enter fullscreen mode Exit fullscreen mode

Right now, only the #subscribed method is functional. The params id is given to me from javascript. If the URL doesn't have a project id, the subscription won't be set, and no stream will start.

comments_channel.js

import consumer from "./consumer";

// Generated with `rails g channel Comments`

var url = window.location.href;
let id = url.slice(url.length - 1, url.length);

consumer.subscriptions.create({channel: "CommentsChannel", id: id}, {
  connected() {
    // Called when the subscription is ready for use on the server
    console.log("Connected to the comments channel!");
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
    // console.log("Recieving...")
    console.log(data);

    // console.log("Appending...")
    this.appendComment(data);
    // console.log("I have appended!")
  },

  appendComment(data){
    const commentSection = document.getElementById("comments");
    commentSection.insertAdjacentHTML("afterbegin", data);
  }
})
Enter fullscreen mode Exit fullscreen mode

For right now, the server gets the id from the URL. It sends it as a param to the rails channel subscribed method.

ActiveJob & Broadcasting

class CommentBroadcastJob < ApplicationJob
  queue_as :default

  # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.

    # def broadcast_to(model, message)
    #   ActionCable.server.broadcast(broadcasting_for(model), message)
    # end

  # Active Job objects can be defined by creating a class that inherits from the 
  # ActiveJob::Base class. The only necessary method to implement is the “perform” method.


  def perform(project, partial)
    CommentsChannel.broadcast_to(project, partial)
  end
end
Enter fullscreen mode Exit fullscreen mode

This class is used to send the broadcasts. What I'm doing here is having the project and partial broadcasted. It gets called in the CommentsController.

comments_controller.rb

def create
    @comment = Comment.new(comment_params)

    if @comment.valid?

      @comment.save
      # You have to use methods found in ActiveJob::Core::ClassMethods -> 
      # https://edgeapi.rubyonrails.org/classes/ActiveJob/Core/ClassMethods.html

      # To enqueue a job to be performed as soon as the queuing system is free, use:
      # .perform_later(record)

      @obj = {
        id: @comment.id,
        description: @comment.description,
        user_id: @comment.user_id,
        project_id: @comment.project_id,
        display_name: @comment.user.display_name
      }.as_json

      CommentBroadcastJob.perform_later(
        @comment.project, 
        render_to_string(
          partial: 'comments/comment',
          locals: {
            comment: @obj
          } 
        )
      )

    else
      redirect_to project_path(comment.project)
    end
  end
Enter fullscreen mode Exit fullscreen mode

This is all messy right now, but the data in my views are using a comments hash, so I'll end up refactoring this later. Either render or render_to_string works here. The partial will be created with the data you want while using rails helpers in the views:

<!-- views/comments/_comment.html.erb -->

<div>
  <div>
    <h4><%= comment['display_name'] %></h4>
    <p><%= comment['description'] %></p>
  </div>

  <% if current_user.id == comment['user_id'] %>
    <div>
      <button>Edit</button>
      <p>
        <%= link_to 'delete', 
        { controller: "comments", action: "destroy", id: comment['id'] }, 
        data: { confirm: 'Are you sure?' }, 
        method: :delete %>
      </p>
    </div>
  <% end %>
</div>
Enter fullscreen mode Exit fullscreen mode

This has allowed two users to see comments in real-time. Here is a gif showing the process:

Live Comments gif

I still have to figure out how I can stop displaying the edit/delete buttons for other users.

I figured it would be great to have this blog have the meat of everything. I spent a good amount of time going through many Wikipedia pages, rails guides, rails repos, blogs, and videos to figure out exactly how to get Action Cable to run. Hope it helps clear some confusion!

This is the project repo: FilmPitch

If you have any questions or observations, please comment below. 🤩

Latest comments (0)