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.
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:
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.
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.
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
$requestgets 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.
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.
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!