GenServer and async tasks, can they work together?

I have made an experimental system (Frankenstein?) where a GenServer initialises and provides the necessary state for handle_call’s, but the handle_call’s itself doesn’t return the value of the computations, but instead returns a closure that is then executed/waited for by the caller.

defmodule Test do
  use GenServer

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

  def get_foo(foo_id) do
    with {:ok, task} <- GenServer.call(__MODULE__, {:get_foo, foo_id}) do
      task
      |> Task.async()
      |> Task.await()
    end
  end

  def init(options) do
    send(self(), {:load, options})
    {:ok, nil}
  end

  def handle_info({:load, _options}, _state) do
    state = SomeModule.expensive_state_load()
    {:noreply, state}
  end

  def handle_call({:get_foo, foo_id}, _from, state) do
    task = fn -> FooModule.get_foo(foo_id, state) end
    {:reply, {:ok, task}, state}
  end
end

question: is there an equal easy method to do what I want: to avoid blocking a genserver queue containing necessary “global state”?

also worth mentioning that this is not for use in production code. It’s used in exunit tests, which have a configureable amount of concurrent tests.

There’s a very handy GenServer pattern for trying to achieve what you are doing here without blocking GenServer queue, and it boils down to returning {:noreply, state} in the handle_call/3 callback, and remembering the from reference - either on GenServer’s state or - as in your case, by passing it to the Task. This will make the client that’s calling GenServer pause, but GenServer itself won’t be paused.
Now, once the async task finishes, even from within that task you can reply with GenServer.reply.

This article has more info.

This is not a hack, by the way, this is why the handle_call/3 does have the from parameter and why you can return {:noreply, state} from the callback in the first place: this is an intended use case.

11 Likes

cool! Thanks! This was a super nice method to encapsulate the waiting on the async task inside the genserver :slight_smile: Love it ! Btw: it worked wonders in my use case

2 Likes