Restarting a linked process without a supervisor

I’m implementing a game server that tracks questions and answers. Once the game is started, a timer starts running, and after it’s done, the game process will exit. A user can also stop the game before the timer is done. Since I already had the game server implemented for tracking the game state and the module was getting pretty complex, I’ve added the timer as a separate GenServer.

defmodule Qotd.Games.GameSession do
  use GenServer

  # skipped most of the module and GenServer and Supervisor glue code for brevity

  def handle_call(:start, _from, game) do
    game = Game.start(game)
    {:ok, _} = start_timer()
    {:reply, {:ok, game}, game}
  end

  def handle_info({:EXIT, _from, :normal}, game) do
    game = Game.close(game)
    {:stop, :normal, game}
  end

  def handle_info({:EXIT, _from, reason}, game) do
    {:stop, reason, game}
  end

  defp start_timer(game) do
    Process.flag(:trap_exit, true)
    Timer.start_link(id: {__MODULE__, game.id}, ends_at: timer_ends_at())
  end
end

defmodule Qotd.Timer do
  use GenServer

  alias Qotd.Timer.Timer

  defp via(id), do: {:via, Registry, {Qotd.Registry.Timer, id}}

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: via(opts[:id]))
  end

  @impl true
  def init(opts) do
    timer = Timer.new(opts[:ends_at])
    ref = :timer.send_interval(opts[:interval], :tick)
    {:ok, {ref, timer}}
  end

  def handle_info(:tick, {ref, timer}) do
    tick({ref, timer})
  end

  @impl true
  def handle_call(:check, _from, {ref, timer}) do
    {:reply, timer, {ref, timer}}
  end

  def handle_call(:check, _from, {ref, timer}) do
    {:reply, timer, {ref, timer}}
  end

  def handle_call(:stop, _from, state) do
    {:stop, :normal, state}
  end

  defp tick({ref, timer}) do
    case Timer.tick(timer) do
      %{remaining: 0} = timer ->
        {:stop, :normal, timer}
      timer ->
        {:noreply, {ref, timer}}
    end
  end
end

My question is: is this a reasonable approach for running the timer? And if it is, how would I handle the timer process crashing without also killing the game session? Ideally, I would like to restart the timer several times before giving up.

A good OTP way of doing it would be to have the timer not being linked to the game process but to a dedicated supervisor just for the timers. And you can then have yet another (3rd) process that monitors the game process, depending on your requirements.

1 Like

Beware this kind of pattern with that kind of motivation - GenServers are a bad fit for code-organization duties.

For instance, putting the Timer in a separate process means that any call to the :check handler could crash if it arrives in the mailbox after the :tick message that stops the timer!

An alternative approach would be keeping the implementation code separate (treat the {ref, timer} tuple as an opaque thing in GameSession) but the implementation runtime together (invoke plain functions in GameSession to interact with the timer data).