Tutorial
Setup a supervised background task in Phoenix
This post was updated 01 May - 2020
There are times when you need to spawn a background process for a longer running task. And especially if you are interacting with an external system, you cant be sure that task crasher or not. But as we know, Elixir has built in functionality to handle this and makes it easy to get started with. So, in this tutorial, I will start with a LiveView component that just contains a button. The only idea is that pressing the button will trigger a process that takes a long time and might actually result in a crash. In that case, the supervisor should restart it.
STEP 1 - Setup the LiveView
For this example I want to set up a basic LiveView with a clickable button and a handle_event
for the button click. It can also have two states, loading true or false and a result.
# lib/tutorial_web/live/task_starter_live.ex
defmodule TutorialWeb.TaskStarterLive do
use Phoenix.LiveView
def mount(_session, socket) do
assigns = [
loading: false,
result: nil
]
{:ok, assign(socket, assigns)}
end
def render(assigns) do
~L"""
<div class="mt-10">
<button class="btn btn-primary" phx-click="start-task">Long running task that might crash</button>
</div>
<div class="mt-10">
<%= if @result || @loading do %>
<%= if @loading do %>
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1">
<circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline>
</svg>
<% else %>
Result: <%= @result %>
<% end %>
<% end %>
</div>
"""
end
def handle_event("start-task", _, socket) do
assigns = [
loading: true
]
{:noreply, assign(socket, assigns)}
end
end
STEP 2 - Setup the worker
So, every time anyone clicks the button, I want to start the worker. The basic scaffold for the file will look like:
# lib/tutorial/worker.ex
defmodule Tutorial.Worker do
use Task
def start_link(arg) do
Task.start_link(__MODULE__, :run, [arg])
end
def run(_arg) do
end
end
Note that this worker uses Elixirs Task with use Task
When the code for the worker is in place, can return to the LiveView module and add the code for spawning the worker:
# lib/tutorial_web/live/task_starter_live.ex
alias Tutorial.Worker
...
def handle_event("start-task", _, socket) do
Supervisor.start_link([{Worker, nil}], strategy: :one_for_one)
assigns = [
loading: true
]
{:noreply, assign(socket, assigns)}
end
NOTE after this change, make sure to restart the server.
If I click the button nothing happens. And that is basically just because I haven’t implemented any functionality.
If I add a IO.puts "Hey"
inside the run
function
def run(_arg) do
IO.puts "Hey"
end
Refresh the page and presses the button I should in the console see:
Perfect. Everything works!
STEP 3 - Add functionality to the worker
I want to simulate that my worker need to perform a slow task with an uncertain outcome.
def run(_arg) do
:timer.sleep(3000)
number = Enum.random(0..3)
if number == 1 do
IO.puts "CRASH"
raise inspect(number)
end
IO.puts number
number
end
So the simulate the slowness, there is a 3 second sleep. That I generate I number between 0 and 3. If the number is 1
, the process crashes.
That basically looks like this:
The main issue here is that if it crashes, it doesn’t start again. That is most likely what you want.
Task has a default :restart
of :temporary
. That is easily fixable. I will instruct my Task process to restart. I can do that with the line restart: :transient
# lib/tutorial/worker.ex
defmodule AwesomeDemo.Demo.Worker do
use Task, restart: :transient
...
Now, when it crashes, it will restart the task and then hopefully come to a better outcome.
STEP 4 - Report back to LiveView
It would be nice of I could get the task to report back to the LiveView when there is a success. This can easily be done using Phoenix PubSub.
Once again, open the worker to add some code in my worker.
# lib/tutorial/worker.ex
alias Tutorial.PubSub
@topic inspect(__MODULE__)
def run(_arg) do
...
number
|> notify_subscribers()
end
defp notify_subscribers(number) do
Phoenix.PubSub.broadcast(PubSub, @topic, {:message, number})
end
def subscribe do
Phoenix.PubSub.subscribe(PubSub, @topic)
end
And in the LiveView I first need to make sure that I subscribe to the pubsub channel. And I also need to add the handle_info
function that actually listens to when the worker is done and returns a result.
# lib/tutorial_web/live/task_starter_live.ex
defmodule TutorialWeb.TaskStarterLive do
use Phoenix.LiveView
def mount(_session, socket) do
if connected?(socket) do
Worker.subscribe()
end
...
end
...
def handle_info({:message, number}, socket) do
assigns = [
loading: false,
result: number
]
{:noreply, assign(socket, assigns)}
end
With that in place, I can refresh the page and test the button again. As soon as the worker are done, the result should be visible:
Conclusion
This is just one of many ways to run an async task. I would go for a pattern like this if I need the task to be supervised and also, if more than one process needs to know about the outcome.