Summary: One of the best parts of being an independent consultant is having the freedom to explore ideas and spin up fun side products using cool technology. Today we'll look at my newest project, KLEO, and why Elixir is a great tool for these prototypes.
Have you ever found yourself at the end of the work week wondering where all your time went? You had several meetings here and there, plus lunch with an old friend, and suddenly it was Friday. What happened? Was it a productive week?
To answer this question, I built KLEO. KLEO connects to your Google Calendar and summarizes your time spent in one-off meetings, recurring meetings, and all-day events. It lists the people you’ve been spending all your time with. On Friday evenings, it sends you a quick summary email.
It’s fairly basic, but perhaps you’ll find it useful. What’s interesting is how using Elixir to build this prototype means my MVP can easily scale to thousands of users - with no caching layer or background job system. Can your Rails app do that?
Let's look at the two main pieces of the system: displaying the weekly breakdown, and sending a summary email every Friday evening at 7pm (user’s local time). We'll see how Elixir makes both of these pieces easy.
Showing the Weekly Breakdown
KLEO lets you look into past weeks and upcoming weeks. Because your calendar - like life itself - is constantly changing, we hit Google’s Calendar API on every request to fetch the latest data. “But wait,” you say, “synchronous external calls during user requests? What a laggy experience.” Fortunately, Google’s API is fast and so is the Phoenix framework, so total response times are around 200ms.
The bigger problem for a web application is this: while the server is waiting on a response from Google, are my other users stuck waiting for their pages to load? If you’re using Rails, the answer is yes. Puma will happily allocate a separate thread to a user and do useful work while it’s waiting on an external network call, up to a point- the number of threads you’ve specified, usually fewer than 20. And good luck with the occasional garbage collection that stops your entire app cold.
With Elixir and Phoenix, the answer is no. Phoenix allocates a process per user request, and processes in the BEAM VM are cheap. Processes waiting on IO are put to sleep and not scheduled, meaning that your popular app's increasing number of external calls will not affect its capacity to serve new user requests with brand new processes. Your app stays responsive and your users stay happy, even under load.
Plus, since each process is individually garbage collected, no catastrophic GC spikes will ripple through your system.
Now if an external resource has rate limits or slow response times, you should consider prefetching the data and saving it for later. The point is that kind of complexity is not necessary for our own app’s performance at this level of load. I can cheaply make synchronous network calls whether I have 1 concurrent user or 1,000.
Sending a Summary Email
Seems straightforward, right? If you’re experienced with Rails, setting up a background job system and queue is almost automatic. Most apps barely function without one. In fact, we’ve become so accustomed to this practice that we rarely stop and ask why these extra resources and dependencies are needed for such a straightforward task.
Here’s how we do this in Elixir. When the app boots, a worker process is started for each user and added to a supervision tree which will restart the worker if it crashes. When a user signs up, a worker process is immediately spawned and added to this same tree. We pull the user's timezone off their browser and put the process to sleep for the amount of time until next Friday at 7pm, user's local time. Once that interval expires, the process wakes up, fetches calendar data for the week, sends the user an email, and goes back to sleep until the following Friday.
We rely on the cheapness of Erlang processes to scale this simple approach to tens of thousands of users. Remember, sleeping processes are ignored by the scheduler and will not affect the performance of active processes, so our system remains responsive with 1 or 10,000 users.
Further, because all my data stays in the same process memory, I don’t have to juggle the complexity of passing serializable terms through different systems just to bring them back again to do useful work. No more:
- accidentally passing a reified object to Redis
- matching queue names
- opening a web browser to debug queuing issues or job failures
- remembering to restart Resque/Sidekiq when my code changes
- clearing out stale data from the queue when my objects change
God help me if a user disables their notifications and I have to pull their scheduled job off the queue!
Some of you might say “well that’s not too hard, just multiplex your terminal/use this nifty script I wrote/add a bunch of
if...else blocks/stop complaining.” My point is: isn’t it simpler and easier to have all this functionality in one single application? Defending the status quo reminds one of cutting the ends off the roast.
With 10,000 concurrent users, my architecture may shift slightly. But not much. And 10,000 is much further on the horizon than, say, 300 concurrent users, which is fairly healthy in itself. How many users does your current architecture support?
Rails can scale. It’s true. With time and money, Rails can scale. I've done it.
No More Caveats
With Elixir, I get the best of both worlds. I can rapidly spin up products because the language is sensible and because I don’t need additional infrastructure just for basic performance. Scaling happens at scale, not at a few dozen simultaneous users.
If you’ve been looking for a language that lets you build a productive prototype that you don’t have to throw away in six months, try Elixir.
If you’ve been wondering where your time goes, try KLEO :D.
Project? Question? Reach out and say hello.
Sign up to our infrequent newsletter to hear what we are thinking about.