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:
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.