Help understanding the difference between Task and GenServer

Hi. I recently started picking up Elixir, and have been reading code here and there. I came across the Clock module on the Bandit repo.

This module updates a value in an ets table every second, and it is implemented using the Task module. My question is, what is the difference between this implementation and implementing the same functionality using GenServer? (more or less like this, I guess):

defmodule Bandit.ClockGen do
  use GenServer

  def date_header do
    # exactly the same as in Bandit.Clock
  end

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, [])
  end

  @impl true
  def init(_) do
    __MODULE__ = :ets.new(__MODULE__, [:set, :protected, :named_table, {:read_concurrency, true}])
    update_header()

    {:ok, %{}}
  end

  @impl true
  def handle_info(:update_header, state) do
    update_header()
    {:noreply, state}
  end

  def handle_info(_, state), do: {:noreply, state}

  defp update_header do
    :ets.insert(__MODULE__, {:date_header, get_date_header()})
    Process.send_after(self(), :update_header, 1_000)
  end

  defp get_date_header, do: Calendar.strftime(DateTime.utc_now(), "%a, %d %b %Y %X GMT")
end

Are these two implementations equivalent? What am I missing?

Thank you.

The difference mostly manifests when you want to interact with the process by sending messages from other processes.

For instance, imagine you wanted to be able to tell the ClockGen to temporarily stop updating.

GenServer provides a straightforward extension point via handle_call/handle_cast/handle_info (depending on the specific shape of the messages), while a pure Task version will need to write an explicit receive somewhere.

2 Likes

Complementing @al2o3cr answer, while you can supervise a Task started with Task.Supervisor, by itself a Task is not an OTP behavior. It doesn’t have built-in callbacks for restarts or fault-tolerance logic. Tasks are often used for “fire and forget” jobs or for parallelizing parts of a computation.

In short, anything that can be done with a Task can also be done with a GenServer, but not the other way around. The author of Bandit chose to use a Task as a design decision. I believe they opted for a Task because it simplifies the implementation, as there was no need to control the lifecycle or handles fault-tolerance.

These two implementations are equivalent. In fact, if it were my design decision, I would prefer to use a GenServer to implement this Clock and eliminate the recursion.

2 Likes