Recurring Tasks in Elixir

Running periodic, or recurring tasks is a common undertaking for any web application. The stacks I’ve used in the past have all relied heavily on external databases and job queues to accomplish this task.

Elixir is a little different.

Thanks to Elixir’s Erlang heritage and the power of OTP, we’re given the option to opt out of relying on an external database and manage our recurring tasks from entirely within our application.

The Fruit Printer

Before we implement our recurring task runner, we should have a task that we want to repeat. Let’s pretend that we want to print out a random item from a list of @fruits:

@fruits ["🍉", "🍊", "🌽", "🍒", "🍇", "🌶"] # TODO: Is corn a fruit?

def print_fruit, do: IO.puts("fruit: #{Enum.fetch!(@fruits, :random.uniform(6))}")

To keep up with our voracious appetite for fruit, we want to print a fruit emoji to the console every two seconds. This is our recurring task.

How do we accomplish this?

The first thing we need to do is build our fruit printer into a GenServer process:

defmodule HelloRecurring.FruitPrinter do
use GenServer

@fruits ["🍉", "🍊", "🌽", "🍒", "🍇", "🌶"] # TODO: Is corn a fruit?

def start_link, do: GenServer.start_link(__MODULE__, [])

def print_fruit, do: IO.puts("fruit: #{Enum.fetch!(@fruits, :rand.uniform(6))}")

end

We’ll also want to supervise our new fruit printing operation:

defmodule HelloRecurring do
use Application

import Supervisor.Spec

def start(_type, _args) do
Supervisor.start_link([worker(HelloRecurring.FruitPrinter, [])],
[strategy: :one_for_one, name: HelloRecurring.Supervisor])
end

end

Here we’re simply adding a FruitPrinter as a child of our supervision tree and telling the supervisor to restart the FruitPrinter child process if it ever dies for any reason.

At this point, our FruitPrinter GenServer isn’t doing us much good. It’s running, but it’s not printing fruit. We can still manually print fruit by calling FruitPrinter.print_fruit, but this would run the print_fruit function within the current process, not the GenServer’s process.

Not good enough!

We want our FruitPrinter to automatically print its own fruit every two seconds!

Our solution comes in the form of standard process messages. Let’s wire our FruitPrinter up to print_fruit whenever it receives a :print_fruit message:

def handle_info(:print_fruit, state) do
print_fruit()
{:noreply, state}
end

Now we can send a :print_fruit message to our FruitPrinter process with either Process.send/3, or Process.send_after/4.

Printing Fruit Forever and Ever

Sending delayed messages with Process.send_after/4 will be the key component to implementing our recurring task.

The general idea behind building out a recurring task runner in Elixir is that the task itself should be a GenServer process that schedules sending messages to itself signaling it to carry out its task.

Putting that plan into action, once our FruitPrinter is started, we can schedule a :print_fruit message to be sent to itself in two seconds:

def init(state) do
schedule()
{:ok, state}
end

def schedule, do: Process.send_after(self(), :print_fruit, 2000)

It’s important to note that we need to schedule our :print_fruit message in the GenServer’s init callback, rather than the start_link callback, because start_link is called under the context of the supervising process. The init callback is called once the process is created, and self() will point to our FruitPrinter, not the supervisor.

Next, we’ll add another call to schedule() in our handle_info callback. This will ensure that every handled :print_fruit message will schedule another :print_fruit message to be sent two seconds in the future:

def handle_info(:print_fruit, state) do
schedule()
print_fruit()
{:noreply, state}
end

Spinning up the application, you’ll notice a constant stream of fruit being printed at a steady rate of once every two seconds.

Delicious victory.

Even When Things Go Wrong

Astute readers may have noticed a bug in our initial print_fruit function.

We’re using :rand.uniform(6) to pick a random index out of our list of @fruits. Unfortunately, :rand.uniform/1 produces a random number between 1 and n, not 0 and n - 1, as we assumed. This means that any given call to print_fruit has a one in six chance of crashing with an out of bounds error.

Whoops!

Interestingly, this bug hasn’t affected our recurring task. If we run our application for long enough to see this error raise its head, we’ll notice that two seconds after our FruitPrinter process crashes, it’s up and running again trying to print another random fruit.

Because our FruitPrinter is being supervised, any failures that result in a crash of the process will cause the supervising process to create a new FruitPrinter in its place. This new FruitPrinter will schedule a :print_fruit message in its init callback, and will continue working as expected.

Back to the problem at hand, the proper way to implement our print_fruit function would be with Elixir’s Enum.random/1 function:

def print_fruit, do: IO.puts("fruit: #{Enum.random(@fruits)}")

That’s better. We certainly don’t want bugs in our fruit.

Final Thoughts

While this type of entirely in-application recurring process may not be a solution for every problem out there, it’s a powerful option in the Elixir environment.

The robustness given to us by the concept of supervisors and the “let it crash” mentality gives us a clear advantage over similar patterns in other languages (i.e. setTimeout in Node.js).

Before you go reaching for an external tool, I’ve found that it’s often beneficial to ask yourself, “can I do this with just Elixir?”