Terminate GenServer after a certain period

I have an application that deals with multiple (N number of) sqlite databases. Based on the id of a resource, I have to locate a specific sqlite file, and start the Sqlitex.Server GenServer with the path of the file on disk, read query and return matching results.

I want to keep the GenServer with the sqlite file in memory for a certain period of time, say 30 minutes, and then let it terminate.

However, I also want to ensure that if a GenServer terminates before that, it comes back up, so I have used the strategy: :one_for_one strategy for that.

I cannot quite figure out how I can set a timeout so that the Sqlitex.Server* GenServer terminates after a certain time.

defmodule Server do
  use GenServer

  def init(_) do
    Process.send_after(self(), :harakiri, 30 * 60 * 60)
    {:ok, []}
  end

  def handle_info(:harakiri, _state) do
    exit(:normal)
    {:noreply, []}
  end
end
7 Likes

Nice naming :rofl:

You might also use GenServer timeout…

It depends if You want to stop after 30 minutes, or after a 30 minutes period of inactivity :slight_smile:

2 Likes

Just a note, this works only if the process timer isn’t being used for anything else, since there’s only one. Otherwise you’ll want to use another gen server to run the set of timers or the erlang timer library. I’ve done this kind of thing before, with sqlite databases even, and I found that the DynamicSupervisor and Registry are very useful in their combination.

Where you get that info because I cannot find anything about that in the documentation and you got me curious there, as erlang:start_timer/4 returns TimerRef which would be pointless in such case.

Could you please elaborate?

Process.send_after/4 is based on :erlang.send_after/4 which is different from timer.send_after/3. Up to now I was under the impression that :erlang.send_after/4 is simply a BEAM based capability in no way limited by the process using/receiving it.

3 Likes

You are quite correct! My apologies and thank you for the clarification.

1 Like

Thanks for the reply!

Does this mean that I would need to include Sqlitex.Server into another GenServer module? As it stands, Sqlitex.Server is already a GenServer.

Thanks.

Sqllitex.Server has stop/1

Consider this standin:

# file: my_app/lib/server.ex
#
defmodule Server do
  use GenServer

  def start_link(args),
    do: GenServer.start_link(__MODULE__, args)

  def init(args),
    do: {:ok, args}

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

  def terminate(reason, state) do
    IO.puts("#{__MODULE__} terminate - reason: #{inspect(reason)} state: #{inspect(state, pretty: true)}")
    :ok
  end

  # ---

  def stop(pid),
    do: GenServer.cast(pid, :stop)

end

and this application:

# file: my_app/lib/my_app/application.ex
#
# created with "mix new my_app --sup"
#
defmodule MyApp.Application do
  use Application

  # @server_timeout 30 * 60 * 1000
  @server_timeout 30_000
  @supervisor_name MyApp.Supervisor

  def start(_type, _args) do
    children = [
      Supervisor.child_spec({Server, [:config, :server]}, restart: :transient)
    ]

    # restart: transient
    # otherwise supervisor will restart Server process

    opts = [strategy: :one_for_one, name: @supervisor_name]

    case Supervisor.start_link(children, opts) do
      {:ok, _} = on_start ->
        Task.start(__MODULE__, :timeout_server, [])
        on_start

      on_start ->
        on_start
    end
  end

  def timeout_server() do
    with {:ok, server_pid} <- find_server() do
      # Terminate this task if server terminates (and vice versa)
      Process.link(server_pid)
      # Now wait
      Process.sleep(@server_timeout)
      # Let server terminate normally
      Process.unlink(server_pid)
      # Initiate server termination
      Server.stop(server_pid)
    end
  end

  def find_server() do
    case Enum.find(Supervisor.which_children(@supervisor_name), &server_module?/1) do
      {_, server_pid, _, _} when is_pid(server_pid) ->
        {:ok, server_pid}

      _ ->
        {:error, :not_found}
    end
  end

  defp server_module?({_, _, _, [Server | _]}),
    do: true

  defp server_module?(_),
    do: false
end

then

$ iex -S mix
Erlang/OTP 22 [erts-10.5.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Compiling 2 files (.ex)
Interactive Elixir (1.9.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> MyApp.Application.find_server()
{:ok, #PID<0.149.0>}
iex(2)> Elixir.Server terminate - reason: :normal state: [:config, :server]
iex(2)> MyApp.Application.find_server()
{:error, :not_found}
iex(3)> 
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a
$ 
1 Like