AnyCable 1.0: Four years of real-time web with Ruby and Go

Cover for AnyCable 1.0: Four years of real-time web with Ruby and Go

Translations

If you’re interested in translating or adapting this post, please contact us first.

It is time to proudly announce the 1.0 release of AnyCable—a drop-in turbo-extension for Action Cable that relies on the same API and also works outside of Rails. It took me four years to turn a demented (last name pun intended) idea into a robust backbone for real-time Ruby applications. Discover new features, learn from our wins and fails, peek into the AnyCable’s future, and see how using Ruby and Go together gives you the best of both worlds!

AnyCable brings performance and scalability to real-time applications built with Ruby and Rails. It achieves that by moving low-level WebSocket handling from Ruby to Go but leaving all the business logic in your Rails (or plain Ruby) codebase. In other words, AnyCable makes your application performant without sacrificing productivity.

Check out the introductory post on AnyCable to learn more about its architecture and performance characteristics and visit the official website that is full of interactive explanations.

The project passed a long way from a Ruby gem and a Go binary to a family of tools, from a lone maintainer to the awesome team of collaborators and dozens of contributors, from a ~200 words README to the documentation website with more than 50 articles. But let’s leave la nostalgie behind and discuss the following topics today:

AnyCable 1.0: The highlights

The main idea behind AnyCable v1 was to make a transition from Action Cable to AnyCable as smooth as possible. Even though I stated right from the beginning that AnyCable was a plug-n-play solution for Rails applications, that wasn’t true for most use cases. People wrote long-reads about potential caveats, posted issues, shared workarounds and hacks. We made a tiny step towards a brighter future with v0.6.0 release, and now, with v1.0, we are making a giant leap!

AnyCable 1.0 aims to make a transition from Action Cable as smooth as possible.

You can find everything in the official release notes, but I am here to highlight some of them.

Interactive Quickstart

Configuring an existing project for use with AnyCable has not always been straightforward. A simple “add-a-gem-and-bundle-install” approach does not apply to such an advanced tool: we need to install a WebSocket server, update the framework configuration for development and production, and notify users about potential incompatibilities.

So, we came up with an interactive Rails generator (rails g anycable:setup), which automates most of the initial set up tasks, including:

  • Adding required configuration files and settings for AnyCable.
  • Preconfiguring AnyCable to support authentication with Devise (if used in a project).
  • Installing anycable-go server (or providing a snippet to add it to docker-compose.yml if Docker is detected in development).
  • Preparing the application to run on Heroku (if needed).
  • Running static compatibility checks.

AnyCable setup generator in action

We used the following rule of thumb when designing this script: every reported issue with the historic AnyCable setup should be covered by the generator.

So, if you encounter a problem after running anycable:setup, let us know, and we’ll fix it!

Better Action Cable compatibility

We reduced the number of unsupported Action Cable features from four to just two by introducing remote disconnection support (ActionCable.server.remote_connections.where(user: user).disconnect) and the ability to keep channel states between actions (usually done via instance variables in Action Cable):

class RoomChannel < ApplicationCable::Channel
  # AnyCable API similar to attr_accessor
  state_attr_accessor :room

  def subscribed
    self.room = Room.find(params["room_id"])
    stream_for room
  end

  def speak(data)
    broadcast_to room, message: data["message"]
  end
end

Initially, I was thinking of automagically detecting instance variables and attribute readers/writers, and hijacking them to add AnyCable state management functionality. I even started to implement the following algorithm:

  • Parse source code of channel classes and collect all known instance variables names (explicit or defined via attr_{reader|writer|accessor}).
  • Implicitly register these names as state accessors (via state_attr_accessor).
  • Rewrite all direct instance variables usage with readers/writers, @roomself.room (yeah, I am a big fan of transpiling).

Can you imagine the number of possible edge cases? I could and decided to step back. state_attr_accessor is first and only API method AnyCable adds to Action Cable.

Another major improvement related to compatibility is the ability to use Rack middlewares to enhance Rack request information.

