Genserver global name causing issues when called from LiveView mount upon page refresh

First of all I would like to mention that I would have asked this question in the Phoenix forum but unfortunately I do not have access to make a new topic.

I am new to elixir, LiveView and Phoenix and am making a game as a practice project. I am encountering an issue with my GenServer in the mount of my LiveView.

Basically when a new game is generated with a specific uuid, my app redirects to my LiveView and using the uuid that is sent in the params I check if a GenServer Game with the global name containing that uuid is already created or not. If not it starts the GenServer, if it is already created then it simply gets the state of the game.

The issue I am having is that when I refresh the page when the GenServer has already started it should simply get the game and not start a new one.

However what happens is that because the mount is called twice, the first time it properly gets the game but the second time it starts a new GenServer overriding the previous game state.

My LiveView mount

def mount(%{"uuid" => uuid}, _session, socket) do

    IO.puts("This is the uuid #{uuid}")

    %{current_user: current_user} = socket.assigns

    {my_game_state, pid} =
      case :global.whereis_name({:name, "Catan.Game.State:#{uuid}"}) do
        :undefined ->
          # The GenServer is not running, so you can start it
          {:ok, pid} = Catan.Game.State.start_link(uuid)
          GenServer.call(pid, :get_game)
          game_state = GenServer.call(pid, {:join, current_user})
          IO.puts("It came into the new game creation block")
          {game_state, pid}

        pid ->
          GenServer.call(pid, :get_game)
          game_state = GenServer.call(pid, {:join, current_user})
          IO.puts("It came into the continue game block")
          {game_state, pid}
      end

    IO.inspect(my_game_state, label: "This is the game state")

    # Other code logic below

end

My GenServer

def start_link(uuid) do
    global_name = "Catan.Game.State:#{uuid}"
    GenServer.start_link(__MODULE__, [], name: {:global, {:name, global_name}})
  end

  defstruct [:board, :players, :resources, :cards, :settlements]

  @impl true
  def init(_elements) do
    state = %{
      board: BoardMap.board_map(),
      players: %{},
      turn: 0,
      resources: %{brick: 19, ore: 19, sheep: 19, wheat: 19, wood: 19},
      # cards: Catan.Game.Card.generate()
      settlements: %{}
    }

    {:ok, state}
  end

  @impl true
  def handle_call(:get_game, _from, state) do
    {:reply, state, state}
  end

What I get in my terminal window upon refresh is something like this:

This is the uuid
my uuid displayed here

It came into the continue game block
This works properly

This is the game state:
game state displayed here

On second call of mount

This is the uuid
my uuid displayed here (it is same as above so uuid is not changing)

It came into the new game creation block
should not do this as it now creates a new game and overrides the previous state of the game. it should go into the continue case.

This is the game state:
new game state displayed here as it has overridden the previous game

I do not know why this is happening. It should not start a new GenServer upon page refresh. Any help will be greatly appreciated.

Take a look at the LiveView lifecycle - the call to Catan.Game.State.start_link(uuid) runs twice:

  • once in the “dead” HTTP connection
  • once in the LiveView process once the websocket is open

The process that executes the first call is typically a Cowboy worker - which exits as soon as the response has been sent. That causes the just-started Catan.Game.State process to also exit (because it’s linked).

The usual approach to deal with this sort of thing is to launch processes like Catan.Game.State under a supervisor rather than linked to workers or LiveViews. Check out DynamicSupervisor to get started.


BTW, two general notes about naming:

  • :global will accept any term as a name, so it’s almost always preferred to NOT concatenate binaries etc when creating a name. {Catan.Game.State, uuid} is easier to type and very slightly faster.

  • Many functions like GenServer.call accept names as well as PIDs. You can avoid having to carry pid around everywhere and use GenServer.call({:global, whatever_term_you_named_it}, ...)

2 Likes

A few points:

  • It sounds like you don’t need/want to do anything in the static mount. You can use connected?/1 to limit the look up and game creating at the second and connected mount only.
  • Do you really need distribution (multiple Erlang nodes)? If not, I’d recommend to use Registry instead of :global for lookups
  • calling start_link from the liveView is most likely not what you want; if the live_view exited then the GenServer will be gone as well. You probably should use a DynamicSupervisor to manage the GenServers.
3 Likes

Thank you for the reply. I did not know about DynamicSupervisors and have looked at it. From what I understand I need to set up my Dynamic Supervisor in a separate module which will be something like this:

defmodule Catan.Game.MyDynamicSupervisor do

  children = [
    {DynamicSupervisor, name: Catan.Game.MyDynamicSupervisor}
  ]

  Supervisor.start_link(children, strategy: :one_for_one)

end

And in my LiveView something like

