Simple Realtime Synchronization with Nitrogen's #sync_panel Element

2014-02-27

Overview

Version 2.3 of the Nitrogen Web Framework will feature a new element called #sync_panel{} (which is currently already in master on Github) for simplifying the realtime synchronization of contents of a Nitrogen #panel (really just an HTML div).

The Video

I've made a video demonstrating adding this to actual production Nitrogen code. Watch it on YouTube

The Problem

In the course of expanding BracketPal's tournament system, it became increasingly obvious that the current way of drawing the tournament page is insufficient. The current implementation was developed under the assumption of a single administrative/data entry computer, and as BracketPal begins handling larger and larger tournaments, the need for multiple administrative computers (and even "lookup" computers for players and parents to look up tournament progress), and all the contents need to be synchronized without forcing tournament directors, players, and parents to refresh the page.

Up until now, running multiple computers for administrator purposes has just required occasionally telling admins to refresh the page to see the latest info - something that given the use of the Nitrogen Web Framework and Erlang sounds completely archaic.

Indeed, accomplishing the desired realtime synchronization with Nitrogen isn't exactly a terribly difficult task, as the asynchronous tools baked into Nitrogen are slick. But because of the many pages involved in the tournament system, this would require adding wf:comet calls with receive loops and wf:send calls to each page. Again, not the end of the world, but I was convinced there was a better way to do this.

The Solution

The goal was to add this automatic multi-client realtime synchronization with minimal changes to the current pages by abstracting away the wf:comet and wf:send_global calls and any receive loops. The solution is manifested in the #sync_panel element.

Using the #sync_panel element is simple:

#sync_panel{
    pool=my_pool,
    triggers=[update_panel],
    body_fun=fun my_body_function/0
}

my_body_function() will hit the database and return generated Nitrogen elements as the body of the element.

The real-time portion of this is that any time any connected client calls (through a postback or call in a comet loop) element_sync_panel:refresh(my_pool, update_panel), the panel's content will be updated for every connected player.

So we're talking about realtime content synchronization across all connected clients by adding only a few lines of code:

  • Wrap your content in #sync_panel{} element.
  • Call element_sync_panel:refresh(Pool, Trigger) any time something changes related to the content in the #sync_panel

This provides a massive productivity boost: having realtime updates sent to the browser without having to write any code to deal with the message passing or the comet loops at all - it just updates when you tell it to update everything.

Potential Problems

Admittedly, there are some potential problems you might face with this simplification:

Many connected clients

If there are too many connected clients, then all clients redrawing requests at the same time might introduce a bit of slowdown. How many is "too many" will come down to your architecture, the complexity of the page being redrawn, how many database queries are executed, and the speed or cacheability of those queries.

Rapid updates

If the updates are happening "too fast" (for some definition of "too fast"), then this is clearly not the ideal tool for this particular situation. In that case, you'll definitely want to hand-roll your comet functionality (which isn't all that hard). I would argue that this would be valuable for something where things are changing every now and then - maybe at least once a minute. Imagine an small business office using a CRM built with Nitrogen that shows the status of other employees - are they on the phone, if so, with which client, what is your next appointment, etc.. That kind of information won't be something you're looking at at up-to-the-second like you might a stock ticker - that's the kind of stuff that gets updated periodically throughout the day - and that's the kind of situation where this functionality would shine.

Code upgrades

Handling Code upgrades is no problem as long as you're aware of Erlang's requirement for upgrading running processes. Use fun ?MODULE:some_function/0 if you want the function to be able to upgrade on the fly. If you use an anonymous function (fun() -> do_something end) that version of the body function will be locked to that page until the user refreshes the page. Similarly with fun do_something/0 (note the lack of ?MODULE), you likely cause the comet process to crash for that user after too many hot-code upgrades (again solved by refreshing the page). The recommendation is to just use ?MODULE:do_something/0 unless you have a good reason not to.

The Result

After implementing the #sync_panel{} element and adding it to mainline Nitrogen, it took me a paltry hour to add the realtime updating to the BracketPal tournament module, which consisted of adding #sync_panel to five pages and also updating the refresh() functions on each page to call the element_sync_panel:refresh(Pool, Trigger) function, with each page's refresh function correctly updating all others. It required changes to about 30 total lines of code, an average of about 6 lines per page module.

The verdict: easy-mode realtime updating.

Conclusion

I hope you find the #sync_panel{} element as useful as I have. I thought about deploying it as a Nitrogen Plugin, but figured this is core enough to justify being included into mainline Nitrogen. Given the uses I have for Nitrogen, I see this coming in handy quite frequently.