A common code sample I like to write when learning a new language is to have multiple threads or processes “do something” on an interval. For example, publishing to Kafka or RabbitMQ, invoking a REST API, etc., as a way of simulating a multi-process worker system. As a starting point, I like to build these to simply output a string to the console and then build my way up from there. I figured I would share how to do this in Elixir for those who may be interested.

Asynchronous Work in Elixir

In Elixir, we have various ways to manage concurrency, ranging from built-in primitives to third-party tools that even provide fantastic backpressure mechanisms.

They all have their own pros and cons worth evaluating, but for the simple desire to run multiple processes that do something on an interval, GenServer is the best choice. Task is more of a lightweight process for executing one-off jobs—it will spawn, run to completion, and then terminate—while GenServer is better suited to long-running processes that may maintain state and handle multiple operations over time.

Getting Started

Assuming we have Elixir and Mix set up, let’s create a new Mix project.

$ mix new hello_supervisor --module HelloSupervisor

This will create a standard Elixir project and an empty HelloSupervisor module.

Supervision Tree for GenServer

Alright, for this example, we’ll run a supervisor that spawns N processes and lets them control the interval at which they do their work. From a high level, our application will look like this:

Supervision Tree with application, followed by supervisor, which in turn has three workers underneath

Starting with our application, it will simply start the supervisor and link it to the main process.

# lib/hello_supervisor.ex
defmodule HelloSupervisor do
  use Application

  @impl true
  def start(_type, _args) do
    HelloSupervisor.Supervisor.start_link([])
  end
end

Next up, let’s define our supervisor, which will determine the number of workers we want and start them up during initialization. The nice thing about a supervisor is that it will monitor and restart crashed processes as well, which I won’t dig into today.

# lib/hello_supervisor/supervisor.ex
defmodule HelloSupervisor.Supervisor do
  use Supervisor

  @workers 5

  def start_link(_args) do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  @impl true
  def init(:ok) do
    children = Enum.map(1..@workers, fn _ ->
      %{
        id: make_ref(),
        start: {HelloSupervisor.Worker, :start_link, [[]]},
        restart: :permanent
      }
    end)

    Supervisor.init(children, strategy: :one_for_one)
  end
end

Children need a unique ID, and in this case, I used the dead simple make_ref() function, which returns an “almost unique reference.” Per the docs, a “reference will reoccur after approximately 2^82” calls, which is sufficient for our needs here. In a large-scale example, one might opt for something more robust like the UUID module, but I decided to use something more baked in.

Finally, we define our workers. This GenServer implementation works by invoking schedule_work during initialization and once more when the work is performed in handle_info. It works by using Process.send_after/3, which will publish the message :say_hello after the specified interval. To make things interesting, we will pick a random delay between 500ms and 5000ms.

# lib/hello_supervisor/worker.ex
defmodule HelloSupervisor.Worker do
  use GenServer
  require Logger
  @offset 499

  def start_link(opts) do
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  @impl true
  def init(state) do
    schedule_work()
    {:ok, state}
  end

  @impl true
  def handle_info(:say_hello, state) do
    Logger.info("Hello from process #{inspect(self())}")
    schedule_work()
    {:noreply, state}
  end

  def schedule_work() do
    Process.send_after(self(), :say_hello, random_interval(5_000))
  end

  defp random_interval(max) do
    :rand.uniform(max - @offset) + @offset
  end
end

With all of these pieces in place, it is time to run the demo with iex -S mix.

For Next Time

I hope this was a good introductory example on how to perform some work on a random interval. Next time, I will expand this to demonstrate running multiple workers to publish messages that are consumed by other processes, but feel free to try that on your own as well! You can find the code for this example in my project for learning Elixir on GitHub.

comments powered by Disqus