When I was a kid I went through a phase where I was pretty into magic. I learned some simple card tricks and bought a couple of cheap props such as a disappearing vase and a fake thumb. Not being content with simple tricks, I remember asking my Dad if there was such a thing as “real” magic. He replied there was magic everywhere; as proof he whipped out a calculator and exclaimed how amazing it was that a little device could be so powerful. Magic was everywhere in technology! I admit to not being very impressed at the time… however nowadays with Phoenix and Elixir I’ve become a convert, I feel the magic!
Today we’ll build out a small application that brings together the magic of LiveView, PubSub, and Oban. For our application we’re going to revisit a classic 80’s movie, ET. We’ll write an application to help a group of stranded extraterrestrials “phone home” and get back to their planet.
It will look like:
We’re not going to build anything particularly special or impressive… but IMO what is impressive is how easy and with how little code something like this can be accomplished via LiveView. No API, no front-end javascript framework to worry about, all with a SPA-like feel… how is that not a win!
Creating the project
We’ll call our project phone_home
, so let’s generate it. For reference I’m using version 1.13.4
of Elixir and 1.6.9
of Phoenix.
Terminal
We’ll follow the instructions from the mix phx.new
output and make sure everything seems to be running.
Terminal
Navigating to http://localhost:4000/ everything looks as it should!
All good, let’s get started by creating a database table to hold our extraterrestrials.
Adding the DB table and schema
Since this is just a little demo app we aren’t going to worry too much about the database design or file structure, we’ll create a single database table and place the schema file right in the root of the lib
directory.
Let’s start by generating a migration for the database table.
Terminal
/priv/repo/migrations/’timestamp’_create_extraterrestrials.exs
Very simple, we just have a name for our aliens and then a couple of fields to represent the state of their phone home attempts. In a real application you might want to think about whether the communication fields really belong in this table or should be in a separate table… but the above structure will serve our purpose.
The schema will mirror the database fields, let’s create it next.
Terminal
/lib/phone_home/extraterrestrial.ex
Nothing complicated, we’re making use of an Ecto.Enum to give a bit more structure to the communication_status
field.
Finally let’s add some seed data to initialize the database.
/priv/repo/seeds.exs
And with that we can run the migration.
Terminal
… and the seeds.
Terminal
Sweet! Let’s create an initial bit of front end code to view our extraterrestrials.
Displaying extraterrestrials
For our first iteration of the front end we will simply list out the extraterrestrials. We’ll need a function to retrieve the extraterrestrials so let’s create that first. We’ll put it right in the main context of the application.
/lib/phone_home.ex
Simple, just grabbing all the records ordered by name.
Next we’ll add a very simple LiveView.
Terminal
For now, the liveview file just needs a mount
function along with a couple of helpers to format the data.
/lib/phone_home_web/live/extraterrestrial_live/index.ex
The mount
function loads up the extraterrestrials and puts them in the socket assigns so they will be available to our HTML
. The xxx_display_value
functions just provide some formatting help.
The HTML
looks like:
/lib/phone_home_web/live/extraterrestrial_live/index.html.heex
Simple, we just loop thru the extraterrestrials and display them.
Finally we need to add a route for the liveview.
/lib/phone_home_web/router.ex
Now if we fire up the server…
Terminal
… and navigate to http://localhost:4000/ we’ll see some aliens!
Nothing magical so far, but let’s start brewing some spells by adding the phone home functionality.
ET Phone Home
We’ll start with the backend code. The idea is we’ll queue up a job for each extraterrestrial attempting to phone home. Each job will be attempted 5 times after which we’ll assume our extraterrestrial’s friends are currently busy and not answering their calls.
Repeatable jobs sound like a great use case for a job scheduling library… and an excellent choice in Elixir land is Oban. So let’s add and configure Oban.
Add Oban
First we’ll add the dependecy to mix.exs
.
/mix.exs
And then grab the dependecy.
Terminal
We need to create some Oban
specific tables, so the next step is to create a migration.
Terminal
As per the Oban installation guide the contents of the migration should be:
/priv/repo/migrations/’timestamp’_create_oban_jobs.exs
Now we can run the migration.
Terminal
We next need to add a configuration section for Oban
in config.exs
… we’ll add it just after the JSON
configuration.
/config/config.exs
All this specific configuration does is set up a single Oban queue called phone_calls
which can execute up to 50 concurrent jobs at a time.
The final bit of setup to get Oban working is to include it in application.ex
.
/lib/phone_home/application.ex
All we’ve done above is to add Oban
to the list of children supervised by our application along with a config function.
Oban has the concept of worker modules. We’ll need one of these to process our phone calls. If you watched ET you’ll recall they used a Speak & Spell to phone home, so we’ll follow their lead and create a speak and spell worker.
The Speak and Spell worker
We’ll start with a bare minimum worker module and build out the functionality as we go along…
Terminal
/lib/phone_home/speak_and_spell_worker.ex
The use Oban.Worker
line indicates this is an Oban
worker. We also specify the queue the worker acts upon and the number of times a particular job will be attempted before Oban
gives up, in this case 5 times.
Next let’s add a phone_home
function in our main phone_home.ex
module to queue up jobs.
/lib/phone_home.ex
The phone_home/1
function inserts a job for each extraterrestrial
passed into the function. When inserting an Oban job we include can include a map of args that will be stored in the oban_jobs
table. In this case we pass in an extraterrestrial_id
. This way we’ll know what extraterrestrial
a job refers.
Let’s try this out in the console.
Terminal
iex Terminal
If we look in the database we’ll see our jobs have processed…
It’s not very impressive as the jobs don’t actually do anything… let’s tackle that next.
Add the phone_home logic
Instead of just returning :ok
we’ll update the speak_and_spell_worker
to include a fake phone_home
function. Based on the result of this function we’ll update the communication_status
and communication_attempt
values for the appropriate extraterrestrial
.
/lib/phone_home/speak_and_spell_worker.ex
Pretty simple… the first thing we do is grab the extraterrestrial based on the Oban
args field, i.e.
Next we call into a function we’ve faked up to simulate some phone home logic:
Based on the return value of the above function we update the extraterrestrial
:
Notice we return an error tuple when we receive no answer in the perform
function. When Oban
receives an error tuple it records the error and schedules a retry of the job (assuming the max_attempts
value has not yet been reached). When we return :ok
in the success case Oban just marks the job as complete.
The process_phone_home_result
function we call prior to returning :ok
or {:error, :no_answer}
just updates the extraterrestrial
record in the database.
The worker module is using a few functions that don’t yet exist so we need to add get_extraterrestrial!
and update_extraterrestrial_communication_status!
to the main phone_home.ex
module.
/lib/phone_home.ex
Nothing tricky about either of these, just standard Repo / Changeset stuff.
Let’s hit up the console again now that we’ve made these changes.
Note: when making changes to an Oban
worker you need to recompile in order for the changes to take affect (or restart the server).
iex Terminal
Now if we refresh the browser as our jobs process we’ll see the status of our phone home attempts.
So this works alright but we don’t really want to have to refresh the browser in order to see the status updates… and this is where PubSub
comes in.
Broadcasting alien updates
We need to broadcast
when an alien is updated. We can then subscribe
to these updates in our LiveView
and dynamically update the status fields.
Let’s add the subscribe
and broadcast
functions to phone_home.ex
/lib/phone_home.ex
Pretty darn simple, we’re broadcasting / subscribing to a topic called extraterrestrial
, and in the broadcast function we are passing a tuple containing an atom (:extraterrestrial_updated
) and the actual extraterrestrial
.
We need to call broadcast_extraterrestrial_update
from our speak_and_spell_worker
worker. This is simple, we just need to update process_phone_home_result
.
/lib/phone_home/speak_and_spell_worker.ex
All we’ve done is pipe the result of update_extraterrestrial_communication_status!
to the broadcast
function.
That takes care of the back-end, now for a few front-end updates.
Reflecting the status in the UI
The first thing we’ll do is add a button to our HTML
file so we can trigger the phone home
functionality.
/lib/phone_home_web/live/extraterrestrial_live/index.html.heex
Now in the live view module we’ll:
- Subscribe to the alien update broadcasts.
- Add handlers for the button we just added and for alien broadcast events.
- update the
communication_status_display_value
functions to display a status specificSVG
.
The full code is:
/lib/phone_home_web/live/extraterrestrial_live/index.ex
Let’s have a quick look at some of the changes, starting with the mount
callback.
Here we subscribe to the extraterrestrial broadcasts. Note we need to do this after the live socket has connected… which is why we make use of the connected? function provided by LiveView
.
The button handler is straight forward…
The handler matches on the event name we gave in the HTML
for the button, i.e. phx-click="phone_home"
.
In the body of the function we filter on aliens with a status of nil
or :no_answer
(as these are the aliens who have neither successfully phoned home or are currently phoning home) and queue jobs for them via PhoneHome.phone_home()
.
The handler for broadcasts is also pretty simple.
We match on the {:extraterrestrial_updated, extraterrestrial}
tuple that is sent from the broadcast and then replace the old extraterrestrial struct with the updated struct in our list of extraterrestrials currently contained in the socket assignments.
Other than a couple of changes to the display helper functions which we won’t bother going over that’s it for the liveview module updates.
Update the CSS
The final piece of the pie is to update our css
file with some classes to provide a spin operation for the SVG
we display during the phoning home
status.
/assets/css/app.css
Note: in a real application I would suggest using something like Tailwind CSS instead of writing a bunch of possibly (in my case probably) janky custom CSS.
Let’s give everything a final test… we’ll reset our DB, fire up the server…
Terminal
… and click the Phone Home button… a sped up version of the result is:
Nice, looks like half our extraterrestrials have their transport home all figured out!
Summary
That’s it for our application, the synergy of LiveView, PubSub and Oban is really impressive in terms of how easy these technologies make it to build something out.
There are things you’d want to consider in a real application. For instance we are storing all our extraterrestrials in the LiveView
assigns. This works fine when we have a limited number of extraterrestrials, but if we were in say a They Live situation where there are aliens freakin’ everywhere we might need a different strategy in order to avoid storing everything in memory. Likewise we’d want to think carefully about the broadcast and subscribe functions and what / when we are broadcasting.
And of course some testing would be good! Luckily both LiveView and Oban have solid testing stories… which is as you would expect when it comes to the Elixir eco-system. If you’re interested in diving into LV testing, I recommend the Testing LiveView course by German Velasco. It provides an excellent and comprehensive run thru of LV testing and I personally really enjoyed the course.
Thanks for reading!
Today’s code is available on GitHub.