Getting PID on init for GenServer handle_info & using :timer.send_interval for game engine

We’re currently building a game server in which we’re handling state within GenServer, and we’re also starting a game engine tick function in init using a :timer.set_interval that calls a handle_info.

Within this tick function, we will examine GenServer state and edit it depending on certain conditions, using casts.

We’re passing state to the worker function within handle_info, and within that using the state to lookup the GenServer PID in order to edit state within a specific GenServer.

I feel like there has to be a way to get the PID in init or in the handle_info so we don’t have to keep looking up the process in which we are already running, does anyone know a better practice for doing this?

Any advice appreciated!

The code:

defmodule Games.GameServer do
  # This is a GenServer
  @impl true
  def init(game_id) do
    # game_id = "some-string"
    state = load_game_state(game_id)

    :timer.send_interval(@worker_interval, :work)

    {:ok, state}
  end

  # code omitted
  @spec handle_info(:work, Keyword.t()) :: {:noreply, Keyword.t()}
  def handle_info(:work, state) do
    Engine.run(state)
    {:noreply, state}
  end

  def increment_tick(game_pid) do
    GenServer.cast(game_pid, :increment_tick)
  end

  @impl true
  def handle_cast(:increment_tick, game_state) do
    # Increments an integer on the state
    new_state = GameState.increment_tick(game_state)

    {:noreply, new_state}
  end
end

defmodule Games.Engine do
  def run(state) do
    # Broadcasting the old tick
    Endpoint.broadcast("games:"<> state.name, "timer", state)

    # Getting the PID by running this Registry lookup
    # QUESTION: Is there some way to get the PID within the process?
    game = Games.find_game(state.name)

    # Running the cast to update the tick
    GameServer.increment_tick(game)
  end
end

I may misunderstood what you want, but does you want self/0?

3 Likes

Is there a reason why you have to cast to increment tick?

As a strategy, I would personally architect it like this:

  ...
  def handle_info(:work, state) do
    new_state = Engine.run(state)
    {:noreply, new_state}
  end
end

defmodule Games.Engine do
  def run(state) do
    # do things
     ...
     increment_tick(state)
  end
end
1 Like

We’re running multiple copies of the same GenServer, so my (new-to-GenServer) understanding was that we have to cast to the specific GenServer process in order to “save” the state struct for that process.

Judging from your suggestion, my initial understanding of this is incorrect, and we can just call on the state directly because we are still running in the process?

This is a simplification but you could think of a process as a mini-program that is running in a tight loop

state_0 = result_of_init

state_1 = loop(state_0, new_messages0)
state_2 = loop(state_1, new_messages1)
state_3 = loop(state_2, new_messages2)

And “loop” takes new_messages, unpacks them, and calls your handle_X callbacks as appropriate, depending on the type of the message received.

etc.
When you call any function there is no implicit “static” data or user-accessible stack that is shared between different instances of the function call, regardless of which process the function lives in. Moreover, the loop function itself is “single threaded”, so your handle_x functions are atomic updates to the wrapped state of your process.

1 Like

Many thanks for the insight! I realize that there is no static data shared between different instances of a function call, but I came into using casts because I was doing a lot of state updates in the engine without returning new from run in :work

One of the syntactic conveniences of using casts is I’m able to cast the state and do selective broadcasts of state from within functions called by conditional statements in the engine run (such as entity collision checks) in a composable way, while ignoring the rest of what happens in our :work function. I’ll take a look at refactoring to handle new state and broadcasts there, assuming there may be some performance value

I think this a bad idea. Once you have processes talking to each other, you might get outside messages coming in between your call messages that will result in an unexpected order of processing steps. You’re better off accumulating instructions into a list and processing them atomically before refreshing the state of the process and freeing the process to handle the next incoming message.

1 Like

Oh you may want to look into gen_statem or my library https://hexdocs.pm/state_server/StateServer.html which has the concept of an atomic event list built in.

1 Like