{:ok, pid} = DynamicSupervisor.start_child(Catan.Game.MyDynamicSupervisor, {Catan.Game.State, uuid) # My GenServer module is in Catan.Game.State

Does this seem right?
I assume that with this I can use my case in my mount properly now to check the process? Will that code remain unchanged?

Sorry these questions might be basic but as I mentioned I am new and have not worked with DynamicSupervisors.

Thank you for the reply. I am looking at Dynamic Supervisors now.

I also attempted to use the connected?/1 you mentioned and tried putting my mount code in it but unfortunately it is giving me errors as I have variables that are not available in my render anymore.

I think I do need my static mount to assign the variables to the socket.

My entire mount using connected for your reference is below:

def mount(%{"uuid" => uuid}, _session, socket) do

    if connected?(socket) do
      IO.puts("This is the uuid #{uuid}")

      %{current_user: current_user} = socket.assigns

      {my_game_state, pid} =
        case :global.whereis_name({:name, "Catan.Game.State:#{uuid}"}) do
          :undefined ->
            # The GenServer is not running, so you can start it
            {:ok, pid} = Catan.Game.State.start_link(uuid)
            GenServer.call(pid, :get_game)
            game_state = GenServer.call(pid, {:join, current_user})
            IO.puts("It came into the new game creation block")
            {game_state, pid}

          pid ->
            GenServer.call(pid, :get_game)
            game_state = GenServer.call(pid, {:join, current_user})
            IO.puts("It came into the continue game block")
            {game_state, pid}
        end

      IO.inspect(my_game_state, label: "This is the game state")

      current_player_id = Enum.at(Map.keys(my_game_state.players), my_game_state.turn)
      {:ok, current_player_email} = Map.fetch(my_game_state.players, current_player_id)

      my_turn = current_player_id == current_user.id

      # Presence Tracking Code
      
      Phoenix.PubSub.subscribe(Catan.PubSub, @topic)
      
      {:ok, _} =
        Presence.track(self(), @topic, current_user.id, %{
          username: current_user.email |> String.split("@") |> hd(),
          email: current_user.email,
          turn: current_user.id && my_game_state.turn
        })      

      presences = Presence.list(@topic)

      socket =
        assign(
          socket,
          pid: pid,
          current_user: current_user,
          current_player_id: current_player_id,
          current_player_email: current_player_email,
          my_turn: my_turn,
          turn: my_game_state.turn,
          hexagons: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
          hex_map: my_game_state.board,
          players: players(presences)
        )

      {:ok, socket}
    end
    {:ok, socket}
  end

For example I am using current_player_id and email in my render but it gives me error that :current_player_id not found and more for my other variables.

You’re ignoring all the work you do inside if.

You need to structure it like so

def mount(%{"uuid" => uuid}, _session, socket) do
    if connected?(socket) do
      ...
      {:ok, socket}
    else
      ...
      {:ok, socket}
    end
end

or

def mount(%{"uuid" => uuid}, _session, socket) do
  socket = 
    if connected?(socket) do
      assign(socket, ...)
    else
      socket
    end

  {:ok, socket}
end

I have a minimal chat room project that demonstrate the usage of DynamicSupervisor, GenServer and Registry. It does more or less the same thing you want; creating or connecting to chat rooms on demand at live view mount. You may want to see how I did it.

2 Likes

Thank you for the reply

I made the changes you said and now I am not getting any errors but I am still getting the initial issue. The global name is causing issues when I refresh the page and it starts a new GenServer Process instead of just calling the existing game.

def mount(%{"uuid" => uuid}, _session, socket) do

    socket =
      if connected?(socket) do
        IO.puts("This is the uuid #{uuid}")

        %{current_user: current_user} = socket.assigns

        {my_game_state, pid} =
          case :global.whereis_name({:name, "Catan.Game.State:#{uuid}"}) do
            :undefined ->
              # The GenServer is not running, so you can start it
              {:ok, pid} = Catan.Game.State.start_link(uuid)
              GenServer.call(pid, :get_game)
              game_state = GenServer.call(pid, {:join, current_user})
              IO.puts("It came into the new game creation block")
              {game_state, pid}

            pid ->
              GenServer.call(pid, :get_game)
              game_state = GenServer.call(pid, {:join, current_user})
              IO.puts("It came into the continue game block")
              {game_state, pid}
          end

        IO.inspect(my_game_state, label: "This is the game state")

        current_player_id = Enum.at(Map.keys(my_game_state.players), my_game_state.turn)
        {:ok, current_player_email} = Map.fetch(my_game_state.players, current_player_id)

        my_turn = current_player_id == current_user.id

        # Presence Tracking Code
        Phoenix.PubSub.subscribe(Catan.PubSub, @topic)

        {:ok, _} =
          Presence.track(self(), @topic, current_user.id, %{
            username: current_user.email |> String.split("@") |> hd(),
            email: current_user.email,
            turn: current_user.id && my_game_state.turn
          })

        presences = Presence.list(@topic)

        assign(socket,
          pid: pid,
          current_user: current_user,
          current_player_id: current_player_id,
          current_player_email: current_player_email,
          my_turn: my_turn,
          turn: my_game_state.turn,
          hexagons: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
          hex_map: my_game_state.board,
          players: players(presences)
        )
      else
        %{current_user: current_user} = socket.assigns

        pid = :global.whereis_name({:name, "Catan.Game.State:#{uuid}"})

        GenServer.call(pid, :get_game)
        my_game_state = GenServer.call(pid, {:join, current_user})

        IO.inspect(my_game_state, label: "This is the game state in the else case")

        current_player_id = Enum.at(Map.keys(my_game_state.players), my_game_state.turn)
        {:ok, current_player_email} = Map.fetch(my_game_state.players, current_player_id)

        my_turn = current_player_id == current_user.id

        presences = Presence.list(@topic)

        assign(socket,
          pid: pid,
          current_user: current_user,
          current_player_id: current_player_id,
          current_player_email: current_player_email,
          my_turn: my_turn,
          turn: my_game_state.turn,
          hexagons: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
          hex_map: my_game_state.board,
          players: players(presences)
        )
      end

    {:ok, socket}
  end

Thank you I will have a look at it.

This is not related to the global name, it’s caused by calling start_link in a process that subsequently exits.

The original issue linked the game to the Cowboy worker. The revised version links it to the LiveView process.

Refreshing the page closes the websocket, so the LiveView exits.

2 Likes

Thank you everyone.

I have managed to fix the issue. It was indeed as many of you mentioned a problem with how I was launching processes through my LiveView.

I have set up a DynamicSupervisor and now it is working properly. Special thanks to al2o3cr for clearing up my misunderstanding and derek-zhou for demonstrating the usage of DynamicSupervisor.

1 Like