This is the third in a series of posts discussing the technical decisions that went into creating Ringing Room, the first web platform for distributed English change-ringing. The first post gives a little bit of background on change-ringing for non-ringers to be able to follow the series. This post looks a bit more at how Ringing Room models rooms (“towers”) on the server.
This post is going to go into more detail about how Ringing Room keeps track of “towers” — the virtual rooms in which groups of ringers gather. I’ll also talk a little bit about how we load-balance towers across servers.
Towers from the user’s perspective
Before getting into the back-end detail, let’s talk a little bit about how towers function from the user’s perspective.
Ringing Room towers are all identified by a unique 9-digit ID number.1 The user can reach a particular tower in one of 3 ways:
- Entering the 9-digit number on the front page of Ringing Room.
- Going to the unique link assigned to the tower (
- Going to the My Towers page and selecting it from the list.
Once in a tower, the user can interact with any other ringers who are currently in that tower, either through text-chat or just by ringing bells. Towers have names that are displayed in various places (in the top right when you’re in the tower, or in the My Towers list), but these names are purely cosmetic and not necessarily unique — in particular, you can’t reach a tower if all you know is its name.
ID numbers were probably a mistake
The unique tower ID numbers are actually the biggest thing about Ringing Room that I would change if I were building it from scratch today. When we originally conceived Ringing Room, we had no idea that it would someday be the primary way ringing bands around the world would hold their weekly online practices; we imagined instead that it would be a platform that small groups would use for specially-organized ringing (i.e. quarters & peals — ringing performances rather than practices). Accordingly, we imagined that most “towers” would be one-off: Ringers would create them as needed for a specific event, send the link around, and then abandon that tower when the ringing was done. In this situation, the unique identifier for the tower does not need to be memorable — individuals won’t be using it more than once or twice anyway — and the “tower name” can be entirely cosmetic. In fact, having non-unique tower names is actually a virtue: You can’t crash a piece of ringing you weren’t invited to just by typing e.g. “Quarter Peal for Remembrance Day” in the box and seeing where you wind up.
We were wrong about how Ringing Room would be used: the most frequent use-case for Ringing Room is far-and-away the regular weekly practice. The tower captain (or whoever is organizing the practice) creates a tower, and then the band gathers at that same tower week on week. Many bands like to have a tower named after their actual, real-life tower (e.g. “St. Michael’s”); others have a set of towers with slightly different names so that a practice can split into breakout rooms (e.g. here in Boston we have “bcr1”, “bcr2”, etc.). For this use-case, having unique ID numbers but non-unique names is exactly wrong: Users intuitively want to get to their local tower by typing “St. Michael’s”, and are stymied when this doesn’t work; instead, they have to remember (or, more likely, write down) a particular number just to be able to attend their local practice.
This problem is made worse by another design decision I made early on: The text input on the front page serves a dual purpose. If you enter a valid ID, you join that tower, but if you enter other text you create a new tower with that name. This means that our hypothetical ringer trying to get to the practice at “St. Michael’s” might wind up in a tower that looks like St. Michael’s, but is in fact a different tower. In the early days, we wound up with so many duplicate towers due to this problem, and I know many tower captains had to put quite a lot of effort into educating their ringers (who, as a group, are often older and not especially web-savvy) on how to get to the actual tower for practice. With the benefit of hindsight, I think it would have been better to make tower names unique, or at least to provide some way to search for a tower by its name. At this point, though, Ringing Room users are well-accustomed to using ID numbers, and any change to this system would need to be very carefully planned out to not break any current habits.
Three views of a tower
Having looked at how towers work from the user’s perspective, let’s turn to how they’re implemented on the back-end. There are actually 3 different objects that make up what the user sees:
- The Socket.IO room associated with the tower.
- The database table which tracks stable tower information.
Towerobject which tracks transient tower state.
Socket.IO, the networking framework that Ringing Room uses, has a concept of “rooms” functioning in the familiar way: Clients can join or leave specific rooms, and the server can then broadcast its response to all clients in that room.
These rooms are how Ringing Room structures its communication with towers. When a user enters a tower, their browser sends a
join_tower message to the server with the unique ID number associated with that tower; the server then adds them to the appropriate Socket.IO room, ensuring that they’ll be notified whenever something happens in that tower going forward. The server also sends a bunch of information about the tower’s current state — more on this below.
The database table
Ringing Room uses a SQL database to keep track of information about towers and users.3 For towers, this includes the tower ID number and tower name plus some additional settings like whether “Host Mode” has been enabled and what users have host privileges.4
Aside from the database tables tracking towers and users, there’s also a user-tower association table tracking the many-to-many relationship between towers and users; this table is how we populate the My Towers page with a list of recently-viewed, created, and bookmarked towers. Each row in this table is uniquely identified by the user and tower ID numbers, and then also contains a list of booleans tracking whether it’s bookmarked, etc. We also track the date that the user most recently visited the tower so that we can sort the My Tower lists appropriately.
The Tower object
Of course, any individual tower has much more state than just what is tracked in the database! For example, the tower needs to keep track of which stroke each bell is currently on, how many bells there are in the first place, and whether those bells are set to look & sound like tower bells or like handbells. All of this data is transient, in two senses: First, it changes fairly often — the stroke of a bell changes roughly once per second when ringing. Second, ringers don’t need it to be persistent: If you set the tower to have 12 bells, then come back a week later and discover that it has only 8, bumping it up to 12 again is just one click away. As a result, we don’t want to write any of this information to the database — there’s no need to persistent the data past one session, and it’s going to change often enough that the frequent disk access could slow things down.
This is where the
Tower object comes in. When a user tries to access a tower, the server looks to see whether it already has a
Tower object with that ID; if not, it goes to the database and uses the information there to construct a new
Tower object with default settings. We garbage-collect
Tower objects occasionally, and obviously these in-memory objects don’t persist across server reboots, etc., so in practice the first person to arrive for a practice almost always triggers a database lookup; after that, all interaction with the tower goes through the in-memory object.
Once Ringing Room really took off in popularity, it quickly became clear that running everything on a single virtual server wasn’t going to work: we wanted to be able to distribute the load across a couple of servers. I’m sure that there are well-established patterns for doing this, but at the time I was quite new to the topic and improvised a system that seems to be working fairly well so far.
When a user visits a Ringing Room tower (whether directly or via somewhere on the main page), the HTML for that page is served by Flask from our primary server. Flask uses its templating engine to insert two pieces of information into the HTML: 1) the tower ID, and 2) a URL pointing to one of our load-balancing servers.
Once the client loads, it establishes a connection via Socket.IO with the load-balancing server and then sends a
join_tower message with the tower ID. The load-balancing server loads the relevant
Tower object (consulting the database on the main server if necessary) and sends all of the relevant metadata back down to the client. From that point on, the client only has any contact with the load-balancing server, unless the user navigates away to another Ringing Room page.
The load-balancing algorithm is implemented in a fairly naïve way: It chooses a server by taking the last digit of the tower ID and modding by the number of servers we currently have running (typically 3). On average, this should roughly-evenly distribute towers across servers, but of course there’s always the chance that only towers ending in 3, 6, or 9 decide to hold practice on any given night! We haven’t run into significant issues with this so far, but I would love to someday implement a system that takes into account the actual load on each server.
That’s all for this post! Next time we’ll look at how the actual user interface is implemented.
Actually, there’s a not-so-secret Easter-egg for ringers here: Ringing Room ID numbers are actually rows on 9 bells, meaning that they are permutations of the numbers 1–9. We’ve occasionally thought about relaxing this restriction to allow random 9-digit integers, but
9! = 362,880is plenty of space, and ringers generally like that the IDs are changes. Since about mid-August, thanks to contributor Ben White-Horne, we’ve actually preferentially used “musical” rows for IDs — i.e. rows with sequences of several consecutive numbers. ↩︎
The link generated for a tower is of the form
ringingroom.com/<ID>/<tower_name>, but everything after the ID number is optional and ignored by the server — it’s just there for mnemonic purposes. ↩︎
We interact with that database via the SQLAlchemy ORM, which allows us to treat rows in that table like normal Python objects — very convenient! ↩︎
Host Mode restricts certain actions in the tower to only users with particular privileges; this allows tower captains to have a bit more control over how a practice proceeds. ↩︎