A typical use-case is a Devise-backed authentication. Devise uses Warden under the hood, which, in turn, relies on middleware. This middleware initializes a Warden::Manager instance and stores it in the request.env["warden"] (which is later used by Devise).

Before v1.0, request objects in AnyCable didn’t take any application middlewares into account. That made such a popular scenario as authentication as complex as in the following example:

# AnyCable <1.0
def connect
  self.user = find_verified_user || reject_unauthorized_connection
end

def find_verified_user
  app_cookies_key = Rails.application.config.session_options[:key] ||
                    raise("No session cookies key in config")

  env["rack.session"] = cookies.encrypted[app_cookies_key]
  Warden::SessionSerializer.new(env).fetch(:user)
end

With Rack middleware support in AnyCable 1.0, the code above is very similar to the one you may already have for Action Cable:

# AnyCable >=1.0
def connect
  self.user = env["warden"].user(:user) || reject_unauthorized_connection
end

This feature also made request.session accessible in AnyCable out-of-the-box, which helped us solve another compatibility issue that we are going to discuss next.

AnyCable ❤️ Stimulus Reflex

Stimulus Reflex and its fellow project CableReady are gaining more and more popularity in the Ruby on Rails community. It doesn’t surprise me. Building interactive real-time web applications using a dead-simple server-rendered HTML-over-the-wire approach that does not lock you into any frontend framework is an exciting idea.

Unfortunately, the first attempts to run Stimulus Reflex with AnyCable v0.6 revealed a bunch of issues we haven’t seen in classic Action Cable apps. I decided to fix them all at any cost and as of today we have StimulusReflexExpo running on AnyCable without any issues!

New demo application

A good example application is worth more than tons of documentation.

We had a demo application since the very first release, but it quickly turned into an unhelpful monster. The main mistake was in trying to combine all the possible use cases in a single codebase and not having proper test coverage and CI configuration (thus, making upgrades painful).

So, instead of trying to revamp the old app, I decided to build a new one from scratch: meet AnyWork: AnyCable Rails Demo.

AnyCable Rails demo application

Built with modern tools such as Rails 6, Stimulus, and Tailwind CSS, this example is what I call an “omni-application”: an application with multiple variations living in separate branches and representing different usage scenarios. Every variation has a PR attached with a thorough description. See, for example, From Action to Any.

Thus, the demo application acts as an additional form of documentation, closer to code. We plan to link most of the documentation articles to the dedicated demo app variations. Subscribe to the repo updates and stay tuned!

Improved Heroku deployment instructions

Deploying AnyCable on Heroku still requires having two applications. While investigating potential workarounds (including a Heroku add-on), we’ve been improving our existing documentation based on our production Heroku experience.

In my opinion, the most interesting new addition to it is the ”Choosing the right formation” chapter providing the formula to calculate the required number for dynos depending on the application load:

Heroku formation formula

Calculating Heroku formation

Four years of cables: Lessons learned

During the last four years, I spent a decent amount of my spare time on AnyCable and other cables. I had to make a lot of decisions along the way, and not all of them made me happy in the end.

Before building a bright new future, we need to re-evaluate the past. So, let’s do that!

“A very cool project”—DHH

The first documented usage of the name “AnyCable” dates back to the June 10th, 2016. I made it up while ruminating over an upcoming conference talk: I wanted to speak at RailsClub Moscow (now RubyRussia), but I had no idea what to talk about. So, I shared a few ideas with my colleagues at Evil Martians, they liked the one on better Action Cable and encouraged me to propose it as well as to work on the prototype. That’s how I began to speak at conferences and maintain an open source project to speak at conferences!

Neither me nor other Martians were using AnyCable (or even plain Action Cable) in production until late 2017. For more than a year, I was flying blind: all I had was a handful of user-posted issues on GitHub and Gitter. So, I almost lost all the motivation to work on a very cool project and turned into a conference-driven developer: v0.5.0 was timed to my RubyConfMY talk and v0.6.0 to the RubyConf one. The hardest question for me during Q&A sessions was “How do you use AnyCable in production?” Telling ”A dinnae, ye ken” in response made me feel like I’m selling snake oil.

