lib/plugin_manager/state/plugin_ets.ex

defmodule MishkaInstaller.PluginETS do
  @moduledoc """

    ## ETS part to optimization state instead of dynamic supervisor and Genserver
    `public` — Read/Write available to all processes.
    `protected` — Read available to all processes. Only writable by owner process. This is the default.
    `private` — Read/Write limited to owner process.

    #### Essential functions in ETS
    1.  :ets.new
    ```elixir
      :ets.new(@tab, [:set, :named_table, :public, read_concurrency: true, write_concurrency: true])
    ```
    2.  :ets.insert
    3.  :ets.insert_new
    4.  :ets.lookup
    5.  :ets.match
    6.  :ets.match_object
    7.  :ets.select
    8.  :ets.fun2ms
    9.  :ets.delete
    10. :dets.open_file
    11. :ets.update_counter
    12. :ets.delete_all_objects
    13. :ets.tab2list - to spy on the data in the table
    14. :ets.update_counter
    15. :ets.all
    16. :ets.info

    ### Refs
    * https://www.erlang.org/doc/man/ets.html
    * https://dockyard.com/blog/2017/05/19/optimizing-elixir-and-phoenix-with-ets
    * https://learnyousomeerlang.com/ets
    * https://elixir-lang.org/getting-started/mix-otp/ets.html
    * https://elixirschool.com/en/lessons/storage/ets
    * https://github.com/TheFirstAvenger/ets
  """
  use GenServer
  require Logger
  @ets_table :plugin_ets_state
  @sync_with_database 100_000

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

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

  @impl true
  def init(_state) do
    Logger.info("The ETS state of plugin was staretd")
    Process.send_after(self(), :sync_with_database, @sync_with_database)

    {:ok,
     %{
       set:
         ETS.Set.new!(
           name: @ets_table,
           protection: :public,
           read_concurrency: true,
           write_concurrency: true
         )
     }}
  end

  @impl true
  def terminate(_reason, _state) do
    Logger.warn("Your ETS state of plugin was restarted by a problem")
    sync_with_database()
  end

  @impl true
  def handle_info(:sync_with_database, state) do
    Logger.info("Plugin ETS state was synced with database")
    Process.send_after(self(), :sync_with_database, @sync_with_database)
    {:noreply, state}
  end

  def push(%MishkaInstaller.PluginState{name: name, event: event} = state) do
    ETS.Set.put!(table(), {String.to_atom(name), event, state})
  end

  def get(module: name) do
    case ETS.Set.get(table(), String.to_atom(name)) do
      {:ok, {_key, _event, data}} -> data
      _ -> {:error, :get, :not_found}
    end
  end

  def get_all(event: event_name) do
    ETS.Set.match!(table(), {:_, event_name, :"$3"})
    |> Enum.map(&List.first/1)
  end

  def delete(module: module_name) do
    ETS.Set.delete(table(), String.to_atom(module_name))
  end

  def delete(event: event_name) do
    ETS.Set.match_delete(table(), {:_, event_name, :_})
  end

  defp table() do
    case ETS.Set.wrap_existing(@ets_table) do
      {:ok, set} ->
        set

      _ ->
        start_link([])
        table()
    end
  end

  def sync_with_database() do
    MishkaInstaller.Plugin.plugins()
    |> Enum.reject(&(&1.status in [:stopped]))
    |> Enum.map(&push/1)
  end
end