In modern web applications, real-time communication has become more than a feature: it's gradually evolved into a necessity. Users expect instant updates, live interactions, and dynamic content.
In Rails applications, Action Cable has long been the go-to solution, harnessing WebSockets to fulfill these demands. In this article, we introduce:
- The basics of WebSockets
- How Action Cable enables real-time communication via WebSockets
- Why AnyCable was created
- How to get AnyCable up and running
- The improvements AnyCable brings to the table
Let's get started!
The WebSocket Technology
The WebSocket technology was introduced in 2011 with the publication of RFC 6455, titled "The WebSocket Protocol". This standardized protocol facilitated the establishment of persistent, real-time communication channels between web browsers and servers, allowing for more efficient and continuous data exchange compared to traditional HTTP.
The WebSocket technology enables continuous bi-directional communication between clients and servers, a stark departure from the request-response model of HTTP (where connections terminate after each exchange). This persistence in communication proves pivotal for real-time applications like chat platforms and gaming systems.
Before the introduction of WebSocket technology, many applications often relied on a technique called polling. Polling involves the client repeatedly sending requests to the server at specified intervals to check for updates or new information. While this method allowed for some level of real-time updates, it was less efficient than WebSocket communication. Polling could lead to unnecessary server load and increased latency, as the client had to initiate a new request each time it wanted to retrieve updated data.
WebSocket technology addressed these limitations by establishing a persistent connection between the client and the server, enabling more immediate and efficient data transmission without the need for constant polling. This shift significantly improved the responsiveness and real-time capabilities of applications, providing a seamless and dynamic experience for users, particularly in scenarios demanding instant, up-to-the-moment information.
Both Action Cable and AnyCable harness the power of web sockets to furnish Rails applications with real-time updates, liberating users from the cumbersome need to refresh pages or poll for the latest information.
Action Cable and Real-time Communication
Action Cable enables the incorporation of real-time features into a Rails application via web sockets. This real-time communication happens between the client side (browser) and the Rails server. A few things are important to note:
- A connection forms the foundation of this client-server relationship. For every WebSocket connection, Action Cable creates one connection instance.
- A channel refers to a communication pathway that allows clients to subscribe to and receive updates about specific topics or categories of information, e.g.,
BookChannel
,MovieChannel
,SportsChannel
. - A subscriber is a client subscribed to a channel to receive information.
- Broadcasting is when the server publishes information that is transmitted by the relevant channel to the subscribed client/s.
- A stream is the route or pathway through which broadcasts get to the subscribers. By using
stream_from
, subscriptions can be made for broadcasting.
When a connection is established between the client and the server, Action Cable creates a connection object for that connection. With this connection, the client can subscribe to one or more channels. Action Cable broadcasts updates to all subscribed clients via their respective channels.
A simple example is a browser view showing a library book list that updates when a book is added.
# create the rails application
rails new book_list
cd book_list
# create the needed controller and model
rails g controller books index
rails g model Book name
rails db:migrate
# change the development Action Cable adapter from Async (the default one) to Redis
rails turbo:install:redis
# start the redis server
redis-server
# start the rails server
rails s
To get a real-time update on the book model, we employ Action Cable for broadcasting the updates. We set up Action Cable to broadcast these updates to a channel called "books". Via Turbo Streams, we create the subscriber to this channel and deliver the updates.
# app/models/books.rb
class Book < ApplicationRecord
broadcasts_to -> (book) { :books }
# Broadcasts all the activities on this model -> create, update, destroy.
end
We can now render the list of books:
# app/views/rooms/_book.html.erb
<div id=<%= dom_id(book) %>> <%= book.name %> </div>
# app/views/books/index.html.erb
<h1>Books</h1>
<%= turbo_stream_from "books" %>
<div id="books">
<%= render @books %>
</div>
As seen below, different actions trigger a broadcast, and Action Cable broadcasts to the channel specified — "books". The view is also subsequently updated to reflect these updates.
irb> Book.create(name: "The Avatar")
Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 11)
[ActionCable] Broadcasting to books: "<turbo-stream action=\"append\" target=\"books\"><template><div> The Avatar </div></template></turbo-stream>"
=> #<Book:0x0000000107c13358>
irb> book_1 = Book.find(1)
irb> book_1.update(name: "The Demo")
Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 11)
[ActionCable] Broadcasting to books: "<turbo-stream action=\"replace\" target=\"book_1\"><template><div> The Demo </div></template></turbo-stream>"
=> true
irb> book_1.destroy
[ActionCable] Broadcasting to books: "<turbo-stream action=\"remove\" target=\"book_7\"></turbo-stream>"
=> #<Book:0x0000000107bd2948
In Rails, integrating Action Cable (and, as a result, real-time updates into an application) is quite easy: even a beginner can get up and running in minutes. With Action Cable, we can manage connections, channels, and broadcasting to clients without worrying about the underlying WebSocket details. It also uses the same connection for multiple subscriptions, reducing the overhead of creating new connections for each new real-time update.
Why AnyCable for Ruby on Rails?
Action Cable is built on Ruby and has significant differences from other servers written in Golang and Erlang. Check out a detailed summary of AnyCable vs. Action Cable benchmark findings. Here are the key results:
- Action Cable consumes approximately 4 times more memory when handling 20 thousand idle clients without subscriptions or transmissions.
- Action Cable requires much higher CPU usage.
- Action Cable takes around 10 times longer to broadcast to 10,000 clients. Starting at 1 second for a thousand clients, the time required increases by 1 second for every additional thousand clients.
The AnyCable WebSocket Server was created to combine the beauty of Action Cable with the performance benefits gained from Golang. AnyCable handles WebSockets on a different server called AnyCable-Go, effectively reducing the burden on your primary web application.
In addition to performance advantages, AnyCable offers resumability. If a client becomes disconnected from the server (e.g., due to network issues leading to an offline status), upon reconnection, the client will receive all the messages missed during the disconnection. They are also not required to re-subscribe as sessions are recovered. See a brief demo of the resumability feature.
View detailed information on the recent enhancements to AnyCable.
Integrating AnyCable
Let's replace Action Cable with AnyCable in our BookList
example.
Install the gem:
# add the gem to your gemfile
gem "anycable-rails"
# install it
bundle install
Next, run the setup generator.
This generator is interactive and asks questions about:
- The type of development environment in use.
- How you would like to install the
AnyCable-Go
websocket server. - If Heroku is being used for deployment.
- If
JWT
is the intended means of authentication. - If you want a
Procfile.dev
file.
All of this information helps to reduce the amount of manual work you need to do.
rails g anycable:setup
If you did say "yes" to the generation of a Procfile.dev
, it would look something like this:
# Procfile.dev
web: bin/rails s
anycable: bundle exec anycable
ws: bin/anycable-go --port=8080
Hence, you do not need to manually run the rest of the commands below.
Run the AnyCable RPC server:
bundle exec anycable
Run the WebSocket server:
anycable-go --port=8080
Restart the Rails server.
In the cable.yml
file, you'll find that the adapter changes to any_cable
if the ACTION_CABLE_ADAPTER
environment variable is not set.
adapter: <%= ENV.fetch("ACTION_CABLE_ADAPTER", "any_cable") %>
In all the $env.rb
files, you'll find the Action Cable URL set as:
config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL", "/cable") if AnyCable::Rails.enabled?
In the any_cable.yml
file, the Redis channel is set to __anycable__
. You can confirm this in the terminal via:
redis-cli PUBSUB CHANNELS
# 1) "__anycable__"
Read more about the AnyCable configuration.
In the Rails server, you'll find a routing error related to the /cable
endpoint. You'll also find that, when you filter by websockets
in the Network
tab of the browser, the Request URL
for cable
is ws://localhost:3000/cable
, which is wrong. We should be attempting to connect to ws://localhost:8080/cable
, the value of config.action_cable.url
.
To fix this, the action_cable_meta_tag
has to be added to the application.html.erb
file.
# application.html.erb
<head>
# ...
<%= action_cable_meta_tag %>
# ...
</head>
This resets the url
to the set value. At this point, any broadcasted stream will show up!
Swapping Action Cable for AnyCable in Ruby
To enable resumability, AnyCable utilizes the Action Cable Extended Protocol, which is implemented by the AnyCable server version 1.4 and later. This protocol is inherently supported by the AnyCable JS client.
To integrate this with Turbo Streams, it's necessary to swap out the default Action Cable client for the AnyCable client. However, when using the @hotwired/turbo-rails
package, the Action Cable getConsumer
function is invoked before the setConsumer
function during the initial page load, leading to the consumer not being overridden, and two web socket connections being opened. The @anycable/turbo-stream package was created to handle this (read additional details about this issue).
Here's the procedure for this swap.
Add the @anycable/web
and @anycable/turbo-stream
packages:
bin/importmap pin @anycable/web @anycable/turbo-stream
This pins the stated packages, along with @anycable/core
, @hotwired/turbo
, and nanoevents
to your importmap.rb
file.
Create the anycable
client:
// app/javascript/cable.js
import { createCable } from "@anycable/web";
export default createCable();
Refer to this as the new "cable" client:
# config/importmap.rb
pin "cable", to: "cable.js", preload: true
Start the new cable client:
// app/javascript/application.js
// We no longer need @hotwired/turbo-rails
import "@hotwired/turbo";
import { start } from "@anycable/turbo-stream";
import cable from "cable";
start(cable);
With this, we can restart our server, and the consumer/client is replaced with AnyCable.
Read more about advanced configurations for the Turbo Streams package.
With the client replaced, resumability can be implemented.
With resumable sessions, the supported adapters are http
and redisx
(if you use Redis). It is, however, important to note that the broker preset
command automatically sets the adapter as http
. Hence, when using redisx
, it must specified explicitly like so:
$ anycable-go --broker=memory --broadcast_adapter=redisx
There you go! You should have resumable sessions working!
Community Adoption and Feedback
AnyCable has existed for several years and has gained widespread adoption among numerous organizations. Adopters have cited several compelling reasons for its attractiveness, including:
- Seamless migration without the need for architectural changes.
- No requirement to modify Action Cable channels or connections.
- Utilization of a dedicated server for WebSockets.
- Ability to handle a substantial number of concurrent connections without encountering any problems.
See a few testimonials from satisfied users.
Wrapping Up
In this post, we've seen that AnyCable offers performance enhancements to Action Cable and can be used as an alternative when dealing with a large number of connections or high traffic.
With recent improvements to AnyCable (such as the introduction of reliable streams), AnyCable has become a solid choice regardless of performance concerns. It supports broadcasting and subscription confirmation, thereby making it consistent and reliable. It can also be used in non-Rails applications, and to power serverless JavaScript applications.
Overall, AnyCable is a reliable choice for modern web applications and supports a variety of servers, allowing for greater flexibility in development.
Happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Top comments (1)
Thanks, really smart content. It sounds like there are some ways to get hung up with AnyCable, that it’s more effort to setup (definitely) and that its benefits really only apply to a site with 1000+ concurrent users (or thereabouts?). It’s nice to know there is an upgrade path if this happens though, because to be honest I would be concerned otherwise about using Ruby for a highly event-driven system. Especially when there are languages like JS and Go that handle it better. But who wants to start with Golang? Not me. Would rather be in the comfy nest of Rails for the early phases.