Process in parallel after GenServer.cast

Problem

When I have many access in Phoenix application,
GenServer mailbox is stack, and entire process finish late.
code is like this below.

defmodule Hoge do

  def exec(account_code, x1), do: GenServer.cast(pid, {:exec, arg})

  @impl true
  def handle_cast({:exec, arg1}, state) do
      // something process
    {:noreply, state}
  end
end

My understanding is that GenServer.cast is asynchronous and accumulates the jobs that defined in handle_call in the mailbox, but it processes those accumulated jobs one by one.
However, I want to process those jobs in parallel.

What I want to do

I want to proccess entire job quickly.

So I am thinking I chage it using Task.async so that I can process multiple jobs.
It worked, but I wonder this is right.
In this case, handle_call returns {:noreply, state} before process finish, because Task is asynchronous.

defmodule Hoge do

  def exec(account_code, x1), do: GenServer.cast(pid, {:exec, arg})

  @impl true
  def handle_cast({:exec, arg1}, state) do
    Task.async(fn ->
      // something process
    end)

    {:noreply, state}
  end
end

It would be very glad if anyone have ideas and tell me that🙏

Looking at the documentation it states that:

If you are using async tasks, you must await a reply as they are always sent. If you are not expecting a reply, consider using Task.start_link/1 as detailed below.

So if we want to use Task to process something asynchronously and wait for the result, we can return as state it’s awaited result


 def handle_cast({:exec, arg1}, state) do
    task = Task.async(fn ->
      // something process
    end)

    {:noreply, Task.await(task)}
  end

We return as the gen servers state the result of the task, but you might not want that. You should use await though to wait for the task

As a general rule, if you find everything in your system standing in a line for one process you should consider a different design that avoids the problem.

If the results of something process don’t affect the value of state at all, the code likely shouldn’t be in a GenServer. The “when (not) to use a GenServer” section in the docs explains further.

2 Likes

It will help if you explain why are you cast-ing to a single GenServer and introducing a choke point in your app.

@gpopides
Thanks for the reply!

As long as I can process in paralell, I don’t have to use Task necessarily.
In this case, it seems like I better not to use Task.
It would be glad if you have any ideas and tell me that beside beside Task.

@al2o3cr
Thanks for your reply!

I am relatively new to elixir, and I don’t know about so much patterns.
It would be glad if you know other patters and tell me those🙏

@dimitarvp
This is for your reply!

This process is designed to handle webhooks from other applications. Since I need to return a 200 response immediately and process the request later, I am sending the task asynchronously to a GenServer.

There’s two main ways I have seen this solved. One is with a durable queue, for example Oban. Your controller queues a job, and returns the 200 response. Then the job framework executes it asynchronously. The other approach is to use a DynamicSupervisor in your supervision tree and your controller starts a Task within that supervisor, then returns the 200. This approach is most similar to what you described, but avoids the bottleneck of one GenServer.

2 Likes

In this case you don’t need a single process bottleneck like you currently do.