Refactoring Process Identifiers
I’m thinking about where in the startup flow for game sessions to create database records which will be used to track active games. There are scenarios where existing game sessions will go through process initialization more than once such as rolling deployments where active game processes need to shut down and start up again on another node. If the database records for each game are created in the GenServer initialization, I’ll need to account for both cases of new games and existing games.
One way I can handle both cases is to perform an “upsert” operation which would create a new record if the game session didn’t already exist or it would update the existing record if one exists.
However, there is currently no unique identifier for game sessions to determine if a record should be created or updated.
The game_id
field is only unique to the active game session GenServer processes so I will need to make adjustments to ensure uniqueness to the database table rows.
Separating responsibilities
I recognize that game_id
is used for two different purposes.
This id is a short code which users use to connect to the game LiveView as well as an internal identier for game session processses.
Once the LiveView is mounted, the same game_id
is used to send commands to the session process which is registered with to the Registry
module with this id.
If I changed game_id
to a UUID, users would no longer have a short code to easily join games.
To preserve this short code feature, I will have to make a separate id for this purpose and only use game_id
as a unique identifier.
The first thought I had is to use an ETS lookup table to map a short code to a game id.
As I poke around my code for where to make this change, I wonder if I can use a second Registry
to map short codes to the same game session process.
ETS has a faster lookup times over Registry, but this short code mapping should only be used during the initial LiveView mount so this benefit won’t have much of an impact.
One of the advantages of using another registry instead of ETS is automatic cleanup of registrations when a process terminates.
I am also already using a distributed registry with Horde.Registry
which will automatically replicate mapping state across cluster nodes during deployments.
I choose to use a second Registry and see where that gets me.
Implementing join_code
Game processes are started by calling GameEngine.create_game/2
or GameEngine.continue_game/2
which both take game_id
as their first argument.
I update these functions to rename this argument as join_code
.
I also update every place that calls these functions to define a join_code
which is equal to the same value as the game id.
Once the join code feature is complete, these two variables will hold distinct values.
create_game
and continue_game
are responsible for starting SessionServer
game processes as children of GameEngine.SessionSupervisor
.
I update the game SessionServer
child spec to take a join_code
keyword argument in addition to the game state.
I pass join_code
as a :start_link
keyword argument and update the start_link
function to accommodate this change.
defmodule Minotaur.GameEngine do
# ...
def continue_game(join_code, %Game{} = game) do
spec = SessionServer.child_spec(join_code: join_code, game_state: game)
Horde.DynamicSupervisor.start_child(SessionSupervisor, spec)
end
end
defmodule Minotaur.GameEngine.SessionSupervisor do
def child_spec(join_code: join_code, game_state: game) do
%{
id: "#{__MODULE__}_#{join_code}",
start: {__MODULE__, :start_link, [[join_code: join_code, game_state: game]]}
}
end
def start_link(join_code: join_code, game_state: game) do
name = {:via, Horde.Registry, {SessionRegistry, game.id}}
GenServer.start_link(__MODULE__, {game, join_code}, name: name)
end
# ...
end
The GenServer state for SessionServer
is currently just a Game
struct which does not have a join_code
.
Since join_code
is only used for mapping processes, I am hesitant to add join_code
to the Game
struct definition.
I instead update the GenServer state to a tuple with {game, join_code}
to track the code separate from the game state.
I apply this state structure change to all SessionServer
callbacks.
defmodule Minotaur.GameEngine.SessionSupervisor do
# ...
def init({%Game{} = game, join_code}) do
Process.flag(:trap_exit, true)
game_state = get_stashed_state(game.id, game)
{:ok, {game_state, join_code}, {:continue, :continue_game}}
end
end
The next steps will be to update the game session process lookup logic to use join_code
instead of game_id
inside the game LiveView.