loading...
Cover image for My Struggle With Rails ActionCable

My Struggle With Rails ActionCable

shakycode profile image shakycode ・5 min read

Let me give you a bit of background on this article. I'm building a multi-tenant SaaS platform that connects Physicians with Patients. Included in this is the ability to have realtime chat sessions between two parties. This is all built on Ruby on Rails, my framework of choice using Ruby which is my primary language. In Rails 5 ActionCable was introduced which facilitates dead-simple websocket functionality replacing the need for external dependencies such as FAYE or 3rd-party services like pusher.io. This is baked in and a pleasure to work with. Or so I thought.

You see multi-tenancy is a difficult problem to solve and ActionCable likes to transmit its websockets on a specific request origin. The request origin could be for example: https://acme.engagemd.co so all socket requests connect and stream from that request origin. If you aren't sure what a request origin is in Rails, think of a the request as where the HTTP request is coming from. In my case this is a multi-tenant app so the requests would come from a plethora of subdomain names. Actioncable looks for specific request origins based off of a whitelist that you specify in the production.rb environment file. Now you can get clever and use some Regex matching to allow subdomains from a parent domain i.e. acme.engagemd.co, doctor.engagemd.co, etc. But this is not enough.

Problem #1

In my new platform I am using the Apartment gem to give each tenant their own database schema and have written the platform so that the entire app will switch Tenants based on the request origin. When I first started writing a simple chat feature into the app I was presented with the issue of how to get ActionCable to connect based on the request origin. After some help from a friend and doing my own research I was able to get the connection.rb file which drives ActionCable to read the request origin and switch the tenant in realtime. This got rid of the rejected connections and the streams started flowing properly. This is my connection.rb file:

connection.rb

As you can see when we go to connect we set the tenant to the request subdomain then call switch on the second line of the connect method to switch the tenant and following that verifying that the user is part of the tenant and allowed to receive websocket traffic. That part was solved relatively easily.

Problem #2 aka The Show Stopper

In the channel file which is responsible for sending data to the client, there's really no way to access the request origin and switch the tenant.

channel.rb

As you can see in both the subscribed and send_message methods in the channel file it's required that we switch tenants. In the subscribed method I hardcoded a tenant just to see if it would switch and it did. In the send_message method (and the connect method) I really need to be able to access the request's subdomain so that I can switch tenants and livestream the chat.

Now here is the showstopper. After reading the ActionCable docs and Googling for what must have been 4 hours yesterday there's really no way to get the request into the channel file. That being said this is a complete failure and I'm not able to use ActionCable for multi-tenant realtime chat since the request is unavailable in the channel file.

So I went on a witchhunt trying to figure this god damned thing out. My first thought was to set a global like $request in the application controller and allowing the channel file to access that. This works in theory but has multiple problems.

  • Globals are a security risk and are prone to mutex conditions

  • When you have more than one tenant and user the global variable $request gets overwritten and ActionCable will not transmit after the tenant is switched. Or worse yet the global variable gets lost entirely and the tenant will attempt to switch but having no subdomain which to switch on fails and messages are not created.

I've opened up an issue on Rails core to see if anyone can help as the documentation is lacking and I'm pretty sure I can't be the only one with this specific use case. So I'm hoping for some feedback on this soon. In the meantime I'm tearing apart ActionCable in rails to see if there's a way to pass the request to the channel file. If I can figure this out I will do a PR against master and hope for inclusion in the next release of Rails, but I'm not holding my breath as the Rails community is an opinionated bunch and multi-tenancy is not what everyone is doing.

The Case for Polling

Considering the fact that I'm falling face first on my desk dealing with this ActionCable issue I've decided that a good alternative is do Ajax polling in the chatrooms where an ajax call is made every 2–3s and polls for new content and refreshes the div/partial. Surprisingly enough this is how Basecamp does it in their campfire chat instead of using ActionCable. I watched a good talk with DHH about Rails performance and he went into a bit how they are using Ajax polling for their campfire chat instead of livestreaming via websockets. Their thought was not realtime chat, but “realtime enough”. 99% of end users will not notice a 2–3s delay in chat which is plenty of time for the system to poll and refresh the partial showing new messages.

