Recurring tasks in Elixir: Solution #2

Posted: Aug 13, 2017

Recently, I've read Pete Corey's article about recurring tasks in Elixir. It was quite interesting, because a few months ago I solved the same problem by applying an alternative solution.

After getting an experience in Erlang OTP, I feel like it is an operating system embedded in the language. This feeling occurred because libraries are applications which don't block each other if there aren't depending calls. Meantime, applications (libraries) consist of processes which work in parallel too.

Solution

While launching a gen server process we can specify a timeout which defines a number of milliseconds to wait for a call. Once it passes, a handle_info callback gets called with a :timeout atom. So, using this capability, we can periodically launch code in a module.

I would use Pete's example about fruits to show how it works.

defmodule HelloRecurring.FruitPrinter do
  use GenServer

  @fruits ["🍉", "🍊", "🌽", "🍒", "🍇", "🌶"]

  @timeout 2000

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

  def init(state) do
    # wait 2 secs for a call. Since nothing sends a message to this process,
    # it gets the timeout message
    {:ok, state, @timeout}
  end

  # Once timeout passes, this callback gets called,
  # thus we can execute our code
  def handle_info(:timeout, state) do
    print_fruit()

    # after executing the code, we schedule another execution,
    # so it works like a loop
    {:noreply, state, @timeout}
  end

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

This solution is a little bit different from Pete's solution. The print_fruit function won't be called exactly in 2 secs after the first call. Let's say, the print_fruit takes 50 milliseconds, so next time it will be called in 2050 milliseconds. It happens because we schedule another execution after calling the print_fruit. It results in getting time shift. This fact might be a disadvantage in some cases, but it might be an advantage in other cases. For example, if you need to do some periodical work with a DB, you might want to be sure that the previous execution of a recurring task is done before it gets executed again. Otherwise, race conditions might happen.

I use this approach to expire inactive sessions, so the time shift isn't a problem for me. Having applied this solution I am sure the identical work won't be done for the same session twice.

Because of the nature of OTP, we need less external tools/libraries to solve challenges in Elixir. It results in having a simpler stack, so we get lower maintenance cost.