The situation changed by the end of 2018: more and more projects (including but not limited to those that Evil Martians did for commercial clients like eBay) started adopting Action Cable, some of them grew big enough to realize they need AnyCable to cope with high loads. Sometimes they came to us, Evil Martians, for commercial support, and I finally got an opportunity to battle-test a fun project I conceived years ago.

Open source for the sake of open source is not fun.

Although AnyCable survived the crisis, I told myself to never again deal with a massive open source project that is not directly related to my personal pain.

AnyCable-Go: From miss to bliss

The first server implementation for AnyCable was written in Erlang. Switching to Go was the right decision: better gRPC tooling, easier distribution (what could be simpler than a single binary?), a larger community (which means more contributors).

And developing with Go was amazingly fast in the beginning: it took me about a week and eight commits to build the first working version! I was pretty new to Go, and Go was rather young, so, it was much harder to discover best practices for code organization (compared to the Ruby community). So, I chose a typical Go way: a single main package, a dozen of files in the repo’s root, high coupling, zero or less tests (I had black-box testing though, see below). Should I say that this path led to a hardly maintainable mess?

I had a lot of planned features for v0.6.0, and it was clear that I wouldn’t be able to implement them with the “architecture” (or lack of it) we had then. So, the Great Refactoring began.

The Code City visualization for AnyCable-Go

The Code City visualization for AnyCable-Go v0.5.0 (left) and v1.0.0 (right) (made with GoCity)

The refactoring was inspired by a ”Structuring applications in Go” blog post—a needle in the hay of articles on Go code micro optimizations, error handling, and use of pointers. I also started looking for open source Go projects with a well-designed, in my very Ruby-ish opinion, architecture. Obviously, Faktory got on the list as well as Centrifugo and Telegraf. Using other people’s code as a reference (and sometimes borrowing it) helped me transform the mess into a codebase that is now a pleasure to work with.

Writing complex reliable software cannot be easy.

Why didn’t I try to absorb the ancestral knowledge in the first place? Probably, due to the reputation of Go being easy to learn and fast to ship things with (which turned out to be closer to the “slap shit together and deploy” approach). No, Go, writing complex reliable software could not be easy.

AnyCable-bility

One thing I never regret was writing a conformance testing tool for Action Cable-compatible WebSocket servers—AnyT. Technically, it’s a collection of integration tests describing different client-server communication scenarios and a CLI to run them along with a server under testing.

Investing in development tools pays off in the long term.

Without such a tool, writing new server implementations (e.g., AnyCable Rack server) or refactoring the existing ones would be much more difficult. AnyT helps me to avoid regressions and to keep AnyCable in sync with Action Cable (we also run tests against it—this is our baseline).

One fun fact about AnyT: initially, I made up a different name for it—anycablebility. I even made a release under that name, but then the strangest thing happened: my RubyGems ownership was stolen 🙀! I wasn’t able to restore the access and decided to change the name instead. And the new one is much better, don’t you think so?

The duality of Rails compatibility

As I already mentioned, AnyCable was designed as an Action Cable add-on and not a replacement. We still rely on Action Cable’s Ruby code and its client JavaScript libraries.

Without an intentional Action Cable support, we would not get a lot of users. So, that was a right strategy.

On the other hand, this dependency is slowing down the evolution of AnyCable: we need to invest resources in compatibility and cannot add all the new features we want. That would require hacking Action Cable code as well as writing custom clients.

The situation we’re currently in is similar to the story of hub, the CLI for GitHub. And the lessons we learned, too…

From Any to Many: The future of cables and how you can help

The v1.0 release has two purposes. First, we’re stating that AnyCable has stabilized and ready for production (although it was ready a while ago). Secondly, and that’s more important to me, we can now start working on v2!

Let’s turn the fantasy mode on and talk about the future of AnyCable.

AnyCable 2: Improved protocol, own channels framework for Ruby and other languages, modernized JS client, WebSocket gateway.

