Hi there,
when trying to use GenServer.call/2
with a pid that doesn’t exist (anymore), the calling process gets terminated.
Of course, one could use GenServer.whereis/1
to check if the GenServer is currently running but that would still introduce a potential race condition in which the GenServer is terminated from another process between the calls to GenServer.whereis/1
and GenServer.call/2
.
Here is a little module to illustrate the problem:
defmodule Foo do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, :bar, name: __MODULE__)
end
def shutdown(delay) do
Task.async(fn ->
:timer.sleep(delay)
GenServer.stop(__MODULE__)
end)
end
def query(delay) do
Task.async(fn ->
:timer.sleep(delay)
GenServer.whereis(__MODULE__)
|> case do
nil ->
IO.puts "GenServer #{__MODULE__} has gone away"
pid ->
:timer.sleep(delay)
GenServer.call(pid, {:query})
|> IO.inspect
end
end)
end
def handle_call({:query}, _from, state),
do: {:reply, state, state}
end
If you call Foo.start_link && Foo.shutdown(100) && Foo.query(10)
, everything will work out nicely and the state will be printed.
When you call Foo.start_link && Foo.shutdown(100) && Foo.query(150)
, you also get the expected behavior: a message that the GenServer has gone away will be printed.
When calling Foo.start_link && Foo.shutdown(100) && Foo.query(150)
, however, the Task
created in Foo.query/1
crashes.
The reason for this is that GenServer.call/3
internally calls Kernel.exit/3
if GenServer.whereis/1
returns nil
.
So what is the recommended way of only messaging GenServer if it’s still alive? Should I be trapping the exits for processes that try to call GenServers that have potentially been terminated?
Doesn’t this also mean that GenServer.call/3
itself is vulnerable to the same race condition because it uses the same check as the Foo
module shown above?