Any Cables Monthly #22: UI kit preview
July, 2024: UI Kit, preparing for connection avalanches, and the question of per-room vs per-user subscriptions
The time of vacations, berry-picking and sun-burning, music and sports festivals (aka summer) is coming to an end. Soon, we’ll be back to full power mode and start working hard to meet this year’s goals. At AnyCable, we have two goals for 2024: 1) release the Presence feature, and 2) publish our new documentation website. Both are in progress, so stay tuned! Now, let’s take a look at what happened in July.
Posts
Connection avalanche safety tips and prepping for real-time applications
Connection avalanche is a kind of thundering herd situation specific to real-time applications. This post gives an overview of possible prevention and mitigation measures. Even if you don’t expect high loads any time soon, it’s better to be aware of potential issues that may occur as you grow.
News
We’ve been dreaming of providing some kind of real-time kit for AnyCable users for a while, and finally, the work has started: we’re happy to (pre-)announce our ready-made real-time components. The UI/UX part is mostly solved, and we’re ready to start working on the actual implementation. What do you think? Share your thoughts/suggestions on this initiative–respond to this email, or leave a comment on substack.
Events
Developer Tooling For The Modern Hotwire & Rails Era
Marco Roth travels across the world and shares his vision of the future of Hotwired Rails. Check the slides (videos are coming later) and see how much effort Marco puts into improving developer experience (DX) for engineers working on Hotwire applications—this is what the future development looks like, with DX as the top priority for engineers.
Releases
We made the source code of our Stackblitz-hosted AnyCable Next.js example available on GitHub. Feel free to hack around with it on your machine!
This tiny release brings one new useful feature: the ability to react to history-related events (e.g., `history_not_found`), so you can implement application-specific fallbacks. Check out this PR to learn more.
Frame of curiosity: per-room vs per-user subscriptions
Recently, I had a discussion with one of the AnyCable Pro clients on their real-time application design. They build a chat-like application with users and conversations, and they started to question the design of the underlying real-time functionality (receiving conversation updates): “Are we modeling the real-time communication correctly?”, “Will it scale?”. I believe engineers must question themselves and re-visit their prior decisions regularly—that’s one of the keys to staying afloat. But we’re not here to talk software engineering philosophy stuff; let’s go back to the technical problem under consideration.
So, let’s assume we have users and rooms in our chatty application. Rooms can be public with many members or direct messaging rooms with just two communication ends. Here is, for example, our AnyCasts demo:
A typical approach in modeling real-time communication in chat rooms is to have a RoomChannel (we’re going to use Rails Action Cable as an example):
class RoomChannel < ApplicationCable::Channel
def subscribed
room = current_user.rooms.find_by(params[:room_id])
reject unless room
stream_from room
end
end
And then, when we want to deliver a new message, we broadcast it to the room:
class Room < ApplicationRecord
after_create_commit do
RoomChannel.broadcast_to room, serialize(self)
end
end
This is an example of per-room pub/sub model: for each chat room, we have a dedicated stream, and users subscribe to the relevant room streams to receive live updates.
This is a very efficient model in terms of broadcasting since it fully leverages pub/sub capabilities: we don’t care about actual subscribers when we deliver updates, all we know is the stream name. However, from the client-to-server communication point of view, it has some drawbacks.
Imagine we have a Chats screen where we list all the user’s conversations. In order to show new messages on the list, we should subscribe to all conversation streams! And there could be dozens and even hundreds of them. Surely, the WebSocket connection is still the one, but performing “subscribe” requests is not free (especially, in the example above when we reach out to the database to verify access).
Even if we don’t show updates on the Chats screen, browsing conversations may feel laggy, because we need to subscribe every time we open a chat screen (and unsubscribe afterward).
So, after facing such challenges, many decide to look at the kinda opposite strategy—per-user streams.
When using per-user streams, you only have a single stream per user but perform as many broadcasts as the room members number for every message created:
class UserChannel < ApplicationCable::Channel
def subscribed = stream_from current_user
end
class Room < ApplicationRecord
after_create_commit do
room.members.each do
UserChannel.broadcast_to(_1, serialize(self))
end
end
end
This approach drastically simplifies the client side of the communication: just a single data stream to consume updates from. You subscribe once, and it’s always on, you just need to route messages to the corresponding stores.
Well, the server could be not so happy with this change: now it has to perform O(N) broadcasts every time a message is created/updated, where N is the number of room members. In other words, we ditched the pub/sub idea completely. (With AnyCable’s batch broadcasts, we can reduce the overhead, but still…)
As in many software problems, a combination of multiple techniques works better than each one independently. Instead of choosing either per-room or per-user approach, think of using both. For example, for DM conversations, using per-user approach makes sense: the number of DM dialogues can be huge but the number of members in each is very limited (so, the broadcasting overhead is negligible). For crowded public channels, it’s better to stick with pub/sub, i.e., per-room subscriptions.
Thank you for reading. See you next time!