AnyCable 2.0 is going to be a revolution, a paradigm shift. We are no longer going to mimic Action Cable.

First of all, I want to re-visit the protocol. For example, add unique session identifiers, monotonic (within a stream) message IDs, action (perform) acknowledgments, batch operations, etc. These changes will help us to add such features as, for example, reliable delivery and a true RPC experience.

Changing the protocol would require writing the new client-side code. And we need a rewrite not only to support a new protocol but to provide a better developer experience.

Here is an example of a theoretical AnyCable JS client:

// channels/chat.js
import { Channel } from 'anycable'

export default class extends Channel {
  static identifier = 'chat';

  fetchHistory = () => this.perform('fetchHistory')
}

// index.js
import ChatChannel from 'channels/chat'

const roomId = 42
// JS camelCased keys are automatically transformed to Ruby snake_case
// at the server side
const channel = new ChatChannel({roomId})

// All async calls are Promise-based,
// so you can use await (of course, from within an async function)
await channel.connect()

// An alternative events API
channel.on('connect', () => console.log('Connected'))

// Here is where message acks are used
const messages = await channel.fetchHistory()

// Subscribing to incoming messages
channel.on('message', message => console.log(message))

Another useful feature we plan to support from the very beginning is sharing a connection between browser tabs (like Logux does).

Note that, in the example above, we have a simple string as a channel identifier ("chat"), not a Ruby class name. I believe that explicitly defined channel identifiers are much better than leaky abstractions. A client application should not care about my Ruby code.

We are going to eliminate this problem (as well as many others) by providing our custom framework for channels.
Nevertheless, we plan to have an API that is close just enough to Action Cable, so that, in most cases, a migration would be as simple as changing ActionCable::Channel::Base to AnyCable::Channel::Base in application_cable/channel.rb.

In other words, AnyCable Ruby API will become a superset of the currently supported Action Cable API.

The framework will be cable-agnostic: it will be only responsible for business-logic and will have no knowledge of a particular transport or server implementation. Thus, you will be able to use it not only with AnyCable but, for example, with Falcon or Iodine.

Some popular real-time features will be included out of the box, or via plugins: we are talking things like presence tracking and “channel-less” subscriptions.

And we’re not going to stop here.

We have a new shiny protocol and we are no longer Rails-dependent—maybe, it’s time to think about moving beyond Ruby? What about AnyCable for Python or PHP?

Thinking of AnyCable for different languages led to another mindblowing idea—AnyCable as WebSocket Gateway! By adding a routing mechanism to AnyCable-Go, we can serve different channels via different backends. Your client shouldn’t know about your microservice architecture details, one connection to consume them all! Just like Apollo Federation but for WebSockets.

OK, that went too far. I spent four years just on an Action Cable add-on. How long could it take make all the dreams above come true? Ten years or so? Remember, this is an open source project, and I am currently paid for doing “boring” commercial work…

AnyCable meets GitHub Sponsors

That’s why we decided to give GitHub Sponsors program a try and launched our sponsorship program that will help me and other core contributors to keep dedicating valuable time to AnyCable outside of billable hours. Let’s make a better real-time future together ❤️!

Make AnyCable truly yours

AnyCable is also easily extendable and configurable: if you need it fine-tuned for the specific needs of your startup—we offer custom solutions and commercial support. Be sure to check out the AnyCable’s website and always feel free to drop us a line if you need to discuss your production needs!

AnyCable brings turbo speed and improved reliability to the easy-to-use Action Cable API. That’s why we originally called it ”Action Cable on steroids”. If your Ruby or Rails application relies on real-time features, using AnyCable is the simplest way to reduce infrastructure costs while offering your users a better real-time experience. AnyCable also provides a lot of production-required features out of the box: analytics, Prometheus integration, disconnect-less deployments, non-Rails applications support, and more.

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.

In the same orbit

How can we help you?

Martians at a glance
17
years in business

We transform growth-stage startups into unicorns, build developer tools, and create open source products.

If you prefer email, write to us at surrender@evilmartians.com