This is not what I wanted to build nor the way I wanted to build it but at this point I see no other choice but to move forward with Ajax polling and eventually ActionCable will introduce the request origin being passed from connection.rb to the channel file. Or I'll write a PR that introduces this behavior and hope that it's accepted.

Summary

Writing software is hard. Just when you think you have it figured out an 800lb gorilla hops on your back and takes you for a pony ride. I know that I'm making good progress on my platform and will continue to do so but all of these roadblocks can be discouraging. I'm doing my best to take these challenges as opportunities to grow and think outside of the box, but it's a bummer none the less.

Are you working with ActionCable and multitenancy? If so, let's chat and maybe we can come up with a solution that works for all!

Cheers!
Shakycode ❤

Twitter: shakycode
Email: shakycode@gmail.com

Posted on by:

shakycode profile

shakycode

@shakycode

Polyglot programmer, lover of coffee, all things code <3

Discussion

markdown guide
 

Thank's for you content!!!

And , in the channel.rb , i use follwing code:

class UpdateNoticeChannel < ApplicationCable::Channel
  def subscribed
    tenant = self.connection.env['HTTP_HOST'].split('.').first
    Apartment::Tenant.switch! tenant

    stream_from "update_notice:tenant[#{tenant}]:user[#{params[:user_id]}]"
  end
end

 

This is a very nice one and gives in-depth information. I am really happy with the quality and presentation of the article. I’d really like to appreciate the efforts you get with writing this post. Thanks for sharing.
HyperMesh classes in pune

 

Can a User in your multi-tenant app use more than one subdomain? If the answer is no then do you have a relationship between User & Account or User & Subdomain, if yes then you can switch the tenant context based to the subdomain assigned to the user. No ?

How about using ActsAsTenant multi-tenancy gem which stores tenant context in the current thread and using ActsAsTenant.current_tenant = request.subdomain in Connection you should be able to access ActsAsTenant.current_tenant is Connection.

May be I am missing something here ... so let me know if I am wrong.

 

That's exactly what I thought. It should definitely work.

See github.com/rails/rails/issues/27875

 

Total newb so I may not fully understand the problem, but this might be something the HyperReact tools like HyperMesh could help with. Especially if you end up using polling anyway.

 

My struggle with ActionCable and Apartment Gem it's simply doesn't works.

/ [ActionCable] [User user1@example.com] [Tenant user101] Registered connection (xxxxxxxxxxxxxxxxxx)
prints every 2-3 seconds in console, but ActionCable.server.broadcast("test", "ActionCable broadcast!") it does not do anything.

 

Great piece and an interesting problem. At work we started off using action cable for a live log so you could see your requests going through our system in your browser. We were already using redis as a comms layer between the Ruby on Rails app and the erlang simulator so it was easy enough to setup the channels and start listening. We even managed to dynamically set the channel names albeit we where setting the users api key and not changing domains.

Ultimately though we ended up using a really small Phoenix application just to manage the channels (request counters and live log). We had to keep redis for rails/erlang comms and listen on that but overall the Phoenix version is much more performant than the rails version was. Hope you get the issue fixed though actioncable has a lot of promise and serves great for the majority of applications.

 

Thanks for chiming in, James. Hey... we're both James!

I've also looked into Elixir/Phoenix for solving this problem and I may move to that framework/language for this particular platform.

ActionCable is amazing but I've yet to see a multi-tenant platform integrate ActionCable yet. Surely I can't be the only use case on the Internet? Granted AC is brand new with Rails 5 so I'm sure as time progresses the kinks will be worked out.

My goal over the next week is to tear AC apart and figure out how to get the request passed from the connection to the channel. I'm hoping to do a PR against Rails core soon that will introduce the request into the channel, but the Rails community is picky about PRs and use cases. So we'll see :)