Updates to the MongooseIM Inbox in version 5.1
- Nelson Vides
- 14th Jul 2022
- 10 min of reading time
This article describes new improvements to the Inbox feature of MongooseIM – an open-source XMPP server. We added “boxes” so you can keep your inbox organized as well as a lot of performance improvements in the new 5.1 release
When a messaging client starts, it typically presents the user with:
MongooseIM as a messaging server uses XMPP as its underlying protocol, and to gather the data needed above, traditional XMPP makes it possible to build a summary by combining knowledge of the users’ roster (friend list), the archive, and possibly private storage for any extensions, but this means far too many round-trips between the server and the client to build the initial UI, a number of round-trips that worsens the more out-of-date the client is, and which also imposes a load on the server to answer them all.
For this, MongooseIM introduced an extension called inbox, back in 3.0.0, that simply keeps track of the active conversations, the snippet with the last message, and a count of unread messages for each conversation which is, intuitively, restarted when a user sends a new message or a chat marker pointing to the latest message in the chat.
In 3.5.0, we introduced a mechanism to manually set the unread count of a conversation to zero, which is a common operation for a user, when reading the conversation is as undesired as continuing to see it pop up with unread messages, but on 4.2.0 we took this really seriously: we introduced a mechanism to not only set a conversation as read, but also to set it as unread again, to mark a conversation as muted until a certain date, and to unmute it as well.
Until now we had not added a function to set as unread because it would have been easy to introduce a problematic API and it was deemed out-of-scope for the effort at that time: if we allowed a client to set any count they desire, we might allow broken client software to break the experience of a good client software. the same user might use separately, for example by setting a number much higher than the actual number of messages in the conversation. And there’s also the chance of setting a number at the same time more messages arrive, that would therefore invalidate the count: should the number requested be considered an absolute number, or an increment? In the end, we settled for an atomic “set-as-one-if-it-was-zero”, i.e., if the conversation already had unread messages, the request does nothing. If the conversation was fully read, the request only sets it as strictly one. This way, a user cannot possibly move away from the actual number of unread messages, set incorrect numbers, or accidentally set a number at the same time more messages arrive.
But this is not all, what if the user doesn’t want to be notified about a certain conversation? For example, they might say “mute this chat for an hour”, and all their devices should sync on the fact that the chat shouldn’t be making the phone buzz, and maybe the logo should be displayed differently. 4.2.0 introduces a possibility for that as well! A client might provide the number of seconds they want a conversation muted for, and MongooseIM will tell all his other devices, persist this information on the inbox summary, and propagate this information to be picked up by other services, like possibly the push-notification one.
One more functionality (apart from a myriad of performance optimizations!) added in the latest MongooseIM 5.1, the concept of boxes. Like in an email service, a user can classify chats in boxes like “favourites”, “travel”, “business”, or “home”. You can set a box for a chat and then when reconnecting focus on that box specifically in MongooseIM, and build a nice UI accordingly, including a “pinned” box of conversations you always want at the top!
To use this functionality, it is as easy as enabling the inbox module in your MongooseIM configuration, all the defaults are set for you. Out-of-the-box the inbox will have the “inbox”, “archive” and “bin” boxes enabled, and the trash bin will be periodically cleaned every hour for entries older than 30 days. Group chats are enabled, chats are considered read when a displayed marker is sent, and the synchronous backend is chosen. To go further away from the details, try enabling the asynchronous backend, see below for the scalability details.
Another important feature is the creation of an asynchronous backend for inbox. Until now, inbox operations were synchronous in the context of a message processing pipeline: that means that the message processing would be blocked by the database operation, until the database successfully returned. This becomes especially bad for big group-chats where a message updates the inbox of each room participant. This means that MongooseIM’s message processing throughput would be as scalable as the database is, which is bad news.
In order to untie MongooseIM’s scalability from the database performance, we needed to first make the inbox processing asynchronous, that is, the message processing pipeline will only “submit a task” to update the appropriate inbox. But this still leaves the fact that the inbox generates too many database operations. So we tried to do less of them: the trick was that inbox is (mostly) an update-only operation: Alice receives a message from Bob, we update Alice’s inbox’s snippet with the new message, and an incremented count of one, Alice receives a new message, we update the snippet again with an incremented count of another one. We could have just applied the second update with an incremented count of two instead!
% If we don't have any request pending, it means that it is the first task submitted,
% so aggregation is not needed.
handle_task(_, Value, #state{async_request = no_request_pending} = State) ->
State#state{async_request = make_async_request(Value, State)};
handle_task(Key, NewValue, #state{aggregate_callback = Aggregator,
flush_elems = Acc,
flush_queue = Queue,
flush_extra = Extra} = State) ->
case Acc of
#{Key := OldValue} ->
case Aggregator(OldValue, NewValue, Extra) of
{ok, FinalValue} ->
State#state{flush_elems = Acc#{Key := FinalValue}};
{error, Reason} ->
?LOG_ERROR(log_fields(State, #{what => aggregation_failed, reason => Reason})),
State
end;
_ ->
% The queue is used to ensure the order in which elements are flushed,
% so that first requests are first flushed.
State#state{flush_elems = Acc#{Key => NewValue},
flush_queue = queue:in(Key, Queue)}
end.
The new inbox asynchronous backend does just this: it submits updates to the database immediately, but instead of waiting for the database’s response, it aggregates all upcoming requests in the meantime. This means that on an unloaded server inbox is still pretty much immediate, and it will saturate the database throughput as fast as messages arrive: but once the database is saturated, it will not overload it!
aggregate({set_inbox_incr_unread, _, _, _, _, Incrs2, _},
{set_inbox_incr_unread, Entry, Content, MsgId, Timestamp, Incrs1, Box}) ->
{set_inbox_incr_unread, Entry, Content, MsgId, Timestamp, Incrs1 + Incrs2, Box};
To enable this backend, all you need to do is to select “rdbms_async” as the inbox backend in your mongooseim.toml configuration file and you’re good to go!
Note some comparisons:
rdbms 12k users, rooms of 5 user | rdbms_async 30k users, rooms of 10 users | No inbox 35k users, rooms of 10 users | |
Delivery time, .99 | >40s | 50ms | 50ms |
Stanzas sent per minute | 140k | 180k | 180k |
CPU usage | 80% | 99% | 99% |
Memory usage | 800MB | 1200MB | 950MB |
Note how for the traditional backend the CPU is not even saturated and messages are terribly stalled, but for the new asynchronous backend, we saturate the CPU with more than twice the number of users, and get pretty fast delivery times. Even when compared to disabling inbox altogether, the regular backend penalty shoots above 70% of the throughput, while the new backend costs only 12% (35k users lowers to 30k).
Explore the NonStop architecture's influence on Elixir, Gleam, and Erlang developers. Learn about modularity, fault containment, and process-pairs design for resilient software systems.
Erlang’s concurrency model, built for scalability, now competes with C and Rust in performance, powering messaging systems and large-scale platforms.
Embark on a journey into Elixir with our series "Elixir, 7 Steps to Start Your Journey" by Lorena Mireles.