mickel8

mickel8

Membrane Core Team

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 :thinking:

Marked As Solved

lud

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

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

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

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.

Where Next?

Popular in Questions Top

Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: <h1>Create Post</h1> <%= ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
earth10
Hi, I’m just starting to build a side-project with Elixir and Phoenix and doing some basic test with Elixir alone. What strikes me is th...
New

Other popular topics Top

New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43757 214
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54120 245
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement