Task Supervisor with max_restart and max_seconds

I am working on a code that will ping an external API every 15 minutes, to retrieve the response and store it in the database.

def get_forecast(city) do
  app_id = My.api_key()
  query_params = URI.encode_query(%{"q" => city, "APPID" => app_id})
  url =
  "https://api.weather.org/data/2.5/forecast?" <> query_params
    case HTTPoison.get(url) do
      {:ok, %HTTPoison.Response{status_code: 200} = response} ->
        {:ok, Jason.decode!(response.body)}
      {:ok, %HTTPoison.Response{status_code: status_code}} ->
        {:error, {:status, status_code}}
      {:error, reason} ->
        {:error, reason}
    end
end

I am running this inside a Task, and that Task is running inside a Genserver.

In application.ex

  use Application



  def start(_type, _args) do
    children = [
      # Starts a worker by calling: FsiIntegration.Worker.start_link(arg)
      # {FsiIntegration.Worker, arg}
      {Task.Supervisor, name: Integration.TaskSupervisor, restart: :transient,  max_restarts: 3, max_seconds: 4000}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Integration.Supervisor]
    Supervisor.start_link(children, opts)
  end

In genserver file

defmodule Integration.FsiServer do

  use GenServer

  @timeout 15000

  #Public API
  def start_task do
    GenServer.start_link(__MODULE__, %{ref: nil}, name: __MODULE__)
  end

  def execute_task(pid) do
    GenServer.call(pid, {:execute, @timeout})
  end

  #Callbacks API
  def init(state) do
    {:ok, state}
  end

  # In this case the task is already running, so we just return :ok.
  def handle_call({:execute, _task_timeout}, _from, %{ref: ref} = state) when is_reference(ref) do
    {:reply, :ok, state}
  end

  # The task is not running yet, so let's start it.
  def handle_call({:execute, task_timeout}, _from, %{ref: nil} = state) do
    IO.inspect state
    task =
      Task.Supervisor.start_child(Integration.TaskSupervisor, fn ->**
       {:ok, _} = IntegrationGet.get_forecast("city")**
     end)

    {:reply, :ok, %{state | ref: task.ref}}
  end

  # The task completed successfully
  def handle_info({ref, answer}, %{ref: ref} = state) do
    Process.demonitor(ref, [:flush])
    {:noreply, %{state | ref: nil}}
  end

  # The task failed
  def handle_info({:DOWN, ref, :process, _pid, _reason}, %{ref: ref} = state) do
    {:noreply, %{state | ref: nil}}
  end
end

My questions are :

I know I have to improve on the below code. Can you guys me insight on how to make this better

  use Application



  def start(_type, _args) do
    children = [
      # Starts a worker by calling: FsiIntegration.Worker.start_link(arg)
      # {FsiIntegration.Worker, arg}
      {Task.Supervisor, name: Integration.TaskSupervisor, restart: :transient,  max_restarts: 3, max_seconds: 4000}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Integration.Supervisor]
    Supervisor.start_link(children, opts)
  end
  def handle_call({:execute, task_timeout}, _from, %{ref: nil} = state) do
    IO.inspect state
    task =
      Task.Supervisor.start_child(Integration.TaskSupervisor, fn ->
       {:ok, _} = IntegrationGet.get_forecast("city")
     end)

    {:reply, :ok, %{state | ref: task.ref}}
  end
  1. The time interval between restarts I have set max_seconds to 5 seconds. But the restart happens immediately.

  2. Is Task.Supervisor.start_child is good for making an external API request. Just it will make a single API request in this case. Or I have to go for async_nolink or async under Task module
    My use case is request an external API and store the response in the DB.

The max_restarts and max_seconds options only states how many (max_restarts) restarts of a child in a time period of time (max_seconds) the supervisor should tolerate before crashing.

You can read about max_seconds here: https://hexdocs.pm/elixir/Supervisor.html#module-start_link-2-init-2-and-strategies.

  1. I would start with something simpler like this:
defmodule WeatherPoller do
  use GenServer

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

  def init([]) do
    send(self(), :execute)
    {:ok, []}
  end

  def handle_info(:execute, state) do
    case IntegrationGet.get_forecast("city") do
      {:ok, response} ->
        store_response(response)

      {:error, error} ->
        handle_error(response)
    end

    {:noreply, schedule_next(state)}
  end

  defp schedule_next(state) do
    Process.send_after(self(), :execute, :timer.seconds(15))
    state
  end
end

1 Like

I like option 3 here:

But with the part of handling a failure, I would put in a sleep

# Unexpected failure
  def handle_info({:DOWN, ref, :process, _pid, _reason}, %{ref: ref} = state) do
    :timer.sleep(3000)
    # restart the task
    task = do_start_task()
    {:noreply, %{state | ref: task.ref}}
  end

sleep will block the process from handling anything else in the meantime. It’s usually better to use sent_interval to receive another message at a later time to trigger the restart, but staying responsive to other messages.

1 Like

True, 100% agree. Was in ruby land for a moment