Multiplayer Go with Elixir's Registry, PubSub and dynamic supervisors
Tags: elixir, phoenix, beginners
Published: 2019-08-13 10:16:22

---
title: "Multiplayer Go with Elixir's Registry, PubSub and dynamic supervisors"
layout: layout_elixir_alchemy
description: "In part 3 of building the Go Game in Elixir, we'll allow the creation of new games as well as inviting others to join in. Along the way, we touch on Elixir's Registry, PubSub and dynamic supervisors."
tags: elixir, phoenix, beginners
cover_image: https://blog.appsignal.com/images/blog/2019-08/go-multiplayer.jpg
published: true
canonical_url: https://blog.appsignal.com/2019/08/13/elixir-alchemy-multiplayer-go-with-registry-pubsub-and-dynamic-supervisors.html
---

Welcome back to Elixir Alchemy, and to the third part in our series on implementing the Go game using Phoenix LiveView. In [part one](https://blog.appsignal.com/2019/06/18/elixir-alchemy-building-go-with-phoenix-live-view.html), we set up the game using LiveView and in [part two](https://blog.appsignal.com/2019/07/04/elixir-alchemy-building-go-in-elixir-time-travel-and-the-ko-rule.html), we added Game history and implemented the ko rule.

Currently, our game is a local "hot seat" multiplayer game where players take turns on the same browser window. Our next adventure aims to turn the game into a true multiplayer experience by allowing players to play with others online.

In this article, we'll take the first step towards that goal. We'll allow the creation of new games as well as inviting others to join in by turning the Game struct into a dynamically supervised GenServer, we'll keep track of games using Elixir's Registry, we'll allow players to connect to already started games, and we'll broadcast moves to all connected players using `Phoenix.PubSub`.

## Where We Left Off

The [starter app](https://github.com/jeffkreeftmeijer/hayago/tree/starter-3) for this episode is where we left off last time. We have an implementation of the game with buttons to undo and redo moves.

The [final result](https://hayago.herokuapp.com) can be played online, and the code for the completed application can be found in the repository's [master branch](https://github.com/jeffkreeftmeijer/hayago/tree/master) (if you prefer to jump straight into the code).

We have a lot to do, so let's get started!

## Dynamically Supervised GenServers

Currently, the game's state is kept in the player's socket connection. This works for games with a single player, but there's no way for another socket connection to access an existing game. To allow two players to play the game together over two socket connections, we need to store each started game in a separate process that both can access.

We'll use a dynamically supervised GenServer to keep each game's state. The dynamic supervisor allows players to start games and it also automatically restarts them when a game process crashes.

First, let's turn our Game module into a GenServer to allow multiple processes to access it.

``` elixir
# lib/hayago/game.ex
defmodule Hayago.Game do
  # ...
  use GenServer

  def start_link(options) do
    GenServer.start_link(__MODULE__, %Game{}, options)
  end

  @impl true
  def init(game) do
    {:ok, game}
  end

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

  @impl true
  def handle_cast({:place, position}, game) do
    {:noreply, Game.place(game, position)}
  end

  @impl true
  def handle_cast({:jump, destination}, game) do
    {:noreply, Game.jump(game, destination)}
  end

  # ...
end
```

Our GenServer handles the `:game` call, which returns the current game's `Game` struct. The `{:place, position}` and `{:jump, destination}` cast callbacks update the game by calling `Game.place/2` and `Game.jump/2`, respectively.

With our GenServer callbacks in place, we can spawn a process that keeps a game's state. To supervise these processes, we'll use [Elixir's `DynamicSupervisor`](https://hexdocs.pm/elixir/master/DynamicSupervisor.html), which allows spawning children on demand. We'll use it to start a new game when a player opens the page.

``` elixir
# lib/hayago/application.ex
defmodule Hayago.Application do
  # ...

  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      HayagoWeb.Endpoint,
      {DynamicSupervisor, strategy: :one_for_one, name: Hayago.GameSupervisor}
    ]

    opts = [strategy: :one_for_one, name: Hayago.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # ...
end
```

We'll set up our dynamic supervisor in the app's `Application` module. The `:one_for_one` strategy (which is the only one available for dynamic supervisors) ensures that a game is restarted whenever it crashes.

## Pids, Atoms and the Registry

Because our Game module now implements GenServer callbacks, we can spawn a Game through the GenServer module.

    % iex -S mix
    iex(1)> {:ok, pid} = DynamicSupervisor.start_child(Hayago.GameSupervisor, Hayago.Game)
    {:ok, #PID<0.286.0>}
    iex(2)> GenServer.cast(pid, {:place, 0})
    :ok

The returned pid is used to get and update the game's state. In this case, we use it to place a stone on the top-left position of the board.

Instead of keeping the game in the socket for every connection, we could add the pid to the socket and then request and update its state through that.

However, our supervisor is tasked with restarting any game process that crashes. When that happens, the game is restarted in a new process with a new pid. When that happens, the players will still be disconnected from the game because the only reference they have to the game will no longer be working, and they'll have no way of getting the new pid.

    iex(3)> Process.exit(pid, :kill)
    true
    iex(4)> Process.alive?(pid)
    false

A solution to this is naming processes when they're spawned so that we can refer to them by name. Whenever a named process is restarted by a supervisor, the newly started process automatically receives the name of the process it replaces.

    iex(5)> {:ok, pid} = DynamicSupervisor.start_child(Hayago.GameSupervisor, {Hayago.Game, name: :game_1})
    {:ok, #PID<0.285.0>}
    iex(6)> Process.whereis(:game_1)
    #PID<0.285.0>
    iex(7)> Process.exit(pid, :kill)
    true
    iex(8)> Process.whereis(:game_1)
    #PID<0.288.0>
    iex(9)> GenServer.cast(pid, {:place, 0})
    :ok

Here, we start a new game and use `:game_1` as its name. If we kill the game process, another one is automatically spawned to replace it. The new process shares the same name, so we can continue using `:game_1` to refer to the new process.

This way, we could generate a unique name for each process when spawning it, and keep that in our socket to refer to the game later. But, wait! We're naming our processes with atoms here. Since atoms aren't garbage collected, spawning a lot of games can exhaust our memory. Instead, we'd like to use strings, which *are* garbage collected.

Because we can't name a process with a string, we need to use a [Registry](https://hexdocs.pm/elixir/master/Registry.html) to link string names to game pids. Elixir's GenServer implementation has a built-in way of referring to processes in a registry through it's `:via`-tuples. First, let's start the registry in our main supervisor whenever the application starts.

``` elixir
# lib/hayago/application.ex
defmodule Hayago.Application do
  # ...

  def start(_type, _args) do
    children = [
      HayagoWeb.Endpoint,
      {Registry, keys: :unique, name: Hayago.GameRegistry},
      {DynamicSupervisor, strategy: :one_for_one, name: Hayago.GameSupervisor}
    ]

    opts = [strategy: :one_for_one, name: Hayago.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # ...
end
```

With the new registry in place, we can start processes using a string as their name, and refer to them later without creating an atom for every spawned game.

    iex(1)> {:ok, pid} = DynamicSupervisor.start_child(Hayago.GameSupervisor, {Hayago.Game, name: {:via, Registry, {Hayago.GameRegistry, "game_1"}}})
    {:ok, #PID<0.294.0>}
    iex(2)> Process.exit(pid, :kill)
    iex(3)> GenServer.cast({:via, Registry, {Hayago.GameRegistry, "game_1"}}, {:place, 0})
    :ok

## Switching to Game Processes

Instead of creating a new game struct and assigning it directly to the socket, we'll update the `GameLive.mount/2` function to start a supervised process that holds a game's state.

``` elixir
# lib/hayago_web/live/game_live.ex
defmodule HayagoWeb.GameLive do
  # ...

  def mount(_session, socket) do
    name =
      ?a..?z
      |> Enum.take_random(6)
      |> List.to_string()

    {:ok, _pid} =
      DynamicSupervisor.start_child(Hayago.GameSupervisor, {Game, name: via_tuple(name)})

    {:ok, assign_game(socket, name)}
  end

  # ...

  defp via_tuple(name) do
    {:via, Registry, {Hayago.GameRegistry, name}}
  end

  defp assign_game(socket, name) do
    socket
    |> assign(name: name)
    |> assign_game()
  end

  defp assign_game(%{assigns: %{name: name}} = socket) do
    game = GenServer.call(via_tuple(name), :game)
    assign(socket, game: game, state: Game.state(game))
  end
end
```

We create a random six-character string as the name of our process, which we use when spawning the process through our `GameSupervisor`. The `via_tuple/1` convenience function returns the `:via`-tuple that we'll need to register the process' name in the registry.

Finally, we add an `assign_game/1-2` function that takes a socket of the game's registered name. It calls out to the GenServer process to get the game's current state before assigning it to the socket.

Next, we'll switch both `handle_event/3` variants to use the game via the GenServer.

``` elixir
# lib/hayago_web/live/game_live.ex
defmodule HayagoWeb.GameLive do
  # ...

  def handle_event("place", index, %{assigns: %{name: name}} = socket) do
    :ok = GenServer.cast(via_tuple(name), {:place, String.to_integer(index)})
    {:noreply, assign_game(socket)}
  end

  def handle_event("jump", destination, %{assigns: %{name: name}} = socket) do
    :ok = GenServer.cast(via_tuple(name), {:jump, String.to_integer(destination)})
    {:noreply, assign_game(socket)}
  end

  # ...
end
```

Again, we use the `via_tuple/1` function to cast both our `:place` and `:jump` functions directly on the game's process. We then reassign the game to the socket by calling our `assign_game/1` function.

## Multiplayer URL Sharing

To allow multiple players to connect to the same game, we'll add the game's name to a URL that the first user can share. When a user starts a new game, we'll use the `pushState` function from the HTML5 history API to add the game's name to the URL without reloading the page.

We'll use Phoenix LiveView's `live_redirect/2` function for that⁠—which was added recently after we started working on the game. To make sure we're on a recent enough version, let's update the dependency before continuing.

    % mix deps.update phoenix_live_view

When a player visits the game without a game name in the URL, the app will start a new one and update the URL to include the newly created game's name. In our case, the game is served on the root of our application, so visiting [http://localhost:4000](http://localhost:4000) will redirect to [http://localhost:4000?name=abcdef](http://localhost:4000?name=abcdef), where "abcdef" is the game's name.

To do this, we'll replace our `mount/2` function with two variants of the `handle_params/3` function.

``` elixir
# lib/hayago_web/live/game_live.ex
defmodule HayagoWeb.GameLive do
  # ...

  def handle_params(%{"name" => name} = _params, _uri, socket) do
    {:noreply, assign_game(socket, name)}
  end

  def handle_params(_params, _uri, socket) do
    name =
      ?a..?z
      |> Enum.take_random(6)
      |> List.to_string()

    {:ok, _pid} =
      DynamicSupervisor.start_child(Hayago.GameSupervisor, {Game, name: via_tuple(name)})

    {:ok,
    live_redirect(
      socket,
      to: HayagoWeb.Router.Helpers.live_path(socket, HayagoWeb.GameLive, name: name)
    )}
  end

  # ...
end
```

The first variant handles requests that have a name in the URL parameters. In that case, the latest game state is fetched from the process corresponding to the name from the parameters and assigned to the socket using the `assign_game/2` function.

The second is a lot like the `mount/2` function that we're replacing. We're generating a name and starting a game. However, instead of assigning the game to the socket and returning, this function uses LiveView's `live_redirect/2` function to add the name to the current URL. After the URL changes, the first variant is automatically executed, assigning the name and game state to the socket.

## Broadcasting Moves to All Clients

If we open our game right now, we'll see that every visit to [http://localhost:4000](http://localhost:4000) gets redirected to a URL with a name query parameter. Opening that URL twice connects two sockets to the same game.

However, when a stone is placed in one of the windows, it doesn't automatically appear in the other one. Only after refreshing the window do we see the correct placement of stones.

Everything is wired up correctly, but the second socket isn't getting notified when the first makes a move. We need a way to get all connected sockets to subscribe to a system that publishes updates to all clients when one makes a move.

We'll use `Phoenix.PubSub` to make that happen. Back in our `GameLive` module, we'll subscribe each socket connection to a channel when a game is created, and then we'll broadcast a message over that channel whenever we place a stone or travel in history.

``` elixir
# lib/hayago_web/live/game_live.ex
defmodule HayagoWeb.GameLive do
  # ...

  def handle_params(%{"name" => name} = _params, _uri, socket) do
    :ok = Phoenix.PubSub.subscribe(Hayago.PubSub, name)
    {:noreply, assign_game(socket, name)}
  end

  # ...

  def handle_event("place", index, %{assigns: %{name: name}} = socket) do
    :ok = GenServer.cast(via_tuple(name), {:place, String.to_integer(index)})
    :ok = Phoenix.PubSub.broadcast(Hayago.PubSub, name, :update)
    {:noreply, assign_game(socket)}
  end

  def handle_event("jump", destination, %{assigns: %{name: name}} = socket) do
    :ok = GenServer.cast(via_tuple(name), {:jump, String.to_integer(destination)})
    :ok = Phoenix.PubSub.broadcast(Hayago.PubSub, name, :update)
    {:noreply, assign_game(socket)}
  end

  def handle_info(:update, socket) do
    {:noreply, assign_game(socket)}
  end

  # ...
end
```

In `handle_params/3` (the variant that matches on URLs with name query parameters), we call out to `Phoenix.PubSub.subscribe/2`. We use the game's name as the channel's topic. Because all connected clients will hit this function, we know they'll all be subscribed to this topic.

In both `handle_event/3` functions, we use `Phoenix.PubSub.broadcast/3` to send a message to all subscribers in the topic. We send `:update` as the message, which is then picked up in a newly added `handle_info/2` function, which fetches the current Game's state and updates the view whenever it receives an update message.

To make sure games are cleaned up, we need to terminate their processes when they're no longer being used. For now, we'll add a timeout to the GenServer's callback functions. When a game hasn't had any interaction for ten minutes, the process will send itself a `:timeout` message to stop the process.

``` elixir
# lib/hayago/game.ex
defmodule Hayago.Game do
  # ...
  use GenServer, restart: :transient

  @timeout 600_000

  def start_link(options) do
    GenServer.start_link(__MODULE__, %Game{}, options)
  end

  @impl true
  def init(game) do
    {:ok, game, @timeout}
  end

  @impl true
  def handle_call(:game, _from, game) do
    {:reply, game, game, @timeout}
  end

  @impl true
  def handle_cast({:place, position}, game) do
    {:noreply, Game.place(game, position), @timeout}
  end

  @impl true
  def handle_cast({:jump, destination}, game) do
    {:noreply, Game.jump(game, destination), @timeout}
  end

  @impl true
  def handle_info(:timeout, game) do
    {:stop, :normal, game}
  end

  # ...
end
```

By adding a timeout in milliseconds to every callback's response tuple, we tell the GenServer to send itself a timeout message when the process hasn't received a message for ten minutes. The `:timeout` callback returns a `:stop`-tuple to tell the process to stop.

We also make sure to set the `:restart` value to `:transient` when including the GenServer code to ensure that the supervisor only restarts the game when it terminates abnormally.

## What's Next?

This concludes the third part on implementing the Go game in Phoenix. We've come quite a long way in implementing a true multiplayer game, and we've learned about Elixir's dynamic supervisors, the Registry, and `Phoenix.PubSub` along the way.

In a future episode, we'll continue turning our game into a multiplayer game by assigning each connection as a separate player by assigning each of them a color of stones on the board. See you then!