Need help unblocking a supervised Task in the handle_info() of a GenServer

defmodule SomeMonitor do
  use GenServer

  @timeout 5_000

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

  @impl true
  def init(state) do
    Process.flag(:trap_exit, true)
    Process.send(self(), :kickoff_update, state)

    {:ok, state}
  end

  @impl true
  def handle_info(
        :kickoff_update,
        state
      ) do
    DoStuff.launch()

    Process.send(self(), :update_transactions, state)
    {:noreply, state}
  end

  @impl true
  def handle_info(:update_transactions, state) do
    timeout_reference = Process.send_after(self(), :job_timeout, @timeout)

    task =
      Task.Supervisor.async(
        TasksSupervisor,
        fn -> PyOperatorSupervisor.launch({[], "get_fresh_info", "main"}) end
      )

    results = Task.await(task, 10_000)

    Process.cancel_timer(timeout_reference)

    case results do
      {:ok, _} ->
        {:ok, _} = ReviewTransactions.main(results)
        Process.send(self(), :update_transactions, state)
        {:noreply, state}

      {:error, reason} ->
        {:stop, reason, state}
    end

    {:noreply, state}
  end

  @impl true
  def handle_info(:job_timeout, state) do
    LogBook.main(
      "Still waiting for job to finish . . . Elapsed time: #{@timeout / 1000} second(s).",
      __MODULE__,
      70,
      :warning
    )

    {:noreply, state}
  end

  @impl true
  def handle_info({:EXIT, _, :normal}, state), do: {:noreply, state}

  @impl true
  def handle_info(critical_error, state), do: {:stop, critical_error, state}

  @impl true
  def terminate(critical_error, state),
    do: LogBook.main({state, critical_error}, __MODULE__, 77, :error)
end

What’s an efficient approach to avoid Process.send_after/3 from being blocked by Task.await/2?

Can you explain a bit more what you want to achieve and what the problem is?

What I really need is for a warning to be issued if the task is not finished within 5 seconds. On the other hand, if the task is finished within the specified time, ReviewTransactions.main/1 processes the results.

The issue with the current implementation is that Task.await/2 prevents Process.send after/3 from being executed until it is finished; as a result, when the task takes longer than five seconds, the warning is not emitted until Task.await/2 is done.

A warning? Or do you want to cancel the task if it’s not done on time?

Anyway have a look at Task.Supervisor — Elixir v1.14.3

It shows how to use tasks with OTP behaviours. That should allow you to do what you need.

I think you may be looking for yield Task — Elixir v1.12.3

Calling Task.await in GenServer callbacks is not recommended, because it does exactly what you’re seeing (blocks the GenServer receive loop).

In your case, there would be three handle_info heads involved similar to the example in the docs:

  • one for {ref, result} that handles success and cancels the timeout
  • one for {:DOWN, ...} that handles the task shutting down
  • one for a timeout message that kills the task
1 Like