How can I get notification on async failed in GenServer?

Zhao Yi Source

In elixir GenServer, there are sync and async methods, handle_cast and handle_call. In the async case, if the method failed, how can I get the notification?

The method failed means in the handle_call method, I need to define some logic to query/write to the database. If the database actions failed, I need to notify the caller about this failure. In async method, how can I do that?

elixirgen-server

Answers

answered 1 week ago Chris Meyer #1

So the comments encouraging you to 'let it die' are generally correct. Idiomatic Erlang and Elixir call for 'failing fast', and letting the supervisors restart whichever components crash.

That said, there are times when crashing isn't appropriate; usually when you know that the negative outcome is likely to occur. Many APIs in the standard library handle this by returning result tuples, i.e. {:ok, result} or {:error, reason} and make it the responsibility of the calling code to either crash or try something else.

In your use case, I think you should just call the database write/query code from the process with the data, without using an async method at all, fixing your database performance first. If this really is a long-running query, and optimizing the database isn't the right answer, your next best option is the Task module (documentation here), part of Elixir's standard library - it provides baked-in capabilities for async task execution.

I know how frustrating it is for people to not answer your question though, so I'll answer it; but be aware that this is almost certainly not the right way to solve your original problem.

The key insight is to pass the calling process's pid to the Worker so that it can send the outcome message later:

defmodule CastBackExampleWorker do
  use GenServer
  # ...
  def do_operation(args) do
    caller = self()
    ref = make_ref()
    # Pass the caller's pid to the GenServer so that it can message back later
    GenServer.cast(__MODULE__, {:do_operation, caller, ref, args})
    # hand back a unique ref for this request
    ref
  end

  # ...

  def handle_cast({:do_operation, caller, ref, args}, state) do
    case execute_operation(args) do
      {:ok, result} -> send(caller, {__MODULE__, ref, {:ok, result}})
      {:error, reason} -> send(caller, {__MODULE__, ref, {:error, reason}})
    end
    {:noreply, state}
  end
end

defmodule CastBackExampleClient do
  use GenServer
  # ...
  def handle_call(:my_real_work, _from, state) do
    # ignoring the ref, but we could stick it into our state for later...
    _ref = CastBackExampleWorker.do_operation([])
    {:reply, :ok, state}
  end

  def handle_info({CastBackExampleWorker, _ref, outcome}, state) do
    # Do something with the outcome here
    {:noreply, state}  
  end
end

comments powered by Disqus