Genserver terminate strategy and re-bind data

Hi folks,
I have a DynamicSupervisor and under it, I have a Genserver. Each state, when wants to be started, checks Postgres if there is a record which same as the ID, it loads it to init so if there is not, it creates a simple map.

Imagine something happens and the Genserver is terminated, like pushing to state or something else.

  def terminate(reason, %PluginState{} = state) do
    MishkaInstaller.plugin_activity("read", state, "high", "throw")
    # TODO: Introduce a strategy for preparing again ( load from database, disk ?)
    Logger.warn(
      "#{Map.get(state, :name)} from #{Map.get(state, :event)} event of Plugins manager was Terminated,
      Reason of Terminate #{inspect(reason)}"
    )
  end

First option

Hence, I need to re-init or re-bind this state again, so I get the ID of record from the state is passed in the terminate function and call the DynamicSupervisor.start_child again to create a new state which loads from database.


Second option

If terminate is not :normal I call a function inside another Genserver which loads my DynamicSupervisor registry and check which key in my database does not exist in registry so if there is a record, so it creates a new state again.


Third option

I create a new normal Genserver as Cache which keeps all the {pid, id} of the DynamicSupervisor and if my terminate function is run, so it sends a request to this Cache Genserver and it checks which key or PID is missing in my registry, so after finding it, it creates a new state again.


Which strategy do you suggest me? If your suggestion is not there, please let me know what you prefer to do in this situation.

Thank you

1 Like

Do you have any suggestion about Genserver? :pensive:

Hey @shahryarjb I guess I’m a bit confused about your question.

When your GenServer is terminated, it’s going to die. I don’t understand what you mean by re-initing its state in terminate, that state is about to be discarded when it dies.

Depending on your restart strategy, the supervisor is going to restart the genserver and then init simply gets called again.

Hi Dear @benwilson512,
I am sorry because of confusing. Some Unknown errors maybe down or terminate my state, so if a problem happens I want to restart my state, not drop it completely, and bind it with my database data again. I am using strategy: :one_for_one.

I test a function like this:

@impl true
def handle_call({:pop, :module}, _from, %PluginState{} = state) do
  raise "Raising!"
end

I did not find a way to break a state, after it shows me a warning like this (I prefer to see an error instead of warning, but I could not force it to show an error instead of warning):

[warning] test_plug from joomla event of Plugins manager was Terminated,
      Reason of Terminate {%RuntimeError{message: "Raising!"}, [{MishkaInstaller.PluginState, :handle_call, 3, [file: 'lib/plugin_manager/state/plugin_state.ex', line: 94, error_info: %{module: Exception}]}, {:gen_server, :try_handle_call, 4, [file: 'gen_server.erl', line: 721]}, {:gen_server, :handle_msg, 6, [file: 'gen_server.erl', line: 750]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 226]}]}

So after that you can’t access to the state because it is going to be deleted completely, but I have stored all the previous data on my PSQL, so I want to start the state again with the data I saved in my database before.

Unfortunately, with the one_for_one strategy it doesn’t restart the state and call init again, it is the problem I have, and I want it to try 5 times and after that I need it to make a timeout and try 5 times again, after that it skips and do not try if it is not succeed.

Please if you require a more explanation let me know.
Thank you in advance.

Can you show your child spec you’re using for your genserver? The default value is restart: :permanent which should mean that the genserver is automatically re-created.

1 Like

Thank you

My Application

defmodule MishkaInstaller.Application do
  use Application
  @impl true
  def start(_type, _args) do
    plugin_runner_config = [
      strategy: :one_for_one,
      name: MishkaInstaller.Cache.PluginStateOtpRunner
    ]
    children = [
      {Registry, keys: :unique, name: MishkaInstaller.PluginStateRegistry},
      {DynamicSupervisor, plugin_runner_config}
    ]
    opts = [strategy: :one_for_one, name: MishkaInstaller.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

My Dynamic Supervisor

defmodule MishkaInstaller.PluginStateDynamicSupervisor do
  @spec start_job(%{id: atom(), type: atom()}) :: :ignore | {:error, any} | {:ok, :add | :edit, pid}
  def start_job(args) do
    DynamicSupervisor.start_child(MishkaInstaller.Cache.PluginStateOtpRunner, {MishkaInstaller.PluginState, args})
    |> case do
      {:ok, pid} -> {:ok, :add, pid}
      {:ok, pid, _any} -> {:ok, :add, pid}
      {:error, {:already_started, pid}} -> {:ok, :edit, pid}
      {:error, result} -> {:error, result}
    end
  end
end

It should be noted, even the record of my registry is deleted after the warning

Can you show how you’re using the registry? Your children should be getting restarted, but they’ll have a different pid. Are you using the registry to associate children with db ids or something?

2 Likes

Hi @benwilson512.

Yes, each of my records has a unique ID

def start_link(args) do
  {id, type} = {Map.get(args, :id), Map.get(args, :type)}
  GenServer.start_link(__MODULE__, default(id, type), name: via(id, type))
end

defp via(id, value) do
  {:via, Registry, {MishkaInstaller.PluginStateRegistry, id, value}}
end

IDs like :test_plugin_content or :test_plugin_login, example of registry

[%{id: :test_event, pid: #PID<0.7149.0>, type: :joomla}]

After a problem happens I can’t search by ID, and I lose my Registry record (one of them which has a problem not all of them, just a record)


Search in Registry

def get_plugin_pid(module_name) do
  case Registry.lookup(MishkaInstaller.PluginStateRegistry, module_name) do
    [] -> {:error, :get_plugin_pid}
    [{pid, _type}] -> {:ok, :get_plugin_pid, pid}
  end
end

Genserver function

def get(module: module_name) do
  case PSupervisor.get_plugin_pid(module_name) do
    {:ok, :get_plugin_pid, pid} -> GenServer.call(pid, {:pop, :module})
    {:error, :get_plugin_pid} -> {:error, :get, :not_found}
  end
end

def handle_call({:pop, :module}, _from, %PluginState{} = state) do
  {:reply, state, state}
end

Refs

I added the below code in my Genserver, and now when I have a problem it calls the init again.

def child_spec(process_name) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [process_name]},
      restart: :permanent
    }
end

I copy this code from this post:
https://levelup.gitconnected.com/genserver-dynamicsupervisor-and-registry-the-elixir-triad-to-manage-processes-a65d4c3351c1

Is it right? Or I am in the wrong way?

2 Likes