Baby's First Genserver
Hi!
In this post I walk through learning about and creating my first GenServer for Galley.
While on vacation I was able to return to Galley and work on a feature that I've been wanting to do for a while - adding countdown timers to recipes. Ari's Garden originally implemented this feature in javascript using a setInterval, and because it was a static site, I couldn't really persist these timers for users[^1] .
When I started out working on Galley I knew I'd be able to leverage having a server to make this feature work properly. I didn't know anything about GenServer at the time (other than hearing the name) but I knew that somehow I'd be able to manage the state so that users could navigate between recipes, close their browser and open Galley on another device and still see the same count downs.
Making a topic friendly
GenServer was a topic I heard about frequently when people would describe the benefits of Elixir. But for me, I knew it was one of those topics that I probably wouldn't have looked into unless I had explicit need of it (or someone told me to use it). In some ways, I love this about programming - there's always new things to discover - and you don't have to know about them all the time; these topics, concepts and tools can sort of just bob up and down in the ether of your mind just outside of your focus until the time comes to explore it.
In a situation like this, most of the time I reach for a new tool and try and learn about it while building something; this method generally works for me, but can be frustrating. This time around I tried a different approach. I received Elixir In Action courtesy of my old job, and have been slowly going through it. I eventually hit the chapter on GenServer and worked through it and the accompanying exercises. By the end of it, I had a clear picture in my mind of what I needed to do to build my feature.
On top of that, having a bit of practice / muscle memory built up on how to implement a GenServer took some of the chore out of writing the feature.
Distilling Concepts
There are a few things that confused me about GenServer when I started:
- the callback system
- the separation of interface and implementation functions
- how it works with processes
- what you would use it for
I won't try and explain each of these things. I will say that writing one or two GenServers provided enough clarity for these initial stumbling blocks. At this point, I am thinking of GenServer as a small library for interacting with processes and their state; GenServer provides a unified way of doing concurrency-things rather than loosely spawning processes and trying to keep track of pids
.
I would recommend checking out chapter 5 of Elixir in Action as it walks you through building your own version of GenServer. It clarifies how state moves through GenServer (through an infinite loop function in which callbacks handle async and synchronous requests to the state) and it is a great insight into the "concurrency primatives" using just the features of the language.
Galley's GenServer Timer
Galley's use of GenServer wasn't much different from the Elixir in Action example of a KeyValueStore. Mine works like this:
- A user goes to a recipe where one step has a timer (ex: "Roast the vegetables for 45 minutes").
- The user clicks on the timer button for that step
- LiveView's handler for that click runs the GenServer module's function for creating a timer.
- The GenServer now has a timer entry in the state map.
- The GenServer runs
Process.send_after
every second to iterate over the map and decrement all the timers within it. - Whenever a timer is decremented, I use Phoenix PubSub to broadcast that a timer decremented.
- LiveViews are subscribed to the topic for the above and re-render the steps of recipe to show a decremented timer.
It works, but to what scale?!
I'm 99% what I wrote will be completely fine for my use cases. But, since this is my first GenServer in practice, I still feel like I'm working with a bit of magic without reading enough from the spellbook. Galley will likely never have more than a few users, and even if it did I don't see how this could stress test things.
I am curious how much a process can do. How big a piece of state can live in a process, how many messages would I have to stack up in a processes mailbox before it would crash my tiny Fly.io instance, etc. I recall a chapter in Elixir in Action saying that the client/server architecture can be overwhelmed if your process is receiving messages faster than it can handle them (and so memory usage will climb).
Can I speak to the Supervisor?
But speaking of crashing… I haven't even gotten into Supervisors yet (they are another concept floating in the ether). All I know is I'm probably going to want to initialize my timer server in the application state tree, and that has something to do with restoring crashed processes etc. I'm looking forward to learning about the next piece of the puzzle.
Thanks for reading! o/
WT
Footnotes
[^1]: Technically, I could have improved Ari's Garden's timers to be more usable; I don't think it even used localStorage, which meant if a user navigated away from the page and came back the timer would be gone.