Exit reasons in Liveview

Hi!

I have some doubts about this documentation:

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:terminate/2

I was trying to catch when / why every exit reason is triggered to understand the possible situations where some socket is disconnected and make some decisions.

From what I’ve read on the documentation there are 5 possible exit statuses:

  • :normal
  • :shutdown
  • {:shutdown, :left}
  • {:shutdown, :closed}
  • term()

I can’t figure out when :normal or {:shutdown, :left} will be called. Could the documentation be outdated? Probably I’m missing something.

These are some tests I did:

Notice: Just in case, I have trap exit enable on liveview server

Results:

|          Action         |         Reason       |
|-------------------------|----------------------|
|     push_redirect       |        :shutdown     | 
|.       redirect         |        :shutdown     | 
|     kill lv process     |        :shutdown     |
|   close tab / browser   | {:shutdown, :closed} | 
|        change URL       | {:shutdown, :closed} | 
|         refresh         | {:shutdown, :closed} | 
|disconnect socket with JS| {:shutdown, :closed} | 
|       raise error       |          term()      | 

Method used to kill liveview process
Process.whereis(QtixWeb.Endpoint) |> Process.exit(:kill)

I’m not able to get {:shutdown, :left} not :normal

I found this peace of code on liveview project:

def handle_info(%Message{topic: topic, event: "phx_leave"} = msg, %{topic: topic} = state) do
    send(state.socket.transport_pid, {:socket_close, self(), {:shutdown, :left}})
    reply(state, msg.ref, :ok, %{})
    {:stop, {:shutdown, :left}, state}
end

So I think it is something related with the Phoenix.socket, am I right?

On the other hand, I couldn’t find the way to get the :normal reason neither.

Once I have this clear, how possible is it to improve the documentation explaining every case? Is there a standard for keeping the documentation so minimalist?

Thanks!

Did you try closing the socket via the API from the JS on the frontend?

Call exit(:normal) from your code.

Yes, exit reasons are a well defined set:
https://hexdocs.pm/elixir/Kernel.html#exit/1-otp-exits

Given LVs run user supplied code any of those could be triggered by the user defined code, so there’s no possibility to limit this set further.

The only thing LV could be doing it document the reasons behind the ones it controls specifically, but then again any user provided code could hijack the same reason and use it under different circumstances.

I mean in the specific liveview context. I think is good to know when we will get each one, if I would like to know what’s the exit if people close the tab we have to make tests and experiments, that could be explained, I’ve no problem in improve the doc in any case it’s possible.

This seems forced (hijacked as you said), what’s the natural way when I will get this? I can run exit(:foo) but the :foo exit is not in the documentation. I assume that if :normal is on the documentation there is some organic reason that returns it.

Yes, I ran liveSocket.disconnect(); from chrome console and got {:shutdown, :closed}

I will add this last test to the table too.

Thanks!

User code calling exit(:normal) is at least one of the expected paths to get this exit reason. Yes you can call exit with any term, but OTP behaviours react in certain ways according to those specific exit reasons listed. Like a supervised process with restart strategy :transient would only restart on an abnormal (not :normal, :shutdown, {:shutdown, term}) exit reason. And again LV (like any otp behaviour) expect the user provided code to potentially call exit/1, so terminate/2 needs to be able to handle all of them (the non-abnormal ones, because abnormal ones don’t call terminate/2).

I agree that it would be useful to know what values LV does exit independently from user provided code, but that’s never going to be the full set terminate/2 needs to handle.

is not terminate/2 called if you enable trapping exit on the process? I intentionally raised an exception on an handle_event/3 and got it as term() in terminate/2, I think…

Update: Now I remember, for having this work the liveview lifecycle has to be started, if I raise some error on the mount/3 everything blow up and terminate/2 is no called at all, you’re right.

Once I had to detect even those cases and used process monitors, but that is another chapter :slight_smile:

The details are listed here: GenServer — Elixir v1.17.2

But given the many reasons for it not being called I’d stick to non-abnormal exit reasons if the intention is to have it be called (and complete).

Yes… The only mystery is when or why the {:shutdown, :left} is triggered… Just in case I will use a wildcard on my pattern-matching and consider it the same as {:shutdown, :closed}, in any case, the shutdown is shutdown. I intend to detect when the exit is voluntary or because of an error to take different actions.

If I have some time I will prepare some socket routes and try interacting directly with the socket, maybe that is the way.