Terminate vs destruct?

I have been studying GenServers and I have I hope a simple question for a simple use case (I hope!)

When I start a new process, I open a file using File.open during the GenServer init. I would like to call File.close when the process exits. In my mind, I thought maybe this would be like running code during object construction and destruction, but I am not understanding the how and when GenServer.terminate works.

My module has

def terminate(reason, state) do
    Logger.debug("Terminat")
end

and I am testing it in iex, but I never see this message. I also read about Process.flag(:trap_exit, true) but it does not seem to make a difference.

Can someone explain how this works? Or maybe how I should be thinking about it?

Thank you!

As the docs say, don’t rely on it beeing called, us separate processes and monitors/link for clean up:

https://hexdocs.pm/elixir/GenServer.html#c:terminate/2

Thanks, but I am not sure how to do that. I’m sorry, but I cannot follow the docs.

Cool, it is a fascinating topic.

It’s not usual to work with locks in the BEAM.

Don’t, instead use the handle_continue pattern with a long initialization.

It’s more like actors, in an actors model world.

As mentioned, do not count on terminate to do clean up, instead let somebody else do the cleaning for You (Another process in charge, linked or monitoring the process).

Also… if You don’t use variables, You should prepend them with _

Supervisors are specific genservers, linked to children, but trapping exit.

You should show us how You start and stop your genserver…

Here is a super simple genserver…

defmodule Demo do
  use GenServer
  require Logger

  def start_link(args \\ %{}),
    do: GenServer.start_link(__MODULE__, args, name: __MODULE__)

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

  @impl GenServer
  def init(args) do
    Logger.info("#{__MODULE__} is starting with args #{inspect args}")
    {:ok, args, {:continue, :long_init}}
  end

  @impl GenServer
  def handle_continue(:long_init, state) do
    # do long stuff...
    {:noreply, state}
  end

  @impl GenServer
  def handle_cast(:stop, state), do: {:stop, :normal, state}

  @impl GenServer
  def terminate(reason, _state) do
    Logger.info("#{__MODULE__} stopped : #{inspect(reason)}")
    :ok
  end
end

and how to use it.

iex(1)> Demo.start_link
[info] Elixir.Demo is starting with args %{}
{:ok, #PID<0.879.0>}
iex(2)> Demo.stop      
[info] Elixir.Demo stopped : :normal
:ok

Thank you @kokolegorille for your GenServer example!

Ha ha I am sorry I do not know what this is like. Maybe there is a good explanation somewhere?

My GenServer is a simple map – I mostly copied from the “KV” example in the docs.

defmodule MyApp.Registry do
  # Simple map to keep state
  @moduledoc false
  use GenServer
  require Logger

  def start_link(_args \\ %{}), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  @doc """
  Checks to see if the given key exists.
  """
  def exists?(key) when is_atom(key) do
    GenServer.call(__MODULE__, {:exists?})
  end

  @doc """
  Lists all registered keys.
  """
  def list() do
    GenServer.call(__MODULE__, {:list})
  end

  @doc """
  Looks up the info for the given `key`.
  """
  def lookup(key) when is_atom(key) do
    GenServer.call(__MODULE__, {:lookup, key})
  end

  @doc """
  Registers the `info` for the given `key`.
  """
  def register(key, info) when is_atom(key) do
    GenServer.call(__MODULE__, {:register, key, info})
  end

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

  @doc """
  Unregisters the given `key`
  """
  def unregister(key) when is_atom(key) do
    GenServer.call(__MODULE__, {:unregister, key})
  end

  @impl GenServer
  def init(state) do
    {:ok, state}
  end

  @impl true
  def handle_call({:exists?, key}, _from, state) do
    {:reply, Map.has_key?(state, key), state}
  end

  @impl true
  def handle_call({:list}, _from, state) do
    {:reply, Map.keys(state), state}
  end

  @impl true
  def handle_call({:lookup, key}, _from, state) do
    {:reply, Map.get(state, key), state}
  end

  @impl true
  def handle_call({:register, key, info}, _from, state) do
    {:reply, :ok, Map.put(state, key, info)}
  end

  @impl true
  def handle_call({:unregister, key}, _from, state) do
    {:reply, :ok, Map.delete(state, key)}
  end

  @impl true
  def handle_cast(:stop, state) do
    File.write!("/tmp/genserver_stop.txt", ":stop")
    {:stop, :normal, state}
  end
end

I have been exploring :dets tables, and I keep getting errors that the files were not closed properly, so this is why I want to implement some cleanup functionality when my application stops. I also tried adding some functions (callbacks?) to my application.exstop/1 and `prep_stop/``:

defmodule MyApp.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    IO.puts("Starting up myapp")
    children = [
      {Pockets.Registry, []}
    ]

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

  def prep_stop(_) do
    File.write!("/tmp/prep_stop.txt", "prep_stop")
  end

  def stop(_) do
    File.write!("/tmp/stop.txt", "stop")
  end
end

but those functions never seem to get called. When close iex for example, I want to run some cleanup code somewhere but so far I cannot figure out how to do this. Sorry if I am slow in understanding this. Actors and models seems very foreign for me.

I just discovered that when I type :init.stop in iex the stop and prep_stop functions get called!