mickel8
How to correctly handle GenServer.call exits because of non-existing process
GenServer.call might fail when a process we try to execute a call on no longer exists. Are there any guidelines on how to handle such situation? Let’s assume the following scenario:
defmodule RoomService do
def list_rooms() do
RoomRegistry
|> Registry.select([{{:"$1", :_, :_}, [], [:"$1"]}])
|> Enum.map(&Room.get_state(&1))
|> Enum.reject(&(&1 == nil))
end
end
defmodule Room do
def get_state(room_id) do
registry_room_id = registry_id(room_id)
GenServer.call(registry_room_id, :get_state)
end
end
WIth the above API, RoomService will crash when any of the calls to the Room.get_state crashes which is unwanted behavior.
RoomService cannot wrap Room.get_state into try catch as it is not aware of get_state internal implementation.
We could use try catch inside get_state like:
def get_state(room_id) do
registry_room_id = registry_id(room_id)
try do
GenServer.call(registry_room_id, :get_state)
catch
:exit, {:noproc, {GenServer, :call, [^registry_room_id, :get_state, _timeout]}} ->
Logger.warning(
"Cannot get state of #{inspect(room_id)}, the room's process doesn't exist anymore"
)
nil
end
end
but this effectively means that every single GenServer.call should be wrapped into try catch.
I feel like I am missing something ![]()
Marked As Solved
lud
You can search github for safe_call and you will find a buch of results. There may be lots of other names for that, for instance in the brod library it’s called safe_gen_call.
As a general rule of thumb, in a library that deals with processes, you may want to do that try/catch directly in library code if the process not being alive is frequent and desirable. And in all other cases do not wrap the call because when you are calling a process in general it should be there.
A common use case that comes to mind is dealing with transient processes, or timeout-then-stop, because calling Process.alive? before doing a call has an inherent race condition. So you just do the call, and if it exits with :noproc you start the process and try again.
So no you should not default to wrap everything, if the use case arises it’s generally pretty obvious.
Also Liked
LostKobrakai
Yes we should expect processes to crash. How we react on a process not being unavailable is the point of discussion though. The option choosen for GenServer.call and (probably way) earlier for :gen_server.call is to exit if the process is not available. I’d imagine the reasoning being that the calling process requires a response to the call and if that cannot be retrieved all code depending on the response likely shouldn’t run as well. Choosing how to handle (such) errors is a tradeoff for sure, but I don’t think this one is entirely unreasonable.
The idea of supervisors and OTPs fault tolerance is not that things always have fallbacks (I’d even say to the contrary), but rather that failures only affect the users, which run into a fault, but not others. For this case this means the unlucky user trying to call a crashed process will crash as well, but any user not needing to interact with that missing process is fine. Once the process is back up the system is healed and everything works again.
Another thing you can look into is the async alternative to doing a call (send_request, wait_response, …) introduced in OTP 23, which doesn’t fail if a process is not alive, but returns an error tuple on trying to receive the response. That one is not yet in mirrored in elixir though, but the erlang api should work just fine.
lud
I have been there, and it’s very bad. You will go against the Erlang philosophy of letting things crash sometimes, and will have to handle an infinite amount of possible errors at every layer of the application, in every module, etc. Really bad.
The hard part is to accept that yeah, stuff goes wrong sometimes, and there is not much you can do about it. I see two cases, 1. the code will fail 50% of the time. In that case isolate in processes and/or use try/rescue/catch. 2. the code is not expected to fail, failures are exceptional. Then do nothing, let something else retry, it can be the frontend, an Oban job, a Task… just let your supervisor handle that and you can focus on implementing features.
adw632
Yes any code can crash because of a missing process, a dead node or a code error such as divide by zero. That’s ok, you either deal with it at design time because you expect it and know EXACTLY how to handle it, or you don’t really know about the possibility or how to handle something unexpected so the right thing to do is to “let it crash” and defer to policy decided in the layer(s) above to recover e.g. through the supervision tree.
In Elixir our code can be highly concurrent, perhaps down deep in your library code is using Task.async_stream to process a stream of items concurrently. Any error that occurs in the spawned task (e.g. due to bad data your function doesn’t handle) should crash the calling process, because you really don’t expect it to fail.
If you are coming from a static typed exception heavy programming language, you are unreasonably forced to declare all possible exceptions and code defensively and exhaustively and try and handle things you can’t reasonably do much about because you don’t have a process model like Elixir to recover and cleanup state when the happy path is violated, such as when a process dies that you expected to be alive. In these other defensive paradigms your code has to do all the work to detect and cleanup, so you write even more code to try and handle all eventualities because you MUST keep the global shared state consistent. Of course, incorrect programs with even more code than the necessary happy path can’t reasonably be expected to correctly handle all eventualities, more code means more bugs.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance










