Bibek Pandey (7) [Avatar] Offline
#1
In Section 8.23 where Monitors are discussed, there is a small grey box that mentions that exits are propagated through gen_server calls. How do you trap these?

Let's say you have a module A which is a GenServer

You also have a module B, which also is a GenServer


in one of A's handle_call method's you issue a GenServer.call(B_pid, :foo)

if The Server Process B crashes during this call, the error will be propagated to Server Process A.

What needs to be done in either modules A or B to trap exits here? I've experimented with trapping exits in module A's init function and supplying the relevant handle_info function but to no success. The Process A dies. I'm assuming the Server Process A is also monitoring Process B.

Is there some different technique to trap exits that are propagated through gen server calls?

Thank you very much!
sjuric (86) [Avatar] Offline
#2
There are two aspects of the answer. The first one is that GenServer.call will raise an exit in the client process if the server process terminates while the client awaits the response. You can catch that exit with try/catch:

try do 
  GenServer.call(...) 
catch :exit, reason -> 
  # do something here
end


In addition, a crash of the server is propagated to all linked processes. If the client is linked to the server process (directly or indirectly through another non-supervisor process), the exit signal will reach it, and the client will by default crash, even if you're using try/catch. In such case, you also need to trap exits in the client, and handle corresponding messages.

If the client is not linked to the server, trapping exits is not necessary.
Bibek Pandey (7) [Avatar] Offline
#3
Thanks for spelling it out. Yes it works. smilie

defmodule A do
  use GenServer


  def start_link() do
    GenServer.start_link(__MODULE__, nil, name: :a)
  end

  def apple() do
    GenServer.call(:a, :apple)
  end

  def handle_call(:apple, _from, state) do
    IO.puts "In apple handle_call, going to call banana"

    try do
      B.banana(5)
    catch :exit, reason ->
        IO.puts "caught :exit, reason is #{inspect reason}"
    end
    
    {:reply, :ok, state}
  end

  def handle_info(msg, state) do
    IO.puts "Handle_info, msg received #{inspect msg}"

    {:noreply, state}
  end

end


defmodule B do
  use GenServer


  def start() do
    GenServer.start(__MODULE__, nil, name: :b)
  end

  def banana(value) do
    GenServer.call(:b, {:banana, value})
  end

  def handle_call({:banana, value}, _from, state) do
    IO.puts "In banana handle_call, about to explode"

    case value do
      5 -> raise "Pigs are flying, crashing now"
      _ -> ""
    end

    {:reply, :ok, state}
  end

end




Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> B.start
{:ok, #PID<0.109.0>}
iex(2)> A.start_link
{:ok, #PID<0.111.0>}
iex(3)> A.apple
In apple handle_call, going to call banana
In banana handle_call, about to explode
caught :exit, reason is {{%RuntimeError{message: "Pigs are flying, crashing now"}, [{B, :handle_call, 3, [file: 'lib/propagate.ex', line: 50]}, {:gen_server, :try_handle_call, 4, [file: 'gen_server.erl', line: 615]}, {:gen_server, :handle_msg, 5, [file: 'gen_server.erl', line: 647]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 247]}]}, {GenServer, :call, [:b, {:banana, 5}, 5000]}}
:ok
iex(4)> 
12:03:47.149 [error] GenServer :b terminating
** (RuntimeError) Pigs are flying, crashing now
    (propagate) lib/propagate.ex:50: B.handle_call/3
    (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
    (stdlib) gen_server.erl:647: :gen_server.handle_msg/5
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:banana, 5}
State: nil

nil
iex(5)> Process.whereis(:a)
#PID<0.111.0>
iex(6)> Process.whereis(:b)
nil