00:00:00.900
Hello everyone! Thank you for joining me today.
00:00:13.519
I've been a bit late. I tried to practice my talk, but I failed, so this is a bit of an improvisation.
00:00:22.260
Today we’re going to talk about real-time applications, particularly in relation to Rails.
00:00:27.480
I'd like to start by referencing the new homepage of Ruby on Rails because I wanted to find a good starting point for this talk.
00:00:39.960
The homepage has an interesting and catchy phrase: "Build amazing web applications. Rails has everything you need." While this is compelling, it doesn't answer the question of what makes an application amazing.
00:00:51.840
If you ask people on the street, many will say that amazing web applications should have a sleek UI or beautiful designs. While that's true to an extent, especially in this room, many would argue that they shouldn’t have any JavaScript.
00:01:04.140
In the modern web, however, one of the properties of amazing web applications is that they are reactive. You might wonder, what does reactive mean? I would define it by providing an anti-example: a non-reactive interface is one that forces users to refresh the page to see updates. In 2020, that is simply unacceptable.
00:01:24.920
A reactive application, on the other hand, sends updates to users immediately, without requiring any action on their part to receive information about changes happening in the system. We typically achieve this through some form of streaming functionality, often web sockets. Fortunately, Rails has supported this since version 5.0.
00:01:57.420
Real-timeification in Rails began with Action Cable approximately seven years ago. However, it wasn't highly adopted in the early days.
00:02:05.340
The real boost for real-time applications came about a year ago with the advent of Hotwire. Between these two significant events, we also saw the emergence of the Stimulus framework and its ecosystem, which enables building reactive Rails applications without JavaScript.
00:02:25.680
In this talk, we’ll primarily focus on Action Cable and touch a bit on Hotwire. About a year ago, I gave an online talk about new ways of building modern Rails applications. One key feature of these applications is the use of HTML through web sockets. I tried to convince you all that using Turbo with Rails was an amazing approach. You could simply include it in your Gemfile and have everything you need for free.
00:03:05.340
I have to admit that saying it would be free was misleading. You still need to think about how to implement this new way of doing things related to real-time applications.
00:03:38.519
My name is Vladimir Dementyev, and I work at Evil Martians. I specialize in Ruby and Rails as a principal backend engineer. I'm currently based in New York. If you want to talk about Ruby, Rails, or life on Mars, feel free to reach out. I've contributed to Rails, particularly around Action Cable, which I’ll discuss shortly.
00:04:02.879
It's worth mentioning that I also started a screencast series around Christmas where I explore the capabilities of Rails 7, Hotwire, and Action Cable. I’ll be using a demo application from these screencasts for demonstration purposes today, so you can learn more about it on YouTube.
00:04:58.860
Now, let’s move on to the main topic. What has changed in the shift to real-time applications? We used to write request-response applications—synchronous interactions where a client requests something from the server, and the server returns a payload.
00:05:10.979
In contrast, web sockets allow for bi-directional and continuous data flow. You cannot predict when the server will send you data; this opens up a lot of new delivery guarantees.
00:05:32.940
Although it remains HTTP under the surface, it's important to note that web sockets are quite different. Rails effectively hides these differences behind familiar APIs, providing channels for web sockets and controllers for request-response.
00:05:55.080
With Hotwire, you don’t even need to write client-side code; you're simply dealing with HTML while the magic happens behind the scenes. However, this can lead to several pitfalls, which we’ll explore.
00:06:24.660
Let's start with the first pitfall: personalization. In a request-response model, a client requests something, and the server generates a unique payload for that client.
00:06:58.320
In a publish-subscribe pattern commonly used in web sockets, the same payload is broadcast to multiple clients. This means everyone receives the same data, leading to questions around how to personalize content for individual users.
00:07:20.099
For example, if you want to show messages for the current user, how do you broadcast that to everyone but this one user? The problem lies in the fact that we don’t know about users in the same way we do when sending unique requests.
00:08:02.400
Typically, our messaging problem requires us to show messages for a particular user with a unique user interface tailored for them.
00:08:36.000
I've worked with junior Rails developers extensively, and I've seen common techniques they use to tackle these issues. Often the first attempt is to send a create message with an Ajax request and return data uniquely for the requesting user while allowing everyone else to receive updates via Action Cable.
00:09:15.060
The problem with this approach is that a user may be connected across multiple tabs or devices. This inconsistency can lead to issues. It's important to remember that the 'current user' isn’t necessarily the same as the current connected client.
00:09:50.079
Another problem is that Action Cable updates and Ajax responses can become unsynchronized, leading to race conditions and strange UI artifacts.
00:10:16.560
A common approach to mitigate this is to send data to all connected clients, which breaks the idea of a centralized broadcast system.
00:10:40.260
If we have many broadcast messages sent repeatedly to clients that aren’t connected, it creates unnecessary overhead. This issue is compounded when the ratio of connected users to total subscribers is low.
00:11:12.690
For situations where the number of subscribers is significantly greater than the number of connected users, sending many broadcasts creates a lot of redundant work.
00:11:54.030
When considering the Channel per user approach, we can discuss another concept. Channel per group, where you send data to groups based on roles or locales, can provide a better trade-off.
00:12:38.160
Using Hotwire, one can prepare streams based on locales and perform broadcasts to those streams quite easily—just ten lines of code can make your application support multiple languages.
00:13:12.720
Moving on to another pitfall: sending updates through web sockets often leads clients to perform Ajax requests for updated versions of this data.
00:13:30.360
In a Hotwire context, this could involve sending Turbo frames without content but with a source URL pointing to the data's location. Imagine if that was sent to thousands of clients in a chat application—potentially overwhelming the server.
00:14:17.640
Before we discuss potential solutions, I attempted a unified data context-free broadcast that needed some client-side enhancements.
00:14:57.060
In my blog post from a year ago, I described this more thoroughly with snippets of code showcasing client-side enhancements for managing message visibility.
00:15:19.500
Some existing tools readily adopt this concept. For instance, Basecamp employs data visibility attributes to hide delete buttons for messages that don’t belong to the current user.
00:15:44.580
While this approach solves the problem of scaling, it does require some amount of client-side logic.
00:15:58.260
I think there has to be a more cohesive way to build personalized updates without having to consider the difficulties of existing solutions.
00:16:16.680
When we reflect on Elixir and Phoenix, they manage to bypass this problem by not broadcasting data directly to clients. Instead, updates go through a server-side component that represents the current state.
00:16:41.880
However, with Action Cable we also have stream callbacks to help manage sending messages selectively per client.
00:17:03.839
Next we need to consider consistency or delivery guarantees.
00:17:34.920
Action Cable has a simple model where data is transmitted to connected clients without ensuring delivery. This means you're on a 'fire and forget' protocol, which can lead to issues when network conditions are poor.
00:18:15.240
For example, if you're underground with no mobile connection and try to chat, when you reconnect, you may not receive the messages others have sent. This can result in a lack of feedback if a user is waiting for responses.
00:18:54.780
After reconnecting, you'll want to restore the state, ideally by syncing back to the database for the missing messages.
00:19:35.400
Here’s where a series of potential fixes come into play. First, we need to have a reliable source of truth or data that we can restore from.
00:20:23.160
When reconnecting, you should always be able to request this data, even if you have a stable connection. This can become a complex process with many requests involved during data transmission.
00:21:07.440
We need to address how messages are sent before we finalize the reconnection process. Messages could be sent at different points, possibly creating inconsistencies.
00:21:50.040
We could implement a state machine behavior for our subscriptions to track messages sent during a disconnect.
00:22:32.520
To address the issue of delivery guarantees, we can also implement a client-side solution that keeps track of which messages have been viewed, simply dropping duplicates.
00:23:20.310
While this can solve various issues, the overhead of additional code makes it cumbersome, particularly with large applications.
00:24:10.740
Moving on, we should improve the consistency of transactions within Action Cable.
00:24:56.760
Consistent updates can be achieved by extending the Action Cable protocol to include information that identifies the position of a message.
00:25:32.400
This change would allow you to implement history functionality seamlessly without worrying about it from the perspective of your Ruby application.
00:26:08.760
It’s essential to build a reliable process for managing presence tracking as well, determining who is online and when they join or leave.
00:26:58.380
Using a dedicated online channel can often lead to more complications as people may connect from multiple devices.
00:27:38.580
A more scalable solution would involve using a counter-based system to track active users.
00:28:15.240
This can be supported by implementing heartbeat mechanisms so clients regularly signal their activity to the server.
00:28:55.020
However, we need to explore built-in support for presence tracking within Action Cable, making it easier to manage this feature without additional work.
00:29:22.680
It’s crucial to understand that the responsibility for maintaining consistency should lie well within the websocket server, not the client model.
00:30:09.480
As we wrap up, I want to highlight that transport issues still persist in some scenarios, particularly with applications that restrict websocket usage.
00:30:45.540
If you're interested in ensuring robust real-time capabilities, I recommend exploring existing solutions, such as Action Cable, while remaining aware of its limitations.
00:31:23.400
Real-time application development does require a mindset shift. Understanding the intricacies enables you to build amazing web applications.
00:32:00.360
To finalize, if you want to explore some additional opportunities and gain access to advanced features, I'd suggest checking out the Evil Martians booth.
00:32:38.300
We have a special offer for conference attendees regarding our pro version and are happy to answer any questions.
00:33:08.760
Thank you for your time today! Let's make the real-time web in Rails better together.
00:33:45.890
And just a heads up—I have some stickers for you all!