citruz

citruz

Hi,
I am trying migrate a small game I wrote to LiveView. However, I am stuck since multiple hours and can’t figure out why the GenServer processes I create seem to die instantly.
I have an Agent module called Global which maintains a Map of Game (GenServer) processes:

defmodule Fakeartist.Global do
    use Agent
...
    def games do
        Agent.get(__MODULE__, &(&1))
    end

    def new_game(player_name, player_id, num_rounds) do
        ...
        {:ok, game} = Game.start_link(player_name, player_id, num_rounds)
        Agent.update(__MODULE__, &Map.put_new(&1, token, game))
        {:ok, token, game}
    end
end

Currently, without LIveView, I do this to create a game inside a controller:

defmodule FakeartistWeb.GameController do
...
    def create(conn, %{"user" => %{"num_rounds" => num_rounds}}) do
        ...
        {:ok, token, _} = Global.new_game(username, get_session(conn, :user_id), num_rounds)
        conn
        |> redirect(to: Routes.game_path(conn, :show, token))
    end
end

It works perfectly, I can retrieve the Game process, call functions on it and so in.
Now I tried to implement the same with LiveViews:
index.html.leex:

<button phx-click="addgame">Create</button>
...
    <%= for  {token, game} <- @games do %>
        <td><%= token %></td>
        <td><%= length(Game.get_players(game)) %></td>
    <% end %>

index.ex:

defmodule FakeartistWeb.GameLive.Index do
...
 @impl true
    def mount(_params, _session, socket) do
      socket = socket
      |> assign(:games, fetch_games())
      {:ok, socket}
    end
  @impl true
    def handle_event("addgame", params, socket) do
      IO.puts("addgame: #{inspect params}")
      {:ok, token, game} = Global.new_game("some_username", "some_user_id", 2)
      IO.puts("addgame: #{inspect Game.props(game)}")
      IO.puts("addgame: #{inspect Global.games[token]}")

      socket = socket 
      |> assign(:games, fetch_games())
      |> push_redirect(to: "/livegame")
      {:noreply, socket}
    end
    defp fetch_games do
      Global.games()
    end

As soon as I click the button, the new Game process is created which can observed in the log:

addgame: %{<...>}
addgame: %{category: :none, current_player: :none, <...>}
addgame: #PID<0.473.0>

This shows the pid and that I am able to call methods on it. However, as soon as the view tries to display the game list, it crashes:

** (stop) exited in: GenServer.call(#PID<0.473.0>, :get_players, 5000)
    ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started

The game was added to the map in Global, but the process that the map entry is referring to and which was still running in the handle_event call seems to have died now.
I have no clue why, is there something I am missing?

I appreciate any help. I tried to reduce the code as much as possible, hope its understandable.

Thanks
Felix

Showing Posts 1 to 6

mindok

mindok

Have you tried debugging the lifecycle of the game process - e.g. using :sys.trace/3 (see GenServer — Elixir v1.20.2) or implementing the terminate callback?

citruz

citruz OP

I already tried the latter, adding a terminate callback with a debug message, but it was not called.
The same seems to be true for :sys.trace/3 - I can see the props call but nothing more:

*DBG* <0.461.0> got call props from <0.456.0>
*DBG* <0.461.0> sent #{category => none,current_player => none,
                    ...
addgame: %{category: :none, current_player: :none,...
addgame: #PID<0.461.0>
[info] GET /livegame
[debug] Processing with Phoenix.LiveView.Plug.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 551µs
[error] GenServer #PID<0.466.0> terminating
** (stop) exited in: GenServer.call(#PID<0.461.0>, :get_players, 5000)
    ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started

I am new to elixir, am I missing something fundamental about the lifecycle or scope of processes? Are LiveViews in any way different to regular view controllers?

The full code is here https://github.com/citruz/fakeartist, in case that helps.

ityonemo

ityonemo

You’re missing a whole lot about the lifecycle of processes. For starters, you’re calling Game.start_link inside your liveview, which is unusual. Typically start_link should be called by supervisors, you’ll want your games to be supervised, as supervisors are process lifecycle managers. As your code stands, the lifecycle of your game is directly tied to the lifecycle of your liveview (hence the _link). If your lv dies, so does your game.

Also instead of an agent, you probably want to use a registry to keep track of processes with ids that are meaningful (aren’t pids).

citruz

citruz OP

Thanks, I was suspecting something like that. Please help me understand what is wrong with my current approach.
I added a Supervisor child in application.ex:

def start(_type, _args) do
    children = [
     ...
      Fakeartist.Global
    ]
    opts = [strategy: :one_for_one, name: Fakeartist.Supervisor]
    Supervisor.start_link(children, opts)
  end

I though that this would create a “global singleton” of my Global Agent which would persist throughout the whole lifetime of the application. By using Game.start_link in Global.new_game I assumed that the lifetime of the Game process would be bound to the Global process and thus to the application.
What did I get wrong?

citruz

citruz OP

Ah, I see. Game.start_link is called inside Global.new_game but is not called in the context of that process and thus the Game process was bound to the wrong parent.
I refactored Global into a named GenServer and not its working :slight_smile:
Next, I will try to understand the concept of registries… What is the problem with passing around pids?

mindok

mindok

No problem with passing round pids per se, but using a registry to lookup processes based on a key that has meaning to your application is so much easier. I found the initial setup a bit of a pain tbh, but in use it makes the code a lot easier to follow. Here’s the “recipe” I ended up with. I’m still missing a couple of pieces - for some reason my registered processes keep running even though the start_link for each “game” is called from a liveview process, but I think I should really have another level of supervision in there as per the supervision tree towards the end of The Erlangelist - To spawn, or not to spawn?.

Anyway…

  1. In application.ex, start a named registry process. The name is an atom that gets used everywhere else:
children = [
      #You use the value assigned to name everywhere else
      {Registry, keys: :unique, name: :game_registry}, 
    ...]
  1. In your Game GenServer module create a via_tuple function that packages your meaningful key into a token that Registry can use to lookup processes. You will need the same name you used in step 1. You can call the function whatever you want, but everyone seems to use via_tuple
  defp via_tuple(game_token) do
    {:via, Registry, {:game_registry, game_token}}
  end
  1. In you create_game function, initialise the process and register with the registry using the token
  def create_game(game_token) do
    GenServer.start_link(__MODULE__, <init args as before>, via_tuple(game_token))
  end
  1. Implement your API functions using the “meaningful” key to look things up
  def do_some_stuff(game_token, stuff_to_do) do
   GenServer.call(via_tuple(game_token), {:do_stuff, stuff_to_do})
  end
— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
psy-q
I’m trying to set up Emacs with elixir-ls via lsp-mode and credo via Flycheck. This should mostly be preconfigured as Flycheck picks